Skip to content

Commit

Permalink
Merge pull request #18 from ryanspradlin/features/prime
Browse files Browse the repository at this point in the history
Implement ability to directly prime the cache
  • Loading branch information
leebyron committed Apr 5, 2016
2 parents a57c44f + 5e21df6 commit 9b025ee
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 3 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ Clears the entire cache. To be used when some event results in unknown
invalidations across this particular `DataLoader`. Returns itself for
method chaining.

##### `prime(key, value)`

Primes the cache with the provided key and value. If the key already exists, no
change is made. (To forcefully prime the cache, clear the key first with
`loader.clear(key).prime(key, value)`.) Returns itself for method chaining.


## Using with GraphQL

Expand Down
91 changes: 91 additions & 0 deletions src/__tests__/dataloader-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,64 @@ describe('Primary API', () => {
expect(loadCalls).to.deep.equal([ [ 'A', 'B' ], [ 'A', 'B' ] ]);
});

it('allows priming the cache', async () => {
var [ identityLoader, loadCalls ] = idLoader();

identityLoader.prime('A', 'A');

var [ a, b ] = await Promise.all([
identityLoader.load('A'),
identityLoader.load('B')
]);

expect(a).to.equal('A');
expect(b).to.equal('B');

expect(loadCalls).to.deep.equal([ [ 'B' ] ]);
});

it('does not prime keys that already exist', async () => {
var [ identityLoader, loadCalls ] = idLoader();

identityLoader.prime('A', 'X');

var a1 = await identityLoader.load('A');
var b1 = await identityLoader.load('B');
expect(a1).to.equal('X');
expect(b1).to.equal('B');

identityLoader.prime('A', 'Y');
identityLoader.prime('B', 'Y');

var a2 = await identityLoader.load('A');
var b2 = await identityLoader.load('B');
expect(a2).to.equal('X');
expect(b2).to.equal('B');

expect(loadCalls).to.deep.equal([ [ 'B' ] ]);
});

it('allows forcefully priming the cache', async () => {
var [ identityLoader, loadCalls ] = idLoader();

identityLoader.prime('A', 'X');

var a1 = await identityLoader.load('A');
var b1 = await identityLoader.load('B');
expect(a1).to.equal('X');
expect(b1).to.equal('B');

identityLoader.clear('A').prime('A', 'Y');
identityLoader.clear('B').prime('B', 'Y');

var a2 = await identityLoader.load('A');
var b2 = await identityLoader.load('B');
expect(a2).to.equal('Y');
expect(b2).to.equal('Y');

expect(loadCalls).to.deep.equal([ [ 'B' ] ]);
});

});

describe('Represents Errors', () => {
Expand Down Expand Up @@ -249,6 +307,23 @@ describe('Represents Errors', () => {
expect(loadCalls).to.deep.equal([ [ 1 ] ]);
});

it('Handles priming the cache with an error', async () => {
var [ identityLoader, loadCalls ] = idLoader();

identityLoader.prime(1, new Error('Error: 1'));

var caughtErrorA;
try {
await identityLoader.load(1);
} catch (error) {
caughtErrorA = error;
}
expect(caughtErrorA).to.be.instanceof(Error);
expect((caughtErrorA: any).message).to.equal('Error: 1');

expect(loadCalls).to.deep.equal([]);
});

it('Can clear values from cache after errors', async () => {
var loadCalls = [];
var errorLoader = new DataLoader(keys => {
Expand Down Expand Up @@ -485,6 +560,22 @@ describe('Accepts options', () => {
expect(identityLoadCalls[0][0]).to.equal(keyA);
});

it('Allows priming the cache with an object key', async () => {
var [ identityLoader, loadCalls ] = idLoader({ cacheKeyFn: cacheKey });

var key1 = { id: 123 };
var key2 = { id: 123 };

identityLoader.prime(key1, key1);

var value1 = await identityLoader.load(key1);
var value2 = await identityLoader.load(key2);

expect(loadCalls).to.deep.equal([]);
expect(value1).to.equal(key1);
expect(value2).to.equal(key1);
});

});

describe('Accepts custom cacheMap instance', () => {
Expand Down
28 changes: 25 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@

// A Function, which when given an Array of keys, returns a Promise of an Array
// of values or Errors.
type BatchLoadFn<K, V> = (keys: Array<K>) => Promise<Array<V | Error>>
type BatchLoadFn<K, V> = (keys: Array<K>) => Promise<Array<V | Error>>;

type CacheMap<K, V> = {
get(key: K): V | void;
set(key: K, value: V): any;
delete(key: K): any;
clear(): any;
}
};

// Optionally turn off batching or caching or provide a cache key function or a
// custom cache instance.
Expand All @@ -26,7 +26,7 @@ type Options<K, V> = {
cache?: boolean,
cacheMap?: CacheMap<K, Promise<V>>,
cacheKeyFn?: (key: any) => any
}
};

/**
* A `DataLoader` creates a public API for loading data from a particular
Expand Down Expand Up @@ -157,6 +157,28 @@ export default class DataLoader<K, V> {
this._promiseCache.clear();
return this;
}

/**
* Adds the provied key and value to the cache. If the key already exists, no
* change is made. Returns itself for method chaining.
*/
prime(key: K, value: V): DataLoader<K, V> {
var cacheKeyFn = this._options && this._options.cacheKeyFn;
var cacheKey = cacheKeyFn ? cacheKeyFn(key) : key;

// Cache a rejected promise if the value is an Error, in order to match the
// behavior of load(key).
var promise = value instanceof Error ?
Promise.reject(value) :
Promise.resolve(value);

// Only add the key if it does not already exist.
if (this._promiseCache.get(cacheKey) === undefined) {
this._promiseCache.set(cacheKey, promise);
}

return this;
}
}

// Private: Enqueue a Job to be executed after all "PromiseJobs" Jobs.
Expand Down

0 comments on commit 9b025ee

Please sign in to comment.