-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add managed TTL version of the LRU store
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
Showing
7 changed files
with
185 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters