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 `
`. If `false`, it would be marshalled into `
`. (**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(); + }); + }); + }); +});