Skip to content

Commit

Permalink
Implement stale feature
Browse files Browse the repository at this point in the history
  • Loading branch information
moeriki committed Nov 21, 2017
1 parent b615daf commit e28cb95
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 29 deletions.
67 changes: 48 additions & 19 deletions lib/index.js
Expand Up @@ -20,6 +20,10 @@ function memoize(func, keyvOpts, opts) {
? opts.ttl
: constant(opts ? opts.ttl : undefined)
;
const stale = opts && typeof opts.stale === 'number'
? opts.stale
: undefined
;

/**
* This can be better. Check:
Expand All @@ -29,40 +33,49 @@ function memoize(func, keyvOpts, opts) {
* @return {Promise<object>} { expires:number, value:* }
*/
function getRaw(key) {
const absKey = keyv._getKeyPrefix(key);
const store = keyv.opts.store;
return Promise.resolve()
.then(() => store.get(absKey))
.then(() => keyv.opts.store.get(keyv._getKeyPrefix(key)))
.then(data => typeof data === 'string' ? JSONB.parse(data) : data)
;
}

/**
* @param {string} key
* @return {Promise<*>}
* @return {Promise<*>} value
*/
function getFreshValue(args) {
return Promise.resolve(func.apply(null, args));
}

/**
* @param {string} key
* @return {Promise<*>}
* @return {Promise<*>} value
* @throws if not found
*/
function getStoredValue(key) {
return getRaw(key).then((data) => {
return getRaw(key).then(data => {
if (!data || data.value === undefined) {
throw new Error('Not found');
}
return data.value;
});
}

/**
* @param {string} key
* @param {Array<*>} args
* @return {Promise<*>} value
*/
function refreshValue(key, args) {
return getFreshValue(args).then(value =>
updateStoredValue(key, value).then(() => value)
);
}

/**
* @param {string} key
* @param {*} value
* @return {Promise}
* @return {Promise} resolves when updated
*/
function updateStoredValue(key, value) {
return keyv.set(key, value, ttl(value));
Expand All @@ -75,25 +88,41 @@ function memoize(func, keyvOpts, opts) {
const args = Array.from(arguments);
const key = resolver.apply(null, args);

if (pending[key]) {
if (pending[key] !== undefined) {
return pAny([
getStoredValue(key),
pending[key],
pending[key]
]);
}

pending[key] = getRaw(key).then(data => {
if (!data || data.value === undefined) {
return getFreshValue(args)
.then((value) => updateStoredValue(key, value)
.then(() => {
pending[key] = null;
return value;
})
)
;
const hasValue = data ? data.value !== undefined : false;
const hasExpires = hasValue && typeof data.expires === 'number';
const ttl = hasExpires ? data.expires - Date.now() : undefined;
const isExpired = stale === undefined && hasExpires && ttl < 0;
const isStale = stale !== undefined && hasExpires && ttl < stale;
if (hasValue && !isExpired && !isStale) {
pending[key] = undefined;
return data.value;
}
return data.value;
return Promise.resolve(isExpired ? keyv.delete(key) : undefined).then(() => {
const pendingRefresh = refreshValue(key, args);
if (isStale) {
pendingRefresh
.then(value => {
keyv.emit('memoize.fresh.value', value);
})
.catch(err => {
keyv.emit('memoize.fresh.error', err);
})
;
return data.value;
}
return pendingRefresh.then(value => {
pending[key] = undefined;
return value;
});
});
});

return pending[key];
Expand Down
3 changes: 3 additions & 0 deletions test/__snapshots__/index.test.js.snap
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`memoizedKeyv should emit on stale refresh error 1`] = `[Error: NOPE]`;
72 changes: 62 additions & 10 deletions test/index.test.js
Expand Up @@ -6,11 +6,6 @@ const Keyv = require('keyv');

const memoize = require('../lib');

const asyncSum = (...numbers) => numbers.reduce(
(wait, n) => wait.then(sum => sum + n),
Promise.resolve(0)
);

const deferred = () => {
const defer = {};
defer.promise = new Promise((resolve, reject) => {
Expand All @@ -20,9 +15,18 @@ const deferred = () => {
return defer;
};

const syncSum = (...numbers) => numbers.reduce((sum, n) => sum + n, 0);

describe('memoizedKeyv', () => {
let syncSum;
let asyncSum;

beforeEach(() => {
syncSum = (...numbers) => numbers.reduce((sum, n) => sum + n, 0);
asyncSum = jest.fn((...numbers) => numbers.reduce(
(wait, n) => wait.then(sum => sum + n),
Promise.resolve(0)
));
});

it('should store result as arg0', async () => {
const memoizedSum = memoize(asyncSum);
await memoizedSum(1, 2);
Expand Down Expand Up @@ -51,11 +55,59 @@ describe('memoizedKeyv', () => {
});

it('should return cached result', async () => {
const spy = jest.fn(asyncSum);
const memoized = memoize(spy);
const memoized = memoize(asyncSum);
await memoized.keyv.set('5', 5);
await memoized(5);
expect(spy).not.toHaveBeenCalled();
expect(asyncSum).not.toHaveBeenCalled();
});

it('should return fresh result', async () => {
const memoizedSum = memoize(asyncSum, null, { stale: 10 });
memoizedSum.keyv.set('5', 5, 20);
expect(await memoizedSum(5)).toBe(5);
expect(asyncSum).not.toHaveBeenCalled();
});

it('should return stale result but refresh', async done => {
const memoizedSum = memoize(asyncSum, null, { stale: 10 });
await memoizedSum.keyv.set('1', 1, 5);
const sum = await memoizedSum(1, 2);
expect(sum).toBe(1);
expect(asyncSum).toHaveBeenCalledWith(1, 2);
memoizedSum.keyv.on('memoize.fresh.value', value => {
expect(value).toBe(3);
done();
});
});

it('should emit on stale refresh error', async done => {
asyncSum.mockImplementation(() => Promise.reject(new Error('NOPE')));
const memoizedSum = memoize(asyncSum, null, { stale: 10 });
await memoizedSum.keyv.set('1', 1, 5);
memoizedSum(1);
memoizedSum.keyv.on('memoize.fresh.error', err => {
expect(err).toMatchSnapshot();
done();
});
});

it('should return cached result if a stale refresh is pending', async () => {
const defer = deferred();
asyncSum.mockImplementation(() => defer.promise);
const memoizedSum = memoize(asyncSum, null, { stale: 10 });
await memoizedSum.keyv.set('1', 1, 5);
expect(await memoizedSum(1)).toBe(1);
expect(await memoizedSum(1)).toBe(1);
expect(asyncSum).toHaveBeenCalledTimes(1);
});

it('should delete expired result and return fresh result', async done => {
const memoizedSum = memoize(asyncSum);
await memoizedSum.keyv.set('1', 1, 1);
setTimeout(async () => {
expect(await memoizedSum(1, 2)).toBe(3);
done();
}, 5);
});

it('should not store result if undefined', async () => {
Expand Down

0 comments on commit e28cb95

Please sign in to comment.