UNPKG

@johngw/stream

Version:

Reactive programming tools using the WHATWG Streams API.

122 lines 3.14 kB
import { isNonNullObject } from '@johngw/stream-common/Object'; /** * Timed cache using a `Storage` interface. * * @group Storages * @example * Letting cache expire. * * ``` * const cache = new StorageCache(localStorage, 'my-cache', 30 * 60_000) * * cache.set(['a', 'path'], 'foo') * cache.get(['a', 'path']) * // 'foo' * * await setTimeout(30 * 60_000) * cache.get(['a', 'path']) * // undefined * ``` * @example * Manually invalidating cache. * * ``` * const cache = new StorageCache(localStorage, 'my-cache', 30 * 60_000) * cache.set(['foo', 'bar'], 'a') * cache.set(['foo', 'rab'], 'b') * * // Remove a specific cache item * cache.unset(['foo', 'bar']) * * // Remove all "foos" * cache.unset(['foo']) * ``` */ export class StorageCache { #storage; #key; #ms; constructor(storage, key, ms) { this.#storage = storage; this.#key = key; this.#ms = ms; } get ms() { return this.#ms; } /** * Ads or overrides an item in the cache. */ set(path, value, ms = this.#ms) { this.#save({ ...this.getAll(), [this.#pathKey(path)]: { t: Date.now() + ms, v: value, }, }); setTimeout(() => { this.unset(path); }, ms); } /** * Removes one or more items from the cache using the path * as key hierarchy. */ unset(path) { const pathKey = this.#pathKey(path); this.#save(Object.entries(this.getAll()).reduce((acc, [key, value]) => this.#keyMatches(pathKey, key) ? acc : { ...acc, [key]: value }, {})); } /** * Returns the cached value. Returns undefined if it doesn't * exist or if the cache is stale. */ get(path) { return this.#get(path)?.v; } /** * Checks if a path has cache. If not, updates it and returns `true`. * Otherwise returns `false`. */ async updateIfStale(path, update, ms) { let item = this.get(path); if (item === undefined) { item = await update(); if (item === undefined) return false; this.set(path, item, ms); return true; } return false; } /** * Returns the remaining cache time for an item. */ timeLeft(path) { const now = Date.now(); const time = this.#get(path)?.t || now; return Math.max(time - now, 0); } #pathKey(path) { return path.join('.'); } #keyMatches(pathKey, test) { return test === pathKey || test.startsWith(`${pathKey}.`); } #get(path) { const result = this.getAll()[this.#pathKey(path)]; if (!isStoredItem(result)) return undefined; return result; } #save(obj) { this.#storage.setItem(this.#key, JSON.stringify(obj)); } getAll() { return JSON.parse(this.#storage.getItem(this.#key) || '{}'); } } function isStoredItem(x) { return isNonNullObject(x) && typeof x.t === 'number' && 'v' in x; } //# sourceMappingURL=StorageCache.js.map