@acuris/leprechaun-cache
Version:
Caching library that supports double checked caching and stale returns to avoid stampede and slow responses
143 lines • 4.69 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function delay(durationMs) {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, durationMs);
});
}
const defaultBackgroundErrorHandler = (_) => { };
class LeprechaunCache {
constructor({ keyPrefix = '', softTtlMs, hardTtlMs, lockTtlMs, waitTimeMs = 0, waitForUnlockMs, cacheStore, spinMs, returnStale, onMiss, onBackgroundError = defaultBackgroundErrorHandler }) {
this.inProgress = new Map();
this.hardTtlMs = hardTtlMs;
this.softTtlMs = softTtlMs;
this.lockTtlMs = lockTtlMs;
this.waitTimeMs = waitTimeMs;
this.spinWaitCount = Math.ceil(waitForUnlockMs / spinMs);
this.spinMs = spinMs;
this.cacheStore = cacheStore;
this.returnStale = returnStale;
this.onMiss = onMiss;
this.keyPrefix = keyPrefix;
this.onBackgroundError = onBackgroundError;
}
async clear(key) {
const result = await this.cacheStore.del(this.keyPrefix + key);
this.inProgress.delete(key);
return result;
}
async get(key) {
let promise = this.inProgress.get(key);
if (promise === undefined) {
try {
promise = this.doGet(key, this.softTtlMs);
this.inProgress.set(key, promise);
return await promise;
}
finally {
this.inProgress.delete(key);
}
}
return promise;
}
async refresh(key) {
return this.updateCache(key, this.softTtlMs);
}
async doGet(key, ttl) {
const result = await this.cacheStore.get(this.keyPrefix + key);
if (!result) {
return this.updateCache(key, ttl);
}
if (result.expiresAt > Date.now()) {
return result.data;
}
const update = this.updateCache(key, ttl).catch(e => {
if (this.returnStale) {
this.onBackgroundError(e);
return result.data;
}
throw e;
});
if (!this.returnStale) {
return update;
}
return this.race(update, result.data);
}
async race(update, staleData) {
update.catch(e => {
this.onBackgroundError(e);
return staleData;
});
if (this.waitTimeMs <= 0) {
return staleData;
}
const returnStaleAfterWaitTime = new Promise(resolve => {
setTimeout(resolve, this.waitTimeMs, staleData);
});
return Promise.race([update, returnStaleAfterWaitTime]);
}
async spinLock(key) {
const lock = {
lockId: '',
didSpin: false
};
let i = 0;
do {
lock.lockId = (await this.cacheStore.lock(this.keyPrefix + key, this.lockTtlMs)) || '';
if (lock.lockId) {
break;
}
await delay(this.spinMs);
lock.didSpin = true;
} while (i++ <= this.spinWaitCount);
return lock;
}
async updateCache(key, ttl) {
const lock = await this.spinLock(key);
if (!lock.lockId) {
throw new Error('unable to acquire lock and no data in cache');
}
if (lock.didSpin) {
//If we spun while getting the lock, then get the updated version (hopefully updated by another process)
const result = await this.cacheStore.get(this.keyPrefix + key);
if (result && result.data) {
await this.cacheStore.unlock(this.keyPrefix + key, lock.lockId);
return result.data;
}
}
try {
const data = await this.onMiss(key);
//Set and unlock asynchronously so we don't delay the response
this.setAndUnlock(key, {
data,
expiresAt: Date.now() + ttl
}, lock);
return data;
}
catch (e) {
this.unlock(key, lock);
throw e;
}
}
async unlock(key, lock) {
try {
await this.cacheStore.unlock(this.keyPrefix + key, lock.lockId);
}
catch (e) {
this.onBackgroundError(e);
}
}
async setAndUnlock(key, cacheData, lock) {
try {
await this.cacheStore.set(this.keyPrefix + key, cacheData, this.hardTtlMs);
}
catch (e) {
this.onBackgroundError(e);
}
await this.unlock(key, lock);
}
}
exports.LeprechaunCache = LeprechaunCache;
//# sourceMappingURL=leprechaun-cache.js.map