Added implementation for event-aware cache

refs https://github.com/TryGhost/Toolbox/issues/522

- The main feature of this cache wrapper is being able to "reset" the the cache without calling the "reset" on the wrapped cache. Being able to invalidate caches without accessing the data is a feature needed to run on caches with shared environment.
- Cache invalidation happens through a special "reset time" key being added to each key when setting or getting a value, when the cache is reset the reset time is set to a new value - essentially invalidating all previously accessible values.
This commit is contained in:
Naz 2023-02-22 22:16:38 +08:00
parent 0301f5983e
commit f74b19ab61
No known key found for this signature in database
2 changed files with 95 additions and 1 deletions

View File

@ -1,9 +1,51 @@
class EventAwareCacheWrapper {
#cache;
#lastReset;
/**
* @param {Object} deps
* @param {Object} deps.cache - cache instance extending adapter-base-cache
* @param {Object} [deps.eventRegistry] - event registry instance
* @param {Number} [deps.lastReset] - timestamp of last reset
* @param {String[]} [deps.resetEvents] - event to listen to triggering reset
*/
constructor(deps) {
this.#cache = deps.cache;
this.#lastReset = deps.lastReset || Date.now();
if (deps.resetEvents && deps.eventRegistry) {
this.#initListeners(deps.eventRegistry, deps.resetEvents);
}
}
#initListeners(eventRegistry, eventsToResetOn) {
eventsToResetOn.forEach((event) => {
eventRegistry.on(event, () => {
this.reset();
});
});
}
#buildResetAwareKey(key) {
return `${this.#lastReset}:${key}`;
}
async get(key) {
return this.#cache.get(this.#buildResetAwareKey(key));
}
async set(key, value) {
return this.#cache.set(this.#buildResetAwareKey(key), value);
}
/**
* Reset the cache without removing of flushing the keys
* The mechanism is based on adding a timestamp to the key
* This way the cache is invalidated but the keys are still there
*/
reset() {
this.#lastReset = Date.now();
}
}

View File

@ -1,8 +1,60 @@
const assert = require('assert');
const InMemoryCache = require('@tryghost/adapter-cache-memory-ttl');
const EventAwareCacheWrapper = require('../index');
const {EventEmitter} = require('stream');
const sleep = ms => (
new Promise((resolve) => {
setTimeout(resolve, ms);
})
);
describe('EventAwareCacheWrapper', function () {
it('Can initialize', function () {
assert.ok(new EventAwareCacheWrapper());
const cache = new InMemoryCache();
const wrappedCache = new EventAwareCacheWrapper({
cache
});
assert.ok(wrappedCache);
});
describe('get', function () {
it('calls a wrapped cache with extra key', async function () {
const cache = new InMemoryCache();
const lastReset = Date.now();
const wrapper = new EventAwareCacheWrapper({
cache: cache,
lastReset: lastReset
});
await wrapper.set('a', 'b');
assert.equal(await wrapper.get('a'), 'b');
assert.equal(await cache.get(`${lastReset}:a`), 'b');
});
});
describe('listens to reset events', function () {
it('resets the cache when reset event is triggered', async function () {
const cache = new InMemoryCache();
const lastReset = Date.now();
const eventRegistry = new EventEmitter();
const wrapper = new EventAwareCacheWrapper({
cache: cache,
lastReset: lastReset,
resetEvents: ['site.changed'],
eventRegistry: eventRegistry
});
await wrapper.set('a', 'b');
assert.equal(await wrapper.get('a'), 'b');
// let the time tick to get new lastReset
await sleep(100);
eventRegistry.emit('site.changed');
assert.equal(await wrapper.get('a'), undefined);
});
});
});