Skip to content

Commit 01c829d

Browse files
authored
Resolve all options during construction (#226)
1 parent cde0cd8 commit 01c829d

File tree

5 files changed

+126
-68
lines changed

5 files changed

+126
-68
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,10 @@ Create a new `DataLoader` given a batch loading function and options.
344344
| Option Key | Type | Default | Description |
345345
| ---------- | ---- | ------- | ----------- |
346346
| *batch* | Boolean | `true` | Set to `false` to disable batching, invoking `batchLoadFn` with a single load key. This is equivalent to setting `maxBatchSize` to `1`.
347-
| *maxBatchSize* | Number | `Infinity` | Limits the number of items that get passed in to the `batchLoadFn`.
348-
| *cache* | Boolean | `true` | Set to `false` to disable memoization caching, creating a new Promise and new key in the `batchLoadFn` for every load of the same key.
347+
| *maxBatchSize* | Number | `Infinity` | Limits the number of items that get passed in to the `batchLoadFn`. May be set to `1` to disable batching.
348+
| *cache* | Boolean | `true` | Set to `false` to disable memoization caching, creating a new Promise and new key in the `batchLoadFn` for every load of the same key. This is equivalent to setting `cacheMap` to `null`.
349349
| *cacheKeyFn* | Function | `key => key` | Produces cache key for a given load key. Useful when objects are keys and two objects should be considered equivalent.
350-
| *cacheMap* | Object | `new Map()` | Instance of [Map][] (or an object with a similar API) to be used as cache.
350+
| *cacheMap* | Object | `new Map()` | Instance of [Map][] (or an object with a similar API) to be used as cache. May be set to `null` to disable caching.
351351

352352
##### `load(key)`
353353

src/__tests__/abuse.test.js

+20
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,24 @@ describe('Provides descriptive error messages for API abuse', () => {
170170
'Custom cacheMap missing methods: set, delete, clear'
171171
);
172172
});
173+
174+
it('Requires a number for maxBatchSize', () => {
175+
expect(() =>
176+
// $FlowExpectError
177+
new DataLoader(async keys => keys, { maxBatchSize: null })
178+
).toThrow('maxBatchSize must be a positive number: null');
179+
});
180+
181+
it('Requires a positive number for maxBatchSize', () => {
182+
expect(() =>
183+
new DataLoader(async keys => keys, { maxBatchSize: 0 })
184+
).toThrow('maxBatchSize must be a positive number: 0');
185+
});
186+
187+
it('Requires a function for cacheKeyFn', () => {
188+
expect(() =>
189+
// $FlowExpectError
190+
new DataLoader(async keys => keys, { cacheKeyFn: null })
191+
).toThrow('cacheKeyFn must be a function: null');
192+
});
173193
});

src/__tests__/dataloader.test.js

+24
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ describe('Primary API', () => {
4646
expect(that).toBe(loader);
4747
});
4848

49+
it('references the loader as "this" in the cache key function', async () => {
50+
let that;
51+
const loader = new DataLoader<number, number>(async keys => keys, {
52+
cacheKeyFn(key) {
53+
that = this;
54+
return key;
55+
}
56+
});
57+
58+
// Trigger the cache key function
59+
await loader.load(1);
60+
61+
expect(that).toBe(loader);
62+
});
63+
4964
it('supports loading multiple keys in one call', async () => {
5065
const identityLoader = new DataLoader<number, number>(async keys => keys);
5166

@@ -658,6 +673,15 @@ describe('Accepts options', () => {
658673
]);
659674
});
660675

676+
it('cacheMap may be set to null to disable cache', async () => {
677+
const [ identityLoader, loadCalls ] = idLoader<string>({ cacheMap: null });
678+
679+
await identityLoader.load('A');
680+
await identityLoader.load('A');
681+
682+
expect(loadCalls).toEqual([ [ 'A' ], [ 'A' ] ]);
683+
});
684+
661685
it('Does not interact with a cache when cache is disabled', () => {
662686
const promiseX = Promise.resolve('X');
663687
const cacheMap = new Map([ [ 'X', promiseX ] ]);

src/index.d.ts

+13-16
Original file line numberDiff line numberDiff line change
@@ -78,39 +78,36 @@ declare namespace DataLoader {
7878
export type Options<K, V, C = K> = {
7979

8080
/**
81-
* Default `true`. Set to `false` to disable batching,
82-
* instead immediately invoking `batchLoadFn` with a
83-
* single load key.
81+
* Default `true`. Set to `false` to disable batching, invoking
82+
* `batchLoadFn` with a single load key. This is equivalent to setting
83+
* `maxBatchSize` to `1`.
8484
*/
8585
batch?: boolean,
8686

8787
/**
88-
* Default `Infinity`. Limits the number of items that get
89-
* passed in to the `batchLoadFn`.
88+
* Default `Infinity`. Limits the number of items that get passed in to the
89+
* `batchLoadFn`. May be set to `1` to disable batching.
9090
*/
9191
maxBatchSize?: number;
9292

9393
/**
94-
* Default `true`. Set to `false` to disable memoization caching,
95-
* instead creating a new Promise and new key in the `batchLoadFn` for every
96-
* load of the same key.
94+
* Default `true`. Set to `false` to disable memoization caching, creating a
95+
* new Promise and new key in the `batchLoadFn` for every load of the same
96+
* key. This is equivalent to setting `cacheMap` to `null`.
9797
*/
9898
cache?: boolean,
9999

100100
/**
101-
* A function to produce a cache key for a given load key.
102-
* Defaults to `key => key`. Useful to provide when JavaScript
103-
* objects are keys and two similarly shaped objects should
104-
* be considered equivalent.
101+
* Default `key => key`. Produces cache key for a given load key. Useful
102+
* when objects are keys and two objects should be considered equivalent.
105103
*/
106104
cacheKeyFn?: (key: K) => C,
107105

108106
/**
109-
* An instance of Map (or an object with a similar API) to
110-
* be used as the underlying cache for this loader.
111-
* Default `new Map()`.
107+
* Default `new Map()`. Instance of `Map` (or an object with a similar API)
108+
* to be used as cache. May be set to `null` to disable caching.
112109
*/
113-
cacheMap?: CacheMap<C, Promise<V>>;
110+
cacheMap?: CacheMap<C, Promise<V>> | null;
114111
}
115112
}
116113

src/index.js

+66-49
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type Options<K, V, C = K> = {
1919
maxBatchSize?: number;
2020
cache?: boolean;
2121
cacheKeyFn?: (key: K) => C;
22-
cacheMap?: CacheMap<C, Promise<V>>;
22+
cacheMap?: CacheMap<C, Promise<V>> | null;
2323
};
2424

2525
// If a custom cache is provided, it must be of this type (a subset of ES6 Map).
@@ -52,15 +52,17 @@ class DataLoader<K, V, C = K> {
5252
);
5353
}
5454
this._batchLoadFn = batchLoadFn;
55-
this._options = options;
56-
this._promiseCache = getValidCacheMap(options);
55+
this._maxBatchSize = getValidMaxBatchSize(options);
56+
this._cacheKeyFn = getValidCacheKeyFn(options);
57+
this._cacheMap = getValidCacheMap(options);
5758
this._batch = null;
5859
}
5960

6061
// Private
6162
_batchLoadFn: BatchLoadFn<K, V>;
62-
_options: ?Options<K, V, C>;
63-
_promiseCache: ?CacheMap<C, Promise<V>>;
63+
_maxBatchSize: number;
64+
_cacheKeyFn: K => C;
65+
_cacheMap: CacheMap<C, Promise<V>> | null;
6466
_batch: Batch<K, V> | null;
6567

6668
/**
@@ -74,15 +76,13 @@ class DataLoader<K, V, C = K> {
7476
);
7577
}
7678

77-
// Determine options
78-
var options = this._options;
7979
var batch = getCurrentBatch(this);
80-
var cache = this._promiseCache;
81-
var cacheKey = getCacheKey(options, key);
80+
var cacheMap = this._cacheMap;
81+
var cacheKey = this._cacheKeyFn(key);
8282

8383
// If caching and there is a cache-hit, return cached Promise.
84-
if (cache) {
85-
var cachedPromise = cache.get(cacheKey);
84+
if (cacheMap) {
85+
var cachedPromise = cacheMap.get(cacheKey);
8686
if (cachedPromise) {
8787
var cacheHits = batch.cacheHits || (batch.cacheHits = []);
8888
return new Promise(resolve => {
@@ -99,8 +99,8 @@ class DataLoader<K, V, C = K> {
9999
});
100100

101101
// If caching, cache this promise.
102-
if (cache) {
103-
cache.set(cacheKey, promise);
102+
if (cacheMap) {
103+
cacheMap.set(cacheKey, promise);
104104
}
105105

106106
return promise;
@@ -146,10 +146,10 @@ class DataLoader<K, V, C = K> {
146146
* method chaining.
147147
*/
148148
clear(key: K): this {
149-
var cache = this._promiseCache;
150-
if (cache) {
151-
var cacheKey = getCacheKey(this._options, key);
152-
cache.delete(cacheKey);
149+
var cacheMap = this._cacheMap;
150+
if (cacheMap) {
151+
var cacheKey = this._cacheKeyFn(key);
152+
cacheMap.delete(cacheKey);
153153
}
154154
return this;
155155
}
@@ -160,9 +160,9 @@ class DataLoader<K, V, C = K> {
160160
* method chaining.
161161
*/
162162
clearAll(): this {
163-
var cache = this._promiseCache;
164-
if (cache) {
165-
cache.clear();
163+
var cacheMap = this._cacheMap;
164+
if (cacheMap) {
165+
cacheMap.clear();
166166
}
167167
return this;
168168
}
@@ -174,12 +174,12 @@ class DataLoader<K, V, C = K> {
174174
* To prime the cache with an error at a key, provide an Error instance.
175175
*/
176176
prime(key: K, value: V | Error): this {
177-
var cache = this._promiseCache;
178-
if (cache) {
179-
var cacheKey = getCacheKey(this._options, key);
177+
var cacheMap = this._cacheMap;
178+
if (cacheMap) {
179+
var cacheKey = this._cacheKeyFn(key);
180180

181181
// Only add the key if it does not already exist.
182-
if (cache.get(cacheKey) === undefined) {
182+
if (cacheMap.get(cacheKey) === undefined) {
183183
// Cache a rejected promise if the value is an Error, in order to match
184184
// the behavior of load(key).
185185
var promise;
@@ -191,7 +191,7 @@ class DataLoader<K, V, C = K> {
191191
} else {
192192
promise = Promise.resolve(value);
193193
}
194-
cache.set(cacheKey, promise);
194+
cacheMap.set(cacheKey, promise);
195195
}
196196
}
197197
return this;
@@ -251,21 +251,15 @@ type Batch<K, V> = {
251251
// Private: Either returns the current batch, or creates and schedules a
252252
// dispatch of a new batch for the given loader.
253253
function getCurrentBatch<K, V>(loader: DataLoader<K, V, any>): Batch<K, V> {
254-
var options = loader._options;
255-
var maxBatchSize =
256-
(options && options.maxBatchSize) ||
257-
(options && options.batch === false ? 1 : 0);
258-
259254
// If there is an existing batch which has not yet dispatched and is within
260255
// the limit of the batch size, then return it.
261256
var existingBatch = loader._batch;
262257
if (
263258
existingBatch !== null &&
264259
!existingBatch.hasDispatched &&
265-
(maxBatchSize === 0 ||
266-
(existingBatch.keys.length < maxBatchSize &&
267-
(!existingBatch.cacheHits ||
268-
existingBatch.cacheHits.length < maxBatchSize)))
260+
existingBatch.keys.length < loader._maxBatchSize &&
261+
(!existingBatch.cacheHits ||
262+
existingBatch.cacheHits.length < loader._maxBatchSize)
269263
) {
270264
return existingBatch;
271265
}
@@ -369,34 +363,57 @@ function resolveCacheHits(batch: Batch<any, any>) {
369363
}
370364
}
371365

372-
// Private: produce a cache key for a given key (and options)
373-
function getCacheKey<K, V, C>(
374-
options: ?Options<K, V, C>,
375-
key: K
376-
): C {
366+
// Private: given the DataLoader's options, produce a valid max batch size.
367+
function getValidMaxBatchSize(options: ?Options<any, any, any>): number {
368+
var shouldBatch = !options || options.batch !== false;
369+
if (!shouldBatch) {
370+
return 1;
371+
}
372+
var maxBatchSize = options && options.maxBatchSize;
373+
if (maxBatchSize === undefined) {
374+
return Infinity;
375+
}
376+
if (typeof maxBatchSize !== 'number' || maxBatchSize < 1) {
377+
throw new TypeError(
378+
`maxBatchSize must be a positive number: ${(maxBatchSize: any)}`
379+
);
380+
}
381+
return maxBatchSize;
382+
}
383+
384+
// Private: given the DataLoader's options, produce a cache key function.
385+
function getValidCacheKeyFn<K, C>(options: ?Options<K, any, C>): (K => C) {
377386
var cacheKeyFn = options && options.cacheKeyFn;
378-
return cacheKeyFn ? cacheKeyFn(key) : (key: any);
387+
if (cacheKeyFn === undefined) {
388+
return (key => key: any);
389+
}
390+
if (typeof cacheKeyFn !== 'function') {
391+
throw new TypeError(`cacheKeyFn must be a function: ${(cacheKeyFn: any)}`);
392+
}
393+
return cacheKeyFn;
379394
}
380395

381396
// Private: given the DataLoader's options, produce a CacheMap to be used.
382397
function getValidCacheMap<K, V, C>(
383398
options: ?Options<K, V, C>
384-
): ?CacheMap<C, Promise<V>> {
399+
): CacheMap<C, Promise<V>> | null {
385400
var shouldCache = !options || options.cache !== false;
386401
if (!shouldCache) {
387402
return null;
388403
}
389404
var cacheMap = options && options.cacheMap;
390-
if (!cacheMap) {
405+
if (cacheMap === undefined) {
391406
return new Map();
392407
}
393-
var cacheFunctions = [ 'get', 'set', 'delete', 'clear' ];
394-
var missingFunctions = cacheFunctions
395-
.filter(fnName => cacheMap && typeof cacheMap[fnName] !== 'function');
396-
if (missingFunctions.length !== 0) {
397-
throw new TypeError(
398-
'Custom cacheMap missing methods: ' + missingFunctions.join(', ')
399-
);
408+
if (cacheMap !== null) {
409+
var cacheFunctions = [ 'get', 'set', 'delete', 'clear' ];
410+
var missingFunctions = cacheFunctions
411+
.filter(fnName => cacheMap && typeof cacheMap[fnName] !== 'function');
412+
if (missingFunctions.length !== 0) {
413+
throw new TypeError(
414+
'Custom cacheMap missing methods: ' + missingFunctions.join(', ')
415+
);
416+
}
400417
}
401418
return cacheMap;
402419
}

0 commit comments

Comments
 (0)