diff --git a/Readme.md b/Readme.md index d4be5fdf..9ab6ab51 100644 --- a/Readme.md +++ b/Readme.md @@ -111,6 +111,7 @@ GitHub issues have been disabled to focus on pull requests. ([#731](https://gith - `wsdl_headers` (*Object*): Set HTTP headers with values to be sent on WSDL requests. - `wsdl_options` (*Object*): Set options for the request module on WSDL requests. If using the default request module, see [Request Config | Axios Docs](https://axios-http.com/docs/req_config). - `disableCache` (*boolean*): Prevents caching WSDL files and option objects. + - `wsdlCache` (*IWSDLCache*): Custom cache implementation. If not provided, defaults to caching WSDLs indefinitely. - `overridePromiseSuffix` (*string*): Override the default method name suffix of WSDL operations for Promise-based methods. If any WSDL operation name ends with `Async', you must use this option. (**Default:** `Async`) - `normalizeNames` (*boolean*): Replace non-identifier characters (`[^a-z$_0-9]`) with `_` in WSDL operation names. Note: Clients using WSDLs with two operations like `soap:method` and `soap-method` will be overwritten. In this case, you must use bracket notation instead (`client['soap:method']()`). - `namespaceArrayElements` (*boolean*): Support non-standard array semantics. JSON arrays of the form `{list: [{elem: 1}, {elem: 2}]}` will be marshalled into XML as `1 2`. If `false`, it would be marshalled into ` 1 2 `. (**Default:** `true`) diff --git a/src/soap.ts b/src/soap.ts index da6245ef..69ed693f 100644 --- a/src/soap.ts +++ b/src/soap.ts @@ -7,7 +7,8 @@ import debugBuilder from 'debug'; import { Client } from './client'; import * as _security from './security'; import { Server, ServerType } from './server'; -import { IOptions, IServerOptions, IServices } from './types'; +import { IOptions, IServerOptions, IServices, IWSDLCache } from './types'; +import { wsdlCacheSingleton } from './utils'; import { open_wsdl, WSDL } from './wsdl'; const debug = debugBuilder('node-soap:soap'); @@ -23,29 +24,22 @@ export { WSDL } from './wsdl'; type WSDLCallback = (error: any, result?: WSDL) => any; -function createCache() { - const cache: { - [key: string]: WSDL, - } = {}; - return (key: string, load: (cb: WSDLCallback) => any, callback: WSDLCallback) => { - if (!cache[key]) { - load((err, result) => { - if (err) { - return callback(err); - } - cache[key] = result; - callback(null, result); - }); - } else { - process.nextTick(() => { - callback(null, cache[key]); - }); - } - }; +function getFromCache(key: string, cache: IWSDLCache, load: (cb: WSDLCallback) => any, callback: WSDLCallback) { + if (!cache.has(key)) { + load((err, result) => { + if (err) { + return callback(err); + } + cache.set(key, result); + callback(null, result); + }); + } else { + process.nextTick(() => { + callback(null, cache.get(key)); + }); + } } -const getFromCache = createCache(); - function _requestWSDL(url: string, options: IOptions, callback: WSDLCallback) { if (typeof options === 'function') { callback = options; @@ -58,7 +52,13 @@ function _requestWSDL(url: string, options: IOptions, callback: WSDLCallback) { if (options.disableCache === true) { openWsdl(callback); } else { - getFromCache(url, openWsdl, callback); + let cache: IWSDLCache; + if (options.wsdlCache) { + cache = options.wsdlCache; + } else { + cache = wsdlCacheSingleton; + } + getFromCache(url, cache, openWsdl, callback); } } diff --git a/src/types.ts b/src/types.ts index 4e4f9819..518f8e7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import * as req from 'axios'; import { ReadStream } from 'fs'; +import { WSDL } from './wsdl'; export interface IHeaders { [k: string]: any; @@ -117,6 +118,8 @@ export type Option = IOptions; export interface IOptions extends IWsdlBaseOptions { /** don't cache WSDL files, request them every time. */ disableCache?: boolean; + /** Custom cache implementation. If not provided, defaults to caching WSDLs indefinitely. */ + wsdlCache?: IWSDLCache; /** override the SOAP service's host specified in the .wsdl file. */ endpoint?: string; /** set specific key instead of
. */ @@ -164,3 +167,9 @@ export interface IMTOMAttachments { headers: { [key: string]: string }, }>; } + +export interface IWSDLCache { + has(key: string): boolean; + get(key: string): WSDL; + set(key: string, wsdl: WSDL): void; +} diff --git a/src/utils.ts b/src/utils.ts index bfc00ea2..8025ddd1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import * as crypto from 'crypto'; -import { IMTOMAttachments } from './types'; +import { IMTOMAttachments, IWSDLCache } from './types'; +import { WSDL } from './wsdl'; export function passwordDigest(nonce: string, created: string, password: string): string { // digest = base64 ( sha1 ( nonce + created + password ) ) @@ -113,3 +114,29 @@ export function parseMTOMResp(payload: Buffer, boundary: string, callback: (err? }) .catch(callback); } + +class DefaultWSDLCache implements IWSDLCache { + private cache: { + [key: string]: WSDL, + }; + constructor() { + this.cache = {}; + } + + public has(key: string): boolean { + return !!this.cache[key]; + } + + public get(key: string): WSDL { + return this.cache[key]; + } + + public set(key: string, wsdl: WSDL) { + this.cache[key] = wsdl; + } + + public clear() { + this.cache = {}; + } +} +export const wsdlCacheSingleton = new DefaultWSDLCache(); diff --git a/test/client-options-wsdlcache-test.js b/test/client-options-wsdlcache-test.js new file mode 100644 index 00000000..f0ac8762 --- /dev/null +++ b/test/client-options-wsdlcache-test.js @@ -0,0 +1,86 @@ +'use strict'; +var soap = require('..'), + assert = require('assert'), + sinon = require('sinon'), + utils = require('../lib/utils'), + wsdl = require("../lib/wsdl"); +describe('SOAP Client - WSDL Cache', function() { + var sandbox = sinon.createSandbox(); + var wsdlUri = __dirname + '/wsdl/Dummy.wsdl'; + beforeEach(function () { + sandbox.spy(wsdl, 'open_wsdl'); + }); + afterEach(function () { + sandbox.restore(); + }); + + it('should use default cache if not provided', function(done) { + // ensure cache is empty to prevent impacts to this case + // if other test already loaded this WSDL + utils.wsdlCacheSingleton.clear(); + + // cache miss + soap.createClient(wsdlUri, {}, function(err, clientFirstCall) { + if (err) return done(err); + assert.strictEqual(wsdl.open_wsdl.callCount, 1); + + // hits cache + soap.createClient(wsdlUri, {}, function(err, clientSecondCall) { + if (err) return done(err); + assert.strictEqual(wsdl.open_wsdl.callCount, 1); + + // disabled cache + soap.createClient(wsdlUri, {disableCache: true}, function(err, clientSecondCall) { + if (err) return done(err); + assert.strictEqual(wsdl.open_wsdl.callCount, 2); + done(); + }); + }); + }); + }); + + it('should use the provided WSDL cache', function(done) { + /** @type {IWSDLCache} */ + var dummyCache = { + has: function () {}, + get: function () {}, + set: function () {}, + }; + sandbox.stub(dummyCache, 'has'); + sandbox.stub(dummyCache, 'get'); + sandbox.stub(dummyCache, 'set'); + dummyCache.has.returns(false); + var options = { + wsdlCache: dummyCache, + }; + soap.createClient(wsdlUri, options, function(err, clientFirstCall) { + assert.strictEqual(dummyCache.has.callCount, 1); + assert.strictEqual(dummyCache.get.callCount, 0); + assert.strictEqual(dummyCache.set.callCount, 1); + // cache miss + assert.strictEqual(wsdl.open_wsdl.callCount, 1); + + var cacheEntry = dummyCache.set.firstCall.args; + assert.deepStrictEqual(cacheEntry[0], wsdlUri); + + var cachedWSDL = cacheEntry[1]; + assert.ok(cachedWSDL instanceof wsdl.WSDL); + assert.deepStrictEqual(clientFirstCall.wsdl, cachedWSDL); + + sandbox.reset(); + dummyCache.has.returns(true); + dummyCache.get.returns(cachedWSDL); + + soap.createClient(wsdlUri, options, function(err, clientSecondCall) { + // hits cache + assert.strictEqual(wsdl.open_wsdl.callCount, 0); + assert.strictEqual(dummyCache.has.callCount, 1); + assert.strictEqual(dummyCache.get.callCount, 1); + assert.deepStrictEqual(dummyCache.get.firstCall.args, [wsdlUri]); + assert.strictEqual(dummyCache.set.callCount, 0); + assert.deepStrictEqual(clientSecondCall.wsdl, cachedWSDL); + done(); + }); + }); + }); +});