Skip to content

Commit

Permalink
feat: add managed TTL version of the LRU store
Browse files Browse the repository at this point in the history
BREAKING CHANGE: the entry point is now an object with two classes.
  • Loading branch information
Mateu Aguiló Bosch committed Jul 23, 2018
1 parent 87c248d commit 8382d0c
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 4 deletions.
7 changes: 7 additions & 0 deletions flow/types/ExpirableItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// @flow

export type ExpirableItem<T> = {
// Timestamp in millis (like Date.now()) when this item is no longer usable.
expires?: number,
data: T,
};
9 changes: 8 additions & 1 deletion src/KeyvLru.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const EventEmitter = require('events');
class KeyvLru extends EventEmitter implements MapInterface {
// @TODO: Type this in a less generic way.
cache: Object;
defaultTtl: ?number;

constructor(
options: {
Expand All @@ -21,7 +22,13 @@ class KeyvLru extends EventEmitter implements MapInterface {
} = { max: 500 }
) {
super();
this.cache = lru(options.max, options.notify, options.ttl, options.expire);
this.defaultTtl = options.ttl;
this.cache = lru(
options.max,
options.notify,
this.defaultTtl,
options.expire
);
if (options.notify) {
// This seems like a weird construct, but this is because tiny-lru passes
// the execution of this.cache.onchange to process.nextTick. nextTick
Expand Down
2 changes: 1 addition & 1 deletion src/KeyvLru.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('KeyvLru', () => {
jest.spyOn(sut.cache, 'set');
sut.set('foo', { bar: true });
expect(sut.cache.set).toHaveBeenCalledWith('foo', { bar: true });
expect(sut.get('foo')).toEqual({ bar: true });
expect(sut.cache.get('foo')).toEqual({ bar: true });
});

test('delete', () => {
Expand Down
76 changes: 76 additions & 0 deletions src/KeyvLruManagedTtl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// @flow

import type { ExpirableItem } from '../flow/types/ExpirableItem';

const lru = require('tiny-lru');
const KeyvLru = require('./KeyvLru');

/**
* An adaptor from tiny-lru to a Map API.
*
* This version uses a managed TTL strategy instead of tiny-lru. This is useful
* for serverless architectures. tiny-lru expires entries based on timers, that
* means that the event loop is not empty when the lambda function is finished.
* That blocks the end of the execution.
*
* This implementation will store the expiration time along with the cached data
* and it will deleter expired items upon retrieval. Alternatively there is an
* evictExpired method that will evict all the expired items.
*/
class KeyvLruManagedTtl<T> extends KeyvLru {
cache: Object;
defaultTtl: ?number;

constructor(
options: {
max: number,
notify?: boolean,
ttl?: number,
} = { max: 500 }
) {
super();
this.cache = lru(options.max, options.notify);
}

get(key: string): ?T {
const item: ?ExpirableItem<T> = this.cache.get(key);
if (!item) {
return undefined;
}
if (typeof item.expires === 'undefined') {
return item.data;
}
if (item.expires > Date.now()) {
// It's not expired yet.
return item.data;
}
// Schedule removal and return undefined.
process.nextTick(() => this.delete(key));

return undefined;
}

set(key: string, value: T, ttl?: number): 1 | 0 {
const item: ExpirableItem<T> = { data: value };
const theTtl = ttl || this.defaultTtl;
if (typeof theTtl !== 'undefined') {
item.expires = theTtl + Date.now();
}
this.cache.set(key, item);
return 1;
}

/**
* Loop through all the cache entries and get them.
*
* This has the effect to delete all the expired cache entries.
*
* @return {void}
*/
evictExpired() {
// Getting the entries will cause evition on expired entries.
Object.keys(this.cache.cache).forEach(this.get.bind(this));
}
}

module.exports = KeyvLruManagedTtl;
91 changes: 91 additions & 0 deletions src/KeyvLruManagedTtl.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const KeyvLruManagedTtl = require('./KeyvLruManagedTtl');

describe('KeyvLruManagedTtl', () => {
test('constructor', () => {
expect.assertions(2);
const sut = new KeyvLruManagedTtl();
expect(sut.cache).not.toBeUndefined();
expect(sut).toBeInstanceOf(KeyvLruManagedTtl);
});
describe('KeyvLruManagedTtl methods', () => {
let sut;

beforeEach(() => {
sut = new KeyvLruManagedTtl({ max: 100 });
});

afterEach(() => {
jest.restoreAllMocks();
});

test('get a missing item', () => {
expect.assertions(1);
jest.spyOn(sut.cache, 'get');
sut.get('foo');
expect(sut.cache.get).toHaveBeenCalledWith('foo');
});

test('get an item without ttl', () => {
expect.assertions(1);
sut.cache.set('foo');
jest.spyOn(sut.cache, 'get').mockReturnValue({ data: { bar: true } });
expect(sut.get('foo')).toEqual({ bar: true });
});

test('get an expired item', () => {
expect.assertions(1);
sut.cache.set('foo');
jest
.spyOn(sut.cache, 'get')
.mockReturnValue({ expires: 0, data: { bar: true } });
expect(sut.get('foo')).toBeUndefined();
});

test('get an unexpired item', () => {
expect.assertions(1);
sut.cache.set('foo');
jest.spyOn(sut.cache, 'get').mockReturnValue({
expires: Date.now() + 1000000000,
data: { bar: true },
});
expect(sut.get('foo')).toEqual({ bar: true });
});

test('set without a ttl', () => {
expect.assertions(2);
jest.spyOn(sut.cache, 'set');
sut.set('foo', { bar: true });
expect(sut.cache.set).toHaveBeenCalledWith('foo', {
data: { bar: true },
});
expect(sut.cache.get('foo')).toEqual({ data: { bar: true } });
});

test('set with a ttl', () => {
expect.assertions(1);
jest.spyOn(Date, 'now').mockReturnValue(123456789);
jest.spyOn(sut.cache, 'set');
sut.set('foo', { bar: true }, 1000000000);
expect(sut.cache.set).toHaveBeenCalledWith('foo', {
data: { bar: true },
expires: 1123456789,
});
});

test('evictExpired', done => {
expect.assertions(3);
jest.spyOn(sut, 'delete');
sut.cache.set('foo', { data: 'bar', expires: Date.now() + 1000000000 });
sut.cache.set('python', { data: 'cobra' });
sut.cache.set('lorem', { data: 'ipsum', expires: 0 });
sut.cache.set('dolor', { data: 'sid', expires: 0 });
sut.evictExpired();
process.nextTick(() => {
expect(sut.delete).toHaveBeenCalledTimes(2);
expect(sut.delete).toHaveBeenCalledWith('lorem');
expect(sut.delete).toHaveBeenCalledWith('dolor');
done();
});
});
});
});
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow

const KeyvLru = require('./KeyvLru');
const KeyvNullManagedTtl = require('./KeyvLruManagedTtl');

module.exports = KeyvLru;
module.exports = { KeyvLru, KeyvNullManagedTtl };
1 change: 0 additions & 1 deletion test-suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@ const keyvTestSuite = require('@keyv/test-suite').default;
const Keyv = require('keyv');
const KeyvStore = require('./lib');

console.log(keyvTestSuite);
const store = () => new KeyvStore(1000);
keyvTestSuite(testSuite, Keyv, store);

0 comments on commit 8382d0c

Please sign in to comment.