UNPKG

@solomonai/cache

Version:

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

250 lines (245 loc) 6.59 kB
'use strict'; var error = require('@unkey/error'); // src/context.ts var DefaultStatefulContext = class { waitUntil(_p) { } }; var CacheError = class extends error.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 error.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 error.Err( new CacheError({ tier: this.name, key, message: err.message }) ); }) ); return error.Ok(res.val); } } return error.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(() => error.Ok()).catch( (err) => error.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(() => error.Ok()).catch( (err) => error.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 error.Err(res.err); } return error.Ok(res.val.value); } async _get(namespace, key) { const res = await this.store.get(namespace, key); if (res.err) { return error.Err(res.err); } const now = Date.now(); if (!res.val) { return error.Ok({ value: void 0 }); } if (now >= res.val.staleUntil) { this.ctx.waitUntil(this.remove(namespace, key)); return error.Ok({ value: void 0 }); } if (now >= res.val.freshUntil) { return error.Ok({ value: res.val.value, revalidate: true }); } return error.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 error.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 error.Ok(value); } try { const value2 = await this.deduplicateLoadFromOrigin(namespace, key, loadFromOrigin); this.ctx.waitUntil(this.set(namespace, key, value2)); return error.Ok(value2); } catch (err) { return error.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); } }; exports.CacheError = CacheError; exports.DefaultStatefulContext = DefaultStatefulContext; exports.Namespace = Namespace; exports.TieredStore = TieredStore; exports.createCache = createCache; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map