UNPKG

@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
"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