diff --git a/src/__tests__/dataloader-test.js b/src/__tests__/dataloader-test.js index b86e73b..9e6ef7f 100644 --- a/src/__tests__/dataloader-test.js +++ b/src/__tests__/dataloader-test.js @@ -378,6 +378,56 @@ describe('Accepts any kind of key', () => { }); +describe('Accepts object key in custom serializeKey function', () => { + function serializeKey(key) { + var result; + if (typeof key === 'object') { + result = Object.keys(key).sort().map(k => k + ':' + key[k]).join('-'); + } else { + result = String(key); + } + return result; + } + + it('Accepts objects with simple key', async () => { + var keyA = '1234'; + var identityLoadCalls = []; + var identityLoader = new DataLoader(keys => { + identityLoadCalls.push(keys); + return Promise.resolve(keys); + }, {serializeKey}); + + var valueA = await identityLoader.load(keyA); + expect(valueA).to.equal(keyA); + }); + + it('Accepts objects with different order of keys', async () => { + var keyA = { a: 123, b: 321 }; + var keyB = { b: 321, a: 123 }; + + var identityLoadCalls = []; + var identityLoader = new DataLoader(keys => { + identityLoadCalls.push(keys); + return Promise.resolve(keys); + }, {serializeKey}); + + // Fetches as expected + + var [ valueA, valueB ] = await Promise.all([ + identityLoader.load(keyA), + identityLoader.load(keyB), + ]); + + expect(valueA).to.equal(keyA); + expect(valueB).to.equal(valueA); + + expect(identityLoadCalls).to.have.length(1); + expect(identityLoadCalls[0]).to.have.length(1); + expect(identityLoadCalls[0][0]).to.equal(keyA); + }); + +}); + describe('Accepts options', () => { // Note: mirrors 'batches multiple requests' above. diff --git a/src/index.js b/src/index.js index b8ca692..7c8a98e 100644 --- a/src/index.js +++ b/src/index.js @@ -12,8 +12,9 @@ // of values or Errors. type BatchLoadFn = (keys: Array) => Promise> +type SerializeKeyFn = (key: K) => K // Optionally turn off batching or caching. -type Options = { batch?: boolean, cache?: boolean } +type Options = { batch?: boolean, cache?: boolean, keyFn?: SerializeKeyFn } /** * A `DataLoader` creates a public API for loading data from a particular @@ -39,6 +40,9 @@ export default class DataLoader { this._batchLoadFn = batchLoadFn; this._options = options; this._promiseCache = new Map(); + this._serializeKey = options && options.serializeKey ? + options.serializeKey : + this._defaultSerializeKey; this._queue = []; } @@ -47,6 +51,11 @@ export default class DataLoader { _options: ?Options; _promiseCache: Map>; _queue: LoaderQueue; + _serializeKey: SerializeKeyFn; + + _defaultSerializeKey(key: K): K { + return key; + } /** * Loads a key, returning a `Promise` for the value represented by that key. @@ -58,15 +67,14 @@ export default class DataLoader { `but got: ${key}.` ); } - // Determine options var options = this._options; var shouldBatch = !options || options.batch !== false; var shouldCache = !options || options.cache !== false; - + var cacheKey = this._serializeKey(key); // If caching and there is a cache-hit, return cached Promise. - if (shouldCache && this._promiseCache.has(key)) { - return this._promiseCache.get(key); + if (shouldCache && this._promiseCache.has(cacheKey)) { + return this._promiseCache.get(cacheKey); } // Otherwise, produce a new Promise for this value. @@ -90,7 +98,7 @@ export default class DataLoader { // If caching, cache this promise. if (shouldCache) { - this._promiseCache.set(key, promise); + this._promiseCache.set(cacheKey, promise); } return promise; @@ -124,7 +132,7 @@ export default class DataLoader { * method chaining. */ clear(key: K): DataLoader { - this._promiseCache.delete(key); + this._promiseCache.delete(this._serializeKey(key)); return this; } @@ -204,6 +212,7 @@ function dispatchQueue(loader: DataLoader) { `not return a Promise of an Array: ${values}.` ); } + if (values.length !== keys.length) { throw new Error( 'DataLoader must be constructed with a function which accepts ' +