UNPKG

@solomonai/cache

Version:

<div align="center"> <h1 align="center">@solomonai/cache</h1> <h5>Cache all the things</h5> </div>

244 lines (240 loc) 6.37 kB
import { BaseError, Ok, Err } from '@unkey/error'; // src/context.ts var DefaultStatefulContext = class { waitUntil(_p) { } }; var CacheError = class extends BaseError { name = "CacheError"; retry = false; tier; key; constructor(opts) { super(opts); this.name = "CacheError"; this.tier = opts.tier; this.key = opts.key; } }; // src/cache.ts function createCache(namespaces) { return Object.entries(namespaces).reduce( (acc, [n, c]) => { acc[n] = { get: (key) => c.get(n, key), set: (key, value, opts) => c.set(n, key, value, opts), remove: (key) => c.remove(n, key), swr: (key, loadFromOrigin) => c.swr(n, key, loadFromOrigin) }; return acc; }, {} ); } var TieredStore = class { ctx; tiers; name = "tiered"; /** * Create a new tiered store * Stored are checked in the order they are provided * The first store to return a value will be used to populate all previous stores * * * `stores` can accept `undefined` as members to allow you to construct the tiers dynamically * @example * ```ts * new TieredStore(ctx, [ * new MemoryStore(..), * process.env.ENABLE_X_STORE ? new XStore(..) : undefined * ]) * ``` */ constructor(ctx, stores) { this.ctx = ctx; this.tiers = stores.filter(Boolean); } /** * Return the cached value * * The response will be `undefined` for cache misses or `null` when the key was not found in the origin */ async get(namespace, key) { if (this.tiers.length === 0) { return Ok(void 0); } for (let i = 0; i < this.tiers.length; i++) { const res = await this.tiers[i].get(namespace, key); if (res.err) { return res; } if (typeof res.val !== "undefined") { this.ctx.waitUntil( Promise.all( this.tiers.filter((_, j) => j < i).map((t) => () => t.set(namespace, key, res.val)) ).catch((err) => { return Err( new CacheError({ tier: this.name, key, message: err.message }) ); }) ); return Ok(res.val); } } return Ok(void 0); } /** * Sets the value for the given key. */ async set(namespace, key, value) { return Promise.all(this.tiers.map((t) => t.set(namespace, key, value))).then(() => Ok()).catch( (err) => Err( new CacheError({ tier: this.name, key, message: err.message }) ) ); } /** * Removes the key from the cache. */ async remove(namespace, key) { return Promise.all(this.tiers.map((t) => t.remove(namespace, key))).then(() => Ok()).catch( (err) => Err( new CacheError({ tier: this.name, key, message: err.message }) ) ); } }; var SwrCache = class { ctx; store; fresh; stale; /** * To prevent concurrent revalidation of the same data, all revalidations are deduplicated using * this map. */ revalidating = /* @__PURE__ */ new Map(); constructor(ctx, store, fresh, stale) { this.ctx = ctx; this.store = store; this.fresh = fresh; this.stale = stale; } /** * Return the cached value * * The response will be `undefined` for cache misses or `null` when the key was not found in the origin */ async get(namespace, key) { const res = await this._get(namespace, key); if (res.err) { return Err(res.err); } return Ok(res.val.value); } async _get(namespace, key) { const res = await this.store.get(namespace, key); if (res.err) { return Err(res.err); } const now = Date.now(); if (!res.val) { return Ok({ value: void 0 }); } if (now >= res.val.staleUntil) { this.ctx.waitUntil(this.remove(namespace, key)); return Ok({ value: void 0 }); } if (now >= res.val.freshUntil) { return Ok({ value: res.val.value, revalidate: true }); } return Ok({ value: res.val.value }); } /** * Set the value */ async set(namespace, key, value, opts) { const now = Date.now(); return this.store.set(namespace, key, { value, freshUntil: now + (opts?.fresh ?? this.fresh), staleUntil: now + (opts?.stale ?? this.stale) }); } /** * Removes the key from the cache. */ async remove(namespace, key) { return this.store.remove(namespace, key); } async swr(namespace, key, loadFromOrigin) { const res = await this._get(namespace, key); if (res.err) { return Err(res.err); } const { value, revalidate } = res.val; if (typeof value !== "undefined") { if (revalidate) { this.ctx.waitUntil( this.deduplicateLoadFromOrigin(namespace, key, loadFromOrigin).then( (res2) => this.set(namespace, key, res2) ) ); } return Ok(value); } try { const value2 = await this.deduplicateLoadFromOrigin(namespace, key, loadFromOrigin); this.ctx.waitUntil(this.set(namespace, key, value2)); return Ok(value2); } catch (err) { return Err( new CacheError({ tier: "cache", key, message: err.message }) ); } } /** * Deduplicating the origin load helps when the same value is requested many times at once and is * not yet in the cache. If we don't deduplicate, we'd create a lot of unnecessary load on the db. */ async deduplicateLoadFromOrigin(namespace, key, loadFromOrigin) { const revalidateKey = [namespace, key].join("::"); try { const revalidating = this.revalidating.get(revalidateKey); if (revalidating) { return await revalidating; } const p = loadFromOrigin(key); this.revalidating.set(revalidateKey, p); return await p; } finally { this.revalidating.delete(revalidateKey); } } }; // src/namespace.ts var Namespace = class extends SwrCache { constructor(ctx, opts) { const tieredStore = new TieredStore(ctx, opts.stores); super(ctx, tieredStore, opts.fresh, opts.stale); } }; export { CacheError, DefaultStatefulContext, Namespace, TieredStore, createCache }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map