@dotpress/cache 0.0.0-stage → 2.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logger.ts","../src/store.ts","../src/cache.ts"],"sourcesContent":["/**\n * Logger of the cache. A Dotpress or Pino logger fits; `cachePlugin` uses\n * the logger of the request.\n */\nexport interface CacheLogger {\n debug?(data: object, message: string): void\n info(data: object, message: string): void\n warn(data: object, message: string): void\n error(data: object, message: string): void\n}\n\nconst noop = () => undefined\n\n/** Logger that discards everything. */\nexport const silentLogger: CacheLogger = {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n}\n\n/** Checks a logger option. */\nexport const checkLogger = (label: string, logger: unknown) => {\n for (const level of ['info', 'warn', 'error'] as const) {\n if (\n typeof (logger as Record<string, unknown> | null)?.[level] !== 'function'\n ) {\n throw new TypeError(\n `${label}: logger must have info(), warn() and error() methods.`\n )\n }\n }\n return logger as CacheLogger\n}\n","/**\n * Where the cache keeps its entries: Redis, Upstash, Cloudflare KV,\n * memory… Values are strings, serialized by the cache.\n */\nexport interface CacheStore {\n /** Name of the store, in logs and errors. */\n readonly name: string\n /**\n * Longest key accepted, in bytes (UTF-8). Longer keys are hashed by the\n * cache. Undefined without limit.\n */\n readonly maxKeyLength?: number | undefined\n /** Value of a key, or undefined when it is missing or expired. */\n get(key: string): Promise<string | undefined>\n /**\n * Values of several keys, in their order: one round trip for the tags of\n * an entry. Optional: the cache calls `get` for each key without it.\n */\n getMany?(keys: readonly string[]): Promise<(string | undefined)[]>\n /** Sets a value that expires after `ttl` milliseconds (at least 1). */\n set(key: string, value: string, ttl: number): Promise<void>\n /** Removes a key; missing keys are ignored. */\n delete(key: string): Promise<void>\n /**\n * Takes a lock for `ttl` milliseconds: returns its token, or undefined\n * when another holder has it. Optional: the `lock` option of the cache\n * needs it.\n */\n lock?(key: string, ttl: number): Promise<string | undefined>\n /** Releases a lock, if the token still holds it. */\n unlock?(key: string, token: string): Promise<void>\n}\n\n/** Checks a store option. */\nexport const isStore = (value: unknown): value is CacheStore => {\n const store = value as Partial<CacheStore> | null\n return (\n typeof store?.name === 'string' &&\n typeof store.get === 'function' &&\n typeof store.set === 'function' &&\n typeof store.delete === 'function'\n )\n}\n\n/** Options of `createMemoryStore()`. */\nexport interface MemoryStoreOptions {\n /**\n * Entries kept at most: the least recently used ones are removed\n * first. Defaults to 10,000.\n */\n maxEntries?: number | undefined\n}\n\n/** Store in the memory of the process: for one instance, and for tests. */\nexport interface MemoryStore extends CacheStore {\n /** Number of entries, expired ones included until they are read. */\n readonly size: number\n /** Removes every entry. */\n clear(): void\n}\n\n/**\n * Creates a store in the memory of the process, least recently used\n * entries removed first. Each instance has its own: use a shared store\n * (Redis…) for several instances.\n */\nexport const createMemoryStore = (\n options: MemoryStoreOptions = {}\n): MemoryStore => {\n const label = 'createMemoryStore()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (key !== 'maxEntries') {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const { maxEntries = 10_000 } = options\n if (!Number.isInteger(maxEntries) || maxEntries < 1) {\n throw new TypeError(`${label}: maxEntries must be a positive integer.`)\n }\n\n // Insertion order of a Map: the first entry is the least recently used.\n const entries = new Map<string, { value: string; expiresAt: number }>()\n const locks = new Map<string, { token: string; expiresAt: number }>()\n\n const read = (key: string) => {\n const entry = entries.get(key)\n if (!entry) return undefined\n entries.delete(key)\n if (entry.expiresAt <= Date.now()) return undefined\n entries.set(key, entry)\n return entry.value\n }\n\n return Object.freeze({\n name: 'memory',\n get size() {\n return entries.size\n },\n get: async (key: string) => read(key),\n getMany: async (keys: readonly string[]) => keys.map(read),\n set: async (key: string, value: string, ttl: number) => {\n entries.delete(key)\n entries.set(key, { value, expiresAt: Date.now() + ttl })\n for (const oldest of entries.keys()) {\n if (entries.size <= maxEntries) break\n entries.delete(oldest)\n }\n },\n delete: async (key: string) => {\n entries.delete(key)\n },\n lock: async (key: string, ttl: number) => {\n const held = locks.get(key)\n if (held && held.expiresAt > Date.now()) return undefined\n const token = crypto.randomUUID()\n locks.set(key, { token, expiresAt: Date.now() + ttl })\n return token\n },\n unlock: async (key: string, token: string) => {\n if (locks.get(key)?.token === token) locks.delete(key)\n },\n clear: () => {\n entries.clear()\n locks.clear()\n },\n })\n}\n","import { toMilliseconds, type Duration } from './duration.js'\nimport {\n CacheStoreError,\n CacheTimeoutError,\n type StoreOperation,\n} from './errors.js'\nimport { keyOf } from './keys.js'\nimport { checkLogger, silentLogger, type CacheLogger } from './logger.js'\nimport { isStore, type CacheStore } from './store.js'\n\n/**\n * A schema that validates cached values when they are read: a Zod schema,\n * or anything with the same `safeParseAsync()`.\n */\nexport interface CacheSchema<T> {\n safeParseAsync(\n value: unknown\n ): Promise<{ success: true; data: T } | { success: false }>\n}\n\ndeclare const untyped: unique symbol\n/** Value type of an entry without schema: given by the loader. */\nexport interface Untyped {\n readonly [untyped]: true\n}\n\ndeclare const entryTypes: unique symbol\n\n/** A kind of cached value, created by `defineCacheEntry()`. */\nexport interface CacheEntry<A extends readonly unknown[], T> {\n readonly name: string\n readonly version: number\n /** Types only. */\n readonly [entryTypes]?: { args: A; value: (value: T) => T }\n}\n\n/** Value of an entry: `unknown` without schema. */\nexport type CacheValue<T> = T extends Untyped ? unknown : T\n\n/**\n * Lifetime and resilience of values: options of an entry, or defaults of\n * the cache.\n */\nexport interface CachePolicy {\n /** Lifetime of the values. Defaults to 1 hour. */\n ttl?: Duration | undefined\n /**\n * Stale-if-error: how long after expiry a value can still be served when\n * the loader fails (or times out). Defaults to 0 (disabled).\n */\n grace?: Duration | undefined\n /**\n * After a failed load served from the grace period, the loader is not\n * called again before this delay: the stale value is served. Defaults to\n * 10 seconds.\n */\n graceBackoff?: Duration | undefined\n /**\n * How long after expiry a value is still served while a background load\n * refreshes it. Defaults to 0 (disabled).\n */\n staleWhileRevalidate?: Duration | undefined\n /**\n * Soft timeout: a load slower than this serves the stale value of the\n * grace period, if any, and goes on in the background.\n */\n timeout?: Duration | undefined\n /**\n * Hard timeout: a load slower than this fails (`CacheTimeoutError`), or\n * serves the stale value of the grace period, if any. It goes on in the\n * background.\n */\n hardTimeout?: Duration | undefined\n /**\n * Distributed single flight: before loading a missing value, takes a\n * lock in the store (Redis…) for at most this duration; the other\n * instances wait for the value meanwhile, then load it themselves.\n * Ignored by stores without locks. Disabled by default.\n */\n lock?: Duration | undefined\n}\n\n/** Options of `defineCacheEntry()`. */\nexport interface CacheEntryOptions<\n A extends readonly unknown[],\n T,\n> extends CachePolicy {\n /**\n * Name of the entry, in keys and logs: letters, digits, `.`, `_` and\n * `-`.\n */\n name: string\n /**\n * Key of the arguments: a string, a number, or a value compared like the\n * arguments of a memo. Defaults to the argument (or the arguments).\n */\n key?: ((...args: A) => unknown) | undefined\n /**\n * Validates cached values when they are read (entries from an older\n * version of the app) and written. JSON turns dates into strings: use\n * `z.coerce.date()`.\n */\n schema?: CacheSchema<T> | undefined\n /**\n * Version of the values, in the key: increase it when their shape\n * changes, to ignore the values written before. Defaults to 1.\n */\n version?: number | undefined\n /**\n * Tags of the arguments: `invalidateTags()` expires every value with one\n * of them.\n */\n tags?: ((...args: A) => readonly string[]) | undefined\n}\n\n/** A policy in milliseconds; undefined when not set. */\ntype Policy = { [K in keyof CachePolicy]: number | undefined }\n\ninterface EntryConfig {\n name: string\n version: number\n key: ((...args: readonly unknown[]) => unknown) | undefined\n schema: CacheSchema<unknown> | undefined\n tags: ((...args: readonly unknown[]) => readonly string[]) | undefined\n policy: Policy\n}\n\nconst entries = new WeakMap<object, EntryConfig>()\n\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/\nconst POLICY = [\n 'ttl',\n 'grace',\n 'graceBackoff',\n 'staleWhileRevalidate',\n 'timeout',\n 'hardTimeout',\n 'lock',\n] as const\nconst ENTRY_OPTIONS = new Set([\n 'name',\n 'key',\n 'schema',\n 'version',\n 'tags',\n ...POLICY,\n])\n\n/** Reads the durations of a policy (`ttl` and timeouts at least 1 ms). */\nconst policyOf = (label: string, values: Record<string, unknown>): Policy => {\n const policy: Policy = {}\n for (const option of POLICY) {\n const value = values[option]\n if (value === undefined) continue\n policy[option] =\n option === 'ttl' ||\n option === 'timeout' ||\n option === 'hardTimeout' ||\n option === 'lock'\n ? positive(`${label}: ${option}`, value)\n : toMilliseconds(`${label}: ${option}`, value)\n }\n return policy\n}\n\n/**\n * Defines a kind of cached value: its key, its lifetime, and optionally\n * its schema.\n *\n * ```ts\n * export const userCache = defineCacheEntry({\n * name: 'user',\n * key: (id: string) => id,\n * schema: userSchema,\n * ttl: '1h',\n * })\n * ```\n */\nexport function defineCacheEntry<A extends readonly unknown[], T>(\n options: CacheEntryOptions<A, T> & { schema: CacheSchema<T> }\n): CacheEntry<A, T>\nexport function defineCacheEntry<A extends readonly unknown[]>(\n options: CacheEntryOptions<A, never> & { schema?: undefined }\n): CacheEntry<A, Untyped>\nexport function defineCacheEntry<A extends readonly unknown[], T>(\n options: CacheEntryOptions<A, T>\n): CacheEntry<A, T>\nexport function defineCacheEntry(options: unknown): {\n readonly name: string\n readonly version: number\n} {\n const label = 'defineCacheEntry()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n const values = options as Record<string, unknown>\n const { name, key, schema, tags, version = 1 } = values\n if (typeof name !== 'string' || !NAME.test(name)) {\n throw new TypeError(\n `${label}: name must have 1 to 128 letters, digits, \".\", \"_\" or \"-\", starting with a letter or a digit.`\n )\n }\n const named = `defineCacheEntry(\"${name}\")`\n for (const option of Object.keys(values)) {\n if (!ENTRY_OPTIONS.has(option)) {\n throw new TypeError(`${named}: unknown option \"${option}\".`)\n }\n }\n if (key !== undefined && typeof key !== 'function') {\n throw new TypeError(`${named}: key must be a function.`)\n }\n if (\n schema !== undefined &&\n typeof (schema as Partial<CacheSchema<unknown>> | null)?.safeParseAsync !==\n 'function'\n ) {\n throw new TypeError(\n `${named}: schema must have a safeParseAsync() method, like a Zod schema.`\n )\n }\n if (!Number.isInteger(version) || (version as number) < 0) {\n throw new TypeError(`${named}: version must be a non-negative integer.`)\n }\n if (tags !== undefined && typeof tags !== 'function') {\n throw new TypeError(`${named}: tags must be a function.`)\n }\n\n const entry = Object.freeze({ name, version: version as number })\n entries.set(entry, {\n name,\n version: version as number,\n key: key as EntryConfig['key'],\n schema: schema as CacheSchema<unknown> | undefined,\n tags: tags as EntryConfig['tags'],\n policy: policyOf(named, values),\n })\n return entry\n}\n\nconst positive = (label: string, value: unknown) => {\n const ms = toMilliseconds(label, value)\n if (ms < 1) throw new TypeError(`${label} must be at least 1 millisecond.`)\n return ms\n}\n\n/** Turns values into strings for the store, and back. */\nexport interface CacheSerializer {\n serialize(value: unknown): string\n deserialize(text: string): unknown\n}\n\n/** Details of a store failure, for `onError`. */\nexport interface CacheErrorInfo {\n readonly operation: StoreOperation\n /** Key of the store. */\n readonly key: string\n /** Name of the entry. */\n readonly entry: string\n}\n\n/** Something that happened to a value, for `onEvent`. */\nexport interface CacheEvent {\n /**\n * `hit`: fresh value; `miss`: loaded; `stale`: a value past its `ttl`\n * served, for the `reason`.\n */\n readonly type: 'hit' | 'miss' | 'stale'\n readonly reason?: 'revalidate' | 'error' | 'timeout' | 'backoff'\n /** Name of the entry. */\n readonly entry: string\n /** Key of the store. */\n readonly key: string\n}\n\n/** Options of `createCache()`: the policy is the default of the entries. */\nexport interface CacheOptions extends CachePolicy {\n store: CacheStore\n /** Prefix of every key, like the name of the app. */\n prefix?: string | undefined\n /** Defaults to JSON. */\n serializer?: CacheSerializer | undefined\n logger?: CacheLogger | undefined\n /**\n * Called for each store failure, besides the log: metrics, alerts. Its\n * own errors are logged and ignored.\n */\n onError?:\n | ((error: CacheStoreError, info: CacheErrorInfo) => void | Promise<void>)\n | undefined\n /**\n * When the store fails, behaves as a cache miss (reads) or ignores the\n * write, after logging it. Set false to throw a `CacheStoreError`.\n * Defaults to true.\n */\n failOpen?: boolean | undefined\n /** A store operation slower than this counts as a failure. */\n storeTimeout?: Duration | undefined\n /**\n * Called for each hit, miss and stale value served: metrics. Its errors\n * are logged and ignored.\n */\n onEvent?: ((event: CacheEvent) => void) | undefined\n}\n\n/** A cache, created by `createCache()`. */\nexport interface Cache {\n /**\n * The fresh value of the arguments, or undefined when missing, expired\n * or invalidated.\n */\n get<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...args: A\n ): Promise<CacheValue<T> | undefined>\n /** Sets the value of the arguments (the last argument). */\n set<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...argsAndValue: [...A, CacheValue<T>]\n ): Promise<void>\n /** Removes the value of the arguments. */\n delete<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...args: A\n ): Promise<void>\n /**\n * The value of the arguments, loaded by the last argument when missing\n * and cached. Calls made while it loads share the load. Applies the\n * grace period, stale-while-revalidate and timeouts of the entry.\n */\n getOrSet<A extends readonly unknown[], R>(\n entry: CacheEntry<A, Untyped>,\n ...argsAndLoader: [...A, () => R | Promise<R>]\n ): Promise<R>\n getOrSet<A extends readonly unknown[], T>(\n entry: CacheEntry<A, T>,\n ...argsAndLoader: [...A, () => T | Promise<T>]\n ): Promise<T>\n /**\n * Expires every value with one of the tags (they can still be served by\n * the grace period when the loader fails).\n */\n invalidateTags(tags: readonly string[]): Promise<void>\n /** The same cache, logging with another logger. */\n withLogger(logger: CacheLogger): Cache\n}\n\nconst CACHE_OPTIONS = new Set([\n 'store',\n 'prefix',\n 'ttl',\n 'serializer',\n 'logger',\n 'onError',\n 'failOpen',\n 'storeTimeout',\n 'onEvent',\n ...POLICY,\n])\n\nconst jsonSerializer: CacheSerializer = {\n serialize: (value) => JSON.stringify(value),\n deserialize: (text) => JSON.parse(text) as unknown,\n}\n\n/**\n * Stored form of a value: `{ v, c (created), e (expires), d (data),\n * t (token of each tag) }`.\n */\ninterface StoredEntry {\n v: 1\n c: number\n e: number\n d: string\n t?: Record<string, string>\n}\n\nconst parseStored = (text: string): StoredEntry | undefined => {\n try {\n const stored = JSON.parse(text) as Partial<StoredEntry> | null\n if (\n stored?.v === 1 &&\n typeof stored.c === 'number' &&\n typeof stored.e === 'number' &&\n typeof stored.d === 'string' &&\n (stored.t === undefined ||\n (typeof stored.t === 'object' && stored.t !== null))\n ) {\n return stored as StoredEntry\n }\n } catch {\n // Not an entry of this cache.\n }\n return undefined\n}\n\nconst encoder = new TextEncoder()\n\n// Tokens of tags outlive the values that use them: 30 days at least.\nconst TAG_LIFETIME = 30 * 86_400_000\n// While another instance holds the lock, the value is read again so often.\nconst LOCK_POLL = 50\n\n/** SHA-256 of a text, in hexadecimal. */\nconst sha256 = async (text: string) => {\n const digest = await crypto.subtle.digest('SHA-256', encoder.encode(text))\n return [...new Uint8Array(digest)]\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Creates a cache on a store. Reads and writes fail open by default: a\n * store failure is logged (and passed to `onError`), and the app keeps\n * working without cache.\n *\n * ```ts\n * const cache = createCache({ store: createMemoryStore(), prefix: 'app', ttl: '10m' })\n * const user = await cache.getOrSet(userCache, id, () => db.users.findById(id))\n * ```\n */\nexport const createCache = (options: CacheOptions): Cache => {\n const label = 'createCache()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!CACHE_OPTIONS.has(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const {\n store,\n prefix,\n serializer = jsonSerializer,\n onError,\n onEvent,\n failOpen = true,\n } = options\n if (!isStore(store)) {\n throw new TypeError(\n `${label}: store must be a cache store (createMemoryStore(), createRedisStore()…).`\n )\n }\n if (prefix !== undefined && (typeof prefix !== 'string' || prefix === '')) {\n throw new TypeError(`${label}: prefix must be a non-empty string.`)\n }\n const defaults = policyOf(\n label,\n options as unknown as Record<string, unknown>\n )\n if (\n typeof serializer?.serialize !== 'function' ||\n typeof serializer.deserialize !== 'function'\n ) {\n throw new TypeError(\n `${label}: serializer must have serialize() and deserialize() methods.`\n )\n }\n if (onError !== undefined && typeof onError !== 'function') {\n throw new TypeError(`${label}: onError must be a function.`)\n }\n if (onEvent !== undefined && typeof onEvent !== 'function') {\n throw new TypeError(`${label}: onEvent must be a function.`)\n }\n if (typeof failOpen !== 'boolean') {\n throw new TypeError(`${label}: failOpen must be a boolean.`)\n }\n const storeTimeout =\n options.storeTimeout === undefined\n ? undefined\n : positive(`${label}: storeTimeout`, options.storeTimeout)\n const rootLogger =\n options.logger === undefined\n ? silentLogger\n : checkLogger(label, options.logger)\n\n // Shared by every logger of the cache: loads in progress, and the end of\n // the backoff of the keys whose loader failed.\n const loading = new Map<string, Promise<unknown>>()\n const backoffs = new Map<string, number>()\n\n const configOf = (method: string, entry: unknown) => {\n const config =\n typeof entry === 'object' && entry !== null\n ? entries.get(entry)\n : undefined\n if (!config) {\n throw new TypeError(\n `cache.${method}(): expected an entry created by defineCacheEntry().`\n )\n }\n return config\n }\n\n /** Policy of an entry, with the defaults of the cache. */\n const policyFor = (config: EntryConfig) => {\n const value = (option: keyof CachePolicy) =>\n config.policy[option] ?? defaults[option]\n return {\n ttl: value('ttl') ?? 3_600_000,\n grace: value('grace') ?? 0,\n graceBackoff: value('graceBackoff') ?? 10_000,\n staleWhileRevalidate: value('staleWhileRevalidate') ?? 0,\n timeout: value('timeout'),\n hardTimeout: value('hardTimeout'),\n lock: value('lock'),\n }\n }\n\n /** A key of the store, hashed when longer than the store accepts. */\n const fit = async (base: string, part: string) => {\n const key = `${base}:${part}`\n return encoder.encode(key).length > (store.maxKeyLength ?? Infinity)\n ? `${base}:#${await sha256(part)}`\n : key\n }\n\n /** Key of the store for the arguments of an entry. */\n const keyFor = async (config: EntryConfig, args: readonly unknown[]) => {\n const raw = config.key\n ? config.key(...args)\n : args.length === 1\n ? args[0]\n : args\n const base = [prefix, config.name, `v${config.version}`]\n .filter((item) => item !== undefined)\n .join(':')\n if (!config.key && args.length === 0) return base\n return fit(\n base,\n typeof raw === 'string' ? raw : keyOf(`Cache entry \"${config.name}\"`, raw)\n )\n }\n\n /** Checks the tags returned by an entry, or given to invalidate. */\n const checkTags = (label: string, tags: unknown) => {\n if (\n !Array.isArray(tags) ||\n !tags.every((tag) => typeof tag === 'string' && tag !== '')\n ) {\n throw new TypeError(\n `${label}: tags must be an array of non-empty strings.`\n )\n }\n return [...new Set(tags as string[])]\n }\n\n /** Keys of the tokens of tags (`_` cannot start the name of an entry). */\n const tagKeysOf = (tags: readonly string[]) => {\n const base = prefix === undefined ? '_tag' : `${prefix}:_tag`\n return Promise.all(\n tags.map(async (tag) => ({ tag, key: await fit(base, tag) }))\n )\n }\n\n const create = (logger: CacheLogger): Cache => {\n const describe = (error: unknown) =>\n error instanceof Error ? error.message : String(error)\n\n /** Runs a store operation: failures are logged, and fail open. */\n const guard = async <R>(\n operation: StoreOperation,\n key: string,\n entry: string,\n run: () => Promise<R>,\n fallback: R\n ): Promise<R> => {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const running = run()\n return storeTimeout === undefined\n ? await running\n : await Promise.race([\n running,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () =>\n reject(\n new Error(`no answer within ${storeTimeout} milliseconds`)\n ),\n storeTimeout\n )\n }),\n ])\n } catch (cause) {\n const error = new CacheStoreError(store.name, operation, key, {\n cause,\n })\n const data = {\n store: store.name,\n operation,\n key,\n entry,\n error: error.message,\n }\n // A failed deletion or invalidation may leave outdated values.\n if (operation === 'delete' || operation === 'invalidate') {\n logger.error(data, 'Cache store failed')\n } else {\n logger.warn(data, 'Cache store failed')\n }\n if (onError) {\n try {\n await onError(error, { operation, key, entry })\n } catch (hookError) {\n logger.error(\n { error: describe(hookError) },\n 'Cache onError hook failed'\n )\n }\n }\n if (!failOpen) throw error\n return fallback\n } finally {\n clearTimeout(timer)\n }\n }\n\n /** Reports an event to the log (debug) and `onEvent`. */\n const report = (event: CacheEvent) => {\n const { type, reason, entry, key } = event\n logger.debug?.(\n { entry, key, ...(reason ? { reason } : {}) },\n type === 'hit'\n ? 'Cache hit'\n : type === 'miss'\n ? 'Cache miss'\n : 'Cache served a stale value'\n )\n if (!onEvent) return\n try {\n onEvent(event)\n } catch (error) {\n logger.error({ error: describe(error) }, 'Cache onEvent hook failed')\n }\n }\n\n /**\n * Current tokens of tags (undefined for a missing tag), or undefined\n * when the store cannot read them.\n */\n const tokensOf = async (\n entry: string,\n tagKeys: readonly { tag: string; key: string }[]\n ) => {\n const keys = tagKeys.map(({ key }) => key)\n const values = await guard<(string | undefined)[] | undefined>(\n 'get',\n keys.join(' '),\n entry,\n () =>\n store.getMany\n ? store.getMany(keys)\n : Promise.all(keys.map((key) => store.get(key))),\n undefined\n )\n return values?.map((token, index) => ({\n tag: (tagKeys[index] as { tag: string }).tag,\n token,\n }))\n }\n\n /** Deserializes and validates a value: `{ value }`, or undefined. */\n const decode = async (config: EntryConfig, key: string, data: string) => {\n let value: unknown\n try {\n value = serializer.deserialize(data)\n } catch (error) {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache value cannot be deserialized'\n )\n return undefined\n }\n if (!config.schema) return { value }\n const result = await config.schema.safeParseAsync(value)\n return result.success ? { value: result.data } : undefined\n }\n\n /**\n * Reads a stored value: its value, expiry, and whether its tags are\n * still valid. Undefined when missing, invalid, or past every stale\n * window.\n */\n const lookup = async (\n config: EntryConfig,\n policy: ReturnType<typeof policyFor>,\n key: string\n ) => {\n const text = await guard(\n 'get',\n key,\n config.name,\n () => store.get(key),\n undefined\n )\n if (text === undefined) return undefined\n const stored = parseStored(text)\n const now = Date.now()\n if (\n !stored ||\n stored.e + Math.max(policy.grace, policy.staleWhileRevalidate) <= now\n ) {\n return undefined\n }\n const decoded = await decode(config, key, stored.d)\n if (!decoded) return undefined\n const tags = Object.entries(stored.t ?? {})\n let tagsValid = true\n if (tags.length > 0) {\n const current = await tokensOf(\n config.name,\n await tagKeysOf(tags.map(([tag]) => tag))\n )\n // Unreadable tokens: the value may have been invalidated.\n tagsValid =\n current?.every(({ tag, token }) => token === stored.t?.[tag]) ?? false\n }\n return {\n value: decoded.value,\n expiresAt: stored.e,\n fresh: stored.e > now && tagsValid,\n tagsValid,\n }\n }\n\n /**\n * Writes a value, after checking that it reads back: returns the value\n * as read later, or undefined when it cannot be cached.\n */\n const write = async (\n config: EntryConfig,\n key: string,\n args: readonly unknown[],\n value: unknown\n ): Promise<{ value: unknown } | undefined> => {\n if (value === undefined) return undefined\n let data: string\n try {\n data = serializer.serialize(value)\n if (typeof data !== 'string') throw new TypeError('not a string')\n } catch (error) {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache value cannot be serialized'\n )\n return undefined\n }\n const checked = config.schema\n ? await decode(config, key, data)\n : { value }\n if (!checked) {\n logger.warn(\n { entry: config.name, key },\n 'Cache value does not match the schema of the entry: not cached'\n )\n return undefined\n }\n const policy = policyFor(config)\n const lifetime =\n policy.ttl + Math.max(policy.grace, policy.staleWhileRevalidate)\n const now = Date.now()\n const stored: StoredEntry = {\n v: 1,\n c: now,\n e: now + policy.ttl,\n d: data,\n }\n if (config.tags) {\n const tags = checkTags(\n `defineCacheEntry(\"${config.name}\")`,\n config.tags(...args)\n )\n if (tags.length > 0) {\n const tagKeys = await tagKeysOf(tags)\n const current = await tokensOf(config.name, tagKeys)\n // Unreadable tokens: creating new ones would invalidate every\n // value of these tags, so the value is not cached.\n if (!current) return checked\n stored.t = {}\n for (const [index, { tag, token }] of current.entries()) {\n if (token !== undefined) {\n stored.t[tag] = token\n continue\n }\n // First value with this tag: its token is created.\n const created = crypto.randomUUID()\n const tagKey = (tagKeys[index] as { key: string }).key\n await guard(\n 'set',\n tagKey,\n config.name,\n () =>\n store.set(tagKey, created, Math.max(lifetime, TAG_LIFETIME)),\n undefined\n )\n stored.t[tag] = created\n }\n }\n }\n await guard(\n 'set',\n key,\n config.name,\n () => store.set(key, JSON.stringify(stored), lifetime),\n undefined\n )\n return checked\n }\n\n /**\n * Loads and writes a value, once per key at a time, and once for every\n * instance with the `lock` option.\n */\n const load = (\n config: EntryConfig,\n key: string,\n args: readonly unknown[],\n loader: () => unknown\n ) => {\n const running = loading.get(key)\n if (running) return running\n const loaded = (async () => {\n const policy = policyFor(config)\n const lockKey = `${key}:lock`\n let token: string | undefined\n if (policy.lock !== undefined && store.lock) {\n const lock = store.lock.bind(store)\n const ttl = policy.lock\n // null: the store failed, load without lock.\n const taken = await guard<string | undefined | null>(\n 'lock',\n lockKey,\n config.name,\n () => lock(lockKey, ttl),\n null\n )\n if (taken === undefined) {\n // Another instance loads it: wait for its value.\n const until = Date.now() + ttl\n while (Date.now() < until) {\n await new Promise((resolve) => setTimeout(resolve, LOCK_POLL))\n const found = await lookup(config, policy, key)\n if (found?.fresh) return found.value\n }\n logger.warn(\n { entry: config.name, key, lock: ttl },\n 'Cache lock not released in time: loading anyway'\n )\n } else if (taken !== null) {\n token = taken\n }\n }\n try {\n const value: unknown = await loader()\n const written = await write(config, key, args, value)\n backoffs.delete(key)\n return written ? written.value : value\n } finally {\n if (token !== undefined && store.unlock) {\n const unlock = store.unlock.bind(store)\n const held = token\n await guard(\n 'unlock',\n lockKey,\n config.name,\n () => unlock(lockKey, held),\n undefined\n )\n }\n }\n })()\n loading.set(key, loaded)\n void loaded.then(\n () => loading.delete(key),\n () => loading.delete(key)\n )\n return loaded\n }\n\n /** Logs the failure of a load that nobody waits for anymore. */\n const inBackground = (\n config: EntryConfig,\n key: string,\n loaded: Promise<unknown>\n ) => {\n loaded.catch((error: unknown) => {\n logger.warn(\n { entry: config.name, key, error: describe(error) },\n 'Cache refresh failed'\n )\n })\n }\n\n /** Rejects after `ms` (never when undefined), until cleared. */\n const deadline = (ms: number | undefined, error: () => unknown) => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const promise = new Promise<never>((_, reject) => {\n if (ms !== undefined) timer = setTimeout(() => reject(error()), ms)\n })\n return { promise, clear: () => clearTimeout(timer) }\n }\n\n const cache: Cache = {\n get: (async (entry: unknown, ...args: unknown[]) => {\n const config = configOf('get', entry)\n const key = await keyFor(config, args)\n const found = await lookup(config, policyFor(config), key)\n report({ type: found?.fresh ? 'hit' : 'miss', entry: config.name, key })\n return found?.fresh ? found.value : undefined\n }) as Cache['get'],\n\n set: (async (entry: unknown, ...argsAndValue: unknown[]) => {\n const config = configOf('set', entry)\n const args = argsAndValue.slice(0, -1)\n const key = await keyFor(config, args)\n await write(config, key, args, argsAndValue.at(-1))\n }) as Cache['set'],\n\n delete: (async (entry: unknown, ...args: unknown[]) => {\n const config = configOf('delete', entry)\n const key = await keyFor(config, args)\n await guard(\n 'delete',\n key,\n config.name,\n () => store.delete(key),\n undefined\n )\n }) as Cache['delete'],\n\n getOrSet: (async (entry: unknown, ...argsAndLoader: unknown[]) => {\n const config = configOf('getOrSet', entry)\n const loader = argsAndLoader.at(-1)\n if (typeof loader !== 'function') {\n throw new TypeError(\n 'cache.getOrSet(): the last argument must be the function that loads the value.'\n )\n }\n const args = argsAndLoader.slice(0, -1)\n const key = await keyFor(config, args)\n const policy = policyFor(config)\n const found = await lookup(config, policy, key)\n const event = { entry: config.name, key }\n if (found?.fresh) {\n report({ type: 'hit', ...event })\n return found.value\n }\n const now = Date.now()\n\n // Stale-while-revalidate: serve, and refresh in the background.\n if (\n found?.tagsValid &&\n found.expiresAt + policy.staleWhileRevalidate > now\n ) {\n report({ type: 'stale', reason: 'revalidate', ...event })\n inBackground(\n config,\n key,\n load(config, key, args, loader as () => unknown)\n )\n return found.value\n }\n\n // Grace period: stale value served if the loader fails. Without it,\n // an invalidated value is never served.\n const stale =\n found && policy.grace > 0 && found.expiresAt + policy.grace > now\n ? found\n : undefined\n if (stale && (backoffs.get(key) ?? 0) > now) {\n report({ type: 'stale', reason: 'backoff', ...event })\n return stale.value\n }\n\n report({ type: 'miss', ...event })\n const loaded = load(config, key, args, loader as () => unknown)\n const slow = Symbol('slow')\n const soft = deadline(stale ? policy.timeout : undefined, () => slow)\n const hard = deadline(\n policy.hardTimeout,\n () =>\n new CacheTimeoutError(\n config.name,\n key,\n policy.hardTimeout as number\n )\n )\n try {\n return await Promise.race([loaded, soft.promise, hard.promise])\n } catch (error) {\n // The load goes on, and caches its value.\n if (error === slow || error instanceof CacheTimeoutError) {\n inBackground(config, key, loaded)\n }\n if (!stale) throw error\n if (error === slow) {\n logger.warn(\n { ...event, timeout: policy.timeout },\n 'Cache served a stale value: the loader is slow'\n )\n report({ type: 'stale', reason: 'timeout', ...event })\n return stale.value\n }\n backoffs.set(key, Date.now() + policy.graceBackoff)\n logger.warn(\n { ...event, error: describe(error) },\n 'Cache served a stale value: the loader failed'\n )\n report({ type: 'stale', reason: 'error', ...event })\n return stale.value\n } finally {\n soft.clear()\n hard.clear()\n }\n }) as Cache['getOrSet'],\n\n invalidateTags: async (tags) => {\n const tagKeys = await tagKeysOf(\n checkTags('cache.invalidateTags()', tags)\n )\n await Promise.all(\n tagKeys.map(({ tag, key }) =>\n guard(\n 'invalidate',\n key,\n `tag:${tag}`,\n () => store.set(key, crypto.randomUUID(), TAG_LIFETIME),\n undefined\n )\n )\n )\n logger.debug?.({ tags }, 'Cache tags invalidated')\n },\n\n withLogger: (other) => create(checkLogger('withLogger()', other)),\n }\n return Object.freeze(cache)\n }\n\n return create(rootLogger)\n}\n"],"mappings":"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","names":["entries","label"]}
@@ -0,0 +1,51 @@
1
+ import * as _dotpress_core from '@dotpress/core';
2
+ import { Cache, Memo } from './index.js';
3
+ import './duration-2mZnG0_i.js';
4
+ import './store-D0BfLqyM.js';
5
+
6
+ declare module '@dotpress/core' {
7
+ interface RequestContext {
8
+ /** Cache (`cachePlugin`), logging with the request logger. */
9
+ cache: Cache;
10
+ /** Memo of the request (`cachePlugin`): a new one for each request. */
11
+ memo: Memo;
12
+ }
13
+ }
14
+ /**
15
+ * Runs each request inside a scope for its memo, like `asyncLocalMemo` of
16
+ * `@dotpress/cache/als`.
17
+ */
18
+ interface MemoScope {
19
+ run<T>(memo: Memo, next: () => Promise<T>): Promise<T>;
20
+ }
21
+ /** Options of `cachePlugin()`. */
22
+ interface CachePluginOptions {
23
+ /** Provides `ctx.cache`. */
24
+ cache?: Cache | undefined;
25
+ /**
26
+ * Runs each request inside the scope of its memo, so that memoized
27
+ * loaders work without `ctx` (`asyncLocalMemo` of `@dotpress/cache/als`).
28
+ */
29
+ memoScope?: MemoScope | undefined;
30
+ }
31
+ /**
32
+ * Provides `ctx.memo` (a new memo for each request) and, with a cache,
33
+ * `ctx.cache` (logging with the request logger).
34
+ *
35
+ * ```ts
36
+ * await createApp({ adapter, plugins: [cachePlugin({ cache })] })
37
+ *
38
+ * defineRoute({
39
+ * path: '/me',
40
+ * handler: ({ memo, cache, auth }) =>
41
+ * memo(loadUser, auth.userId), // or cache.getOrSet(userCache, …)
42
+ * })
43
+ * ```
44
+ */
45
+ declare const cachePlugin: (options?: CachePluginOptions) => {
46
+ readonly name: "cache";
47
+ readonly setup: (api: _dotpress_core.PluginAPI) => void;
48
+ };
49
+
50
+ export { cachePlugin };
51
+ export type { CachePluginOptions, MemoScope };
package/dist/plugin.js ADDED
@@ -0,0 +1,49 @@
1
+ import {
2
+ createMemo
3
+ } from "./chunk-6ZNJJARH.js";
4
+
5
+ // src/plugin.ts
6
+ import { declareContextKey, definePlugin } from "@dotpress/core";
7
+ declareContextKey("cache", "cachePlugin");
8
+ declareContextKey("memo", "cachePlugin");
9
+ var cachePlugin = (options = {}) => {
10
+ const label = "cachePlugin()";
11
+ if (typeof options !== "object" || options === null) {
12
+ throw new TypeError(`${label}: expected an object of options.`);
13
+ }
14
+ for (const key of Object.keys(options)) {
15
+ if (key !== "cache" && key !== "memoScope") {
16
+ throw new TypeError(`${label}: unknown option "${key}".`);
17
+ }
18
+ }
19
+ const { cache, memoScope } = options;
20
+ if (cache !== void 0 && typeof cache?.withLogger !== "function") {
21
+ throw new TypeError(`${label}: cache must be created by createCache().`);
22
+ }
23
+ if (memoScope !== void 0 && typeof memoScope?.run !== "function") {
24
+ throw new TypeError(
25
+ `${label}: memoScope must have a run() method, like asyncLocalMemo.`
26
+ );
27
+ }
28
+ return definePlugin({
29
+ name: "cache",
30
+ setup(api) {
31
+ api.decorateContext("memo", () => createMemo());
32
+ api.decorateContext("cache", (ctx) => {
33
+ if (!cache) {
34
+ throw new Error(
35
+ '"ctx.cache" is used, but cachePlugin() has no cache: pass cachePlugin({ cache }).'
36
+ );
37
+ }
38
+ return cache.withLogger(ctx.logger);
39
+ });
40
+ if (memoScope) {
41
+ api.hooks.wrapRequest((ctx, next) => memoScope.run(ctx.memo, next));
42
+ }
43
+ }
44
+ });
45
+ };
46
+ export {
47
+ cachePlugin
48
+ };
49
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import { declareContextKey, definePlugin } from '@dotpress/core'\nimport type { Cache } from './cache.js'\nimport { createMemo, type Memo } from './memo.js'\n\ndeclare module '@dotpress/core' {\n interface RequestContext {\n /** Cache (`cachePlugin`), logging with the request logger. */\n cache: Cache\n /** Memo of the request (`cachePlugin`): a new one for each request. */\n memo: Memo\n }\n}\n\n// Accessing ctx.cache or ctx.memo without the plugin throws an explicit\n// error.\ndeclareContextKey('cache', 'cachePlugin')\ndeclareContextKey('memo', 'cachePlugin')\n\n/**\n * Runs each request inside a scope for its memo, like `asyncLocalMemo` of\n * `@dotpress/cache/als`.\n */\nexport interface MemoScope {\n run<T>(memo: Memo, next: () => Promise<T>): Promise<T>\n}\n\n/** Options of `cachePlugin()`. */\nexport interface CachePluginOptions {\n /** Provides `ctx.cache`. */\n cache?: Cache | undefined\n /**\n * Runs each request inside the scope of its memo, so that memoized\n * loaders work without `ctx` (`asyncLocalMemo` of `@dotpress/cache/als`).\n */\n memoScope?: MemoScope | undefined\n}\n\n/**\n * Provides `ctx.memo` (a new memo for each request) and, with a cache,\n * `ctx.cache` (logging with the request logger).\n *\n * ```ts\n * await createApp({ adapter, plugins: [cachePlugin({ cache })] })\n *\n * defineRoute({\n * path: '/me',\n * handler: ({ memo, cache, auth }) =>\n * memo(loadUser, auth.userId), // or cache.getOrSet(userCache, …)\n * })\n * ```\n */\nexport const cachePlugin = (options: CachePluginOptions = {}) => {\n const label = 'cachePlugin()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (key !== 'cache' && key !== 'memoScope') {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const { cache, memoScope } = options\n if (cache !== undefined && typeof cache?.withLogger !== 'function') {\n throw new TypeError(`${label}: cache must be created by createCache().`)\n }\n if (memoScope !== undefined && typeof memoScope?.run !== 'function') {\n throw new TypeError(\n `${label}: memoScope must have a run() method, like asyncLocalMemo.`\n )\n }\n\n return definePlugin({\n name: 'cache',\n setup(api) {\n api.decorateContext('memo', () => createMemo())\n api.decorateContext('cache', (ctx) => {\n if (!cache) {\n throw new Error(\n '\"ctx.cache\" is used, but cachePlugin() has no cache: pass cachePlugin({ cache }).'\n )\n }\n return cache.withLogger(ctx.logger)\n })\n if (memoScope) {\n api.hooks.wrapRequest((ctx, next) => memoScope.run(ctx.memo, next))\n }\n },\n })\n}\n"],"mappings":";;;;;AAAA,SAAS,mBAAmB,oBAAoB;AAehD,kBAAkB,SAAS,aAAa;AACxC,kBAAkB,QAAQ,aAAa;AAmChC,IAAM,cAAc,CAAC,UAA8B,CAAC,MAAM;AAC/D,QAAM,QAAQ;AACd,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,UAAM,IAAI,UAAU,GAAG,KAAK,kCAAkC;AAAA,EAChE;AACA,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,QAAI,QAAQ,WAAW,QAAQ,aAAa;AAC1C,YAAM,IAAI,UAAU,GAAG,KAAK,qBAAqB,GAAG,IAAI;AAAA,IAC1D;AAAA,EACF;AACA,QAAM,EAAE,OAAO,UAAU,IAAI;AAC7B,MAAI,UAAU,UAAa,OAAO,OAAO,eAAe,YAAY;AAClE,UAAM,IAAI,UAAU,GAAG,KAAK,2CAA2C;AAAA,EACzE;AACA,MAAI,cAAc,UAAa,OAAO,WAAW,QAAQ,YAAY;AACnE,UAAM,IAAI;AAAA,MACR,GAAG,KAAK;AAAA,IACV;AAAA,EACF;AAEA,SAAO,aAAa;AAAA,IAClB,MAAM;AAAA,IACN,MAAM,KAAK;AACT,UAAI,gBAAgB,QAAQ,MAAM,WAAW,CAAC;AAC9C,UAAI,gBAAgB,SAAS,CAAC,QAAQ;AACpC,YAAI,CAAC,OAAO;AACV,gBAAM,IAAI;AAAA,YACR;AAAA,UACF;AAAA,QACF;AACA,eAAO,MAAM,WAAW,IAAI,MAAM;AAAA,MACpC,CAAC;AACD,UAAI,WAAW;AACb,YAAI,MAAM,YAAY,CAAC,KAAK,SAAS,UAAU,IAAI,IAAI,MAAM,IAAI,CAAC;AAAA,MACpE;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,39 @@
1
+ import { CacheStore } from './index.js';
2
+
3
+ /** A connected node-redis client (`createClient()` of `redis`, v4 or later). */
4
+ interface NodeRedisClient {
5
+ sendCommand(args: string[]): Promise<unknown>;
6
+ }
7
+ /** An ioredis client (`new Redis()` of `ioredis`). */
8
+ interface IoRedisClient {
9
+ call(command: string, ...args: string[]): Promise<unknown>;
10
+ }
11
+ /** Options of `createRedisStore()`. */
12
+ interface RedisStoreOptions {
13
+ /**
14
+ * A node-redis client (connected) or an ioredis client, created and
15
+ * closed by the app. Redis and Valkey servers, without cluster mode.
16
+ */
17
+ client: NodeRedisClient | IoRedisClient;
18
+ /** Name of the store, in logs and errors. Defaults to `redis`. */
19
+ name?: string | undefined;
20
+ }
21
+ /**
22
+ * Creates a store on Redis or Valkey, with the client of the app
23
+ * (node-redis or ioredis). It supports `getMany` (MGET) and the `lock`
24
+ * option of the cache (`SET NX PX`, released by a Lua script).
25
+ *
26
+ * ```ts
27
+ * import { createClient } from 'redis'
28
+ * import { createRedisStore } from '@dotpress/cache/redis'
29
+ *
30
+ * const client = createClient({ url: process.env.REDIS_URL })
31
+ * client.on('error', (error) => logger.error({ error }, 'Redis error'))
32
+ * await client.connect()
33
+ * const cache = createCache({ store: createRedisStore({ client }), prefix: 'app' })
34
+ * ```
35
+ */
36
+ declare const createRedisStore: (options: RedisStoreOptions) => CacheStore;
37
+
38
+ export { createRedisStore };
39
+ export type { IoRedisClient, NodeRedisClient, RedisStoreOptions };
package/dist/redis.js ADDED
@@ -0,0 +1,73 @@
1
+ // src/stores/redis.ts
2
+ var UNLOCK = `if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`;
3
+ var valueOf = (reply) => {
4
+ if (reply === null) return void 0;
5
+ if (typeof reply === "string") return reply;
6
+ throw new TypeError(
7
+ `Unexpected reply from Redis (${typeof reply}): use a client that returns strings.`
8
+ );
9
+ };
10
+ var createRedisStore = (options) => {
11
+ const label = "createRedisStore()";
12
+ if (typeof options !== "object" || options === null) {
13
+ throw new TypeError(`${label}: expected an object of options.`);
14
+ }
15
+ for (const key of Object.keys(options)) {
16
+ if (key !== "client" && key !== "name") {
17
+ throw new TypeError(`${label}: unknown option "${key}".`);
18
+ }
19
+ }
20
+ const { client, name = "redis" } = options;
21
+ if (typeof name !== "string" || name === "") {
22
+ throw new TypeError(`${label}: name must be a non-empty string.`);
23
+ }
24
+ const value = client;
25
+ if (value?.isCluster === true || typeof value?.getMasters === "function") {
26
+ throw new TypeError(
27
+ `${label}: Redis Cluster clients are not supported: use a client of a standalone server (or of the primary).`
28
+ );
29
+ }
30
+ let run;
31
+ if (typeof value?.call === "function") {
32
+ const io = client;
33
+ run = ([command, ...args]) => io.call(command, ...args);
34
+ } else if (typeof value?.sendCommand === "function") {
35
+ const node = client;
36
+ run = (args) => node.sendCommand(args);
37
+ } else {
38
+ throw new TypeError(
39
+ `${label}: client must be a node-redis or an ioredis client.`
40
+ );
41
+ }
42
+ const px = (ttl) => String(Math.ceil(ttl));
43
+ return Object.freeze({
44
+ name,
45
+ get: async (key) => valueOf(await run(["GET", key])),
46
+ getMany: async (keys) => {
47
+ if (keys.length === 0) return [];
48
+ const reply = await run(["MGET", ...keys]);
49
+ if (!Array.isArray(reply) || reply.length !== keys.length) {
50
+ throw new TypeError("Unexpected reply from Redis to MGET.");
51
+ }
52
+ return reply.map(valueOf);
53
+ },
54
+ set: async (key, value2, ttl) => {
55
+ await run(["SET", key, value2, "PX", px(ttl)]);
56
+ },
57
+ delete: async (key) => {
58
+ await run(["DEL", key]);
59
+ },
60
+ lock: async (key, ttl) => {
61
+ const token = crypto.randomUUID();
62
+ const reply = await run(["SET", key, token, "NX", "PX", px(ttl)]);
63
+ return reply === "OK" ? token : void 0;
64
+ },
65
+ unlock: async (key, token) => {
66
+ await run(["EVAL", UNLOCK, "1", key, token]);
67
+ }
68
+ });
69
+ };
70
+ export {
71
+ createRedisStore
72
+ };
73
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stores/redis.ts"],"sourcesContent":["import type { CacheStore } from '../store.js'\n\n/** A connected node-redis client (`createClient()` of `redis`, v4 or later). */\nexport interface NodeRedisClient {\n sendCommand(args: string[]): Promise<unknown>\n}\n\n/** An ioredis client (`new Redis()` of `ioredis`). */\nexport interface IoRedisClient {\n call(command: string, ...args: string[]): Promise<unknown>\n}\n\n/** Options of `createRedisStore()`. */\nexport interface RedisStoreOptions {\n /**\n * A node-redis client (connected) or an ioredis client, created and\n * closed by the app. Redis and Valkey servers, without cluster mode.\n */\n client: NodeRedisClient | IoRedisClient\n /** Name of the store, in logs and errors. Defaults to `redis`. */\n name?: string | undefined\n}\n\n// Releases a lock only if it is still held with this token.\nconst UNLOCK = `if redis.call(\"get\", KEYS[1]) == ARGV[1] then return redis.call(\"del\", KEYS[1]) else return 0 end`\n\n/** A value of a GET or MGET reply: a string, or undefined for nil (null). */\nconst valueOf = (reply: unknown) => {\n if (reply === null) return undefined\n if (typeof reply === 'string') return reply\n throw new TypeError(\n `Unexpected reply from Redis (${typeof reply}): use a client that returns strings.`\n )\n}\n\n/**\n * Creates a store on Redis or Valkey, with the client of the app\n * (node-redis or ioredis). It supports `getMany` (MGET) and the `lock`\n * option of the cache (`SET NX PX`, released by a Lua script).\n *\n * ```ts\n * import { createClient } from 'redis'\n * import { createRedisStore } from '@dotpress/cache/redis'\n *\n * const client = createClient({ url: process.env.REDIS_URL })\n * client.on('error', (error) => logger.error({ error }, 'Redis error'))\n * await client.connect()\n * const cache = createCache({ store: createRedisStore({ client }), prefix: 'app' })\n * ```\n */\nexport const createRedisStore = (options: RedisStoreOptions): CacheStore => {\n const label = 'createRedisStore()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (key !== 'client' && key !== 'name') {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const { client, name = 'redis' } = options\n if (typeof name !== 'string' || name === '') {\n throw new TypeError(`${label}: name must be a non-empty string.`)\n }\n const value = client as unknown as Record<string, unknown> | null\n if (value?.isCluster === true || typeof value?.getMasters === 'function') {\n throw new TypeError(\n `${label}: Redis Cluster clients are not supported: use a client of a standalone server (or of the primary).`\n )\n }\n\n let run: (args: string[]) => Promise<unknown>\n if (typeof value?.call === 'function') {\n const io = client as IoRedisClient\n run = ([command, ...args]) => io.call(command as string, ...args)\n } else if (typeof value?.sendCommand === 'function') {\n const node = client as NodeRedisClient\n run = (args) => node.sendCommand(args)\n } else {\n throw new TypeError(\n `${label}: client must be a node-redis or an ioredis client.`\n )\n }\n\n /** TTL in whole milliseconds (the cache gives at least 1). */\n const px = (ttl: number) => String(Math.ceil(ttl))\n\n return Object.freeze({\n name,\n get: async (key: string) => valueOf(await run(['GET', key])),\n getMany: async (keys: readonly string[]) => {\n if (keys.length === 0) return []\n const reply = await run(['MGET', ...keys])\n if (!Array.isArray(reply) || reply.length !== keys.length) {\n throw new TypeError('Unexpected reply from Redis to MGET.')\n }\n return reply.map(valueOf)\n },\n set: async (key: string, value: string, ttl: number) => {\n await run(['SET', key, value, 'PX', px(ttl)])\n },\n delete: async (key: string) => {\n await run(['DEL', key])\n },\n lock: async (key: string, ttl: number) => {\n const token = crypto.randomUUID()\n const reply = await run(['SET', key, token, 'NX', 'PX', px(ttl)])\n return reply === 'OK' ? token : undefined\n },\n unlock: async (key: string, token: string) => {\n await run(['EVAL', UNLOCK, '1', key, token])\n },\n })\n}\n"],"mappings":";AAwBA,IAAM,SAAS;AAGf,IAAM,UAAU,CAAC,UAAmB;AAClC,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,IAAI;AAAA,IACR,gCAAgC,OAAO,KAAK;AAAA,EAC9C;AACF;AAiBO,IAAM,mBAAmB,CAAC,YAA2C;AAC1E,QAAM,QAAQ;AACd,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,UAAM,IAAI,UAAU,GAAG,KAAK,kCAAkC;AAAA,EAChE;AACA,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,QAAI,QAAQ,YAAY,QAAQ,QAAQ;AACtC,YAAM,IAAI,UAAU,GAAG,KAAK,qBAAqB,GAAG,IAAI;AAAA,IAC1D;AAAA,EACF;AACA,QAAM,EAAE,QAAQ,OAAO,QAAQ,IAAI;AACnC,MAAI,OAAO,SAAS,YAAY,SAAS,IAAI;AAC3C,UAAM,IAAI,UAAU,GAAG,KAAK,oCAAoC;AAAA,EAClE;AACA,QAAM,QAAQ;AACd,MAAI,OAAO,cAAc,QAAQ,OAAO,OAAO,eAAe,YAAY;AACxE,UAAM,IAAI;AAAA,MACR,GAAG,KAAK;AAAA,IACV;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,OAAO,OAAO,SAAS,YAAY;AACrC,UAAM,KAAK;AACX,UAAM,CAAC,CAAC,SAAY,OAAI,MAAM,GAAG,KAAK,SAAmB,GAAG,IAAI;AAAA,EAClE,WAAW,OAAO,OAAO,gBAAgB,YAAY;AACnD,UAAM,OAAO;AACb,UAAM,CAAC,SAAS,KAAK,YAAY,IAAI;AAAA,EACvC,OAAO;AACL,UAAM,IAAI;AAAA,MACR,GAAG,KAAK;AAAA,IACV;AAAA,EACF;AAGA,QAAM,KAAK,CAAC,QAAgB,OAAO,KAAK,KAAK,GAAG,CAAC;AAEjD,SAAO,OAAO,OAAO;AAAA,IACnB;AAAA,IACA,KAAK,OAAO,QAAgB,QAAQ,MAAM,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC;AAAA,IAC3D,SAAS,OAAO,SAA4B;AAC1C,UAAI,KAAK,WAAW,EAAG,QAAO,CAAC;AAC/B,YAAM,QAAQ,MAAM,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;AACzC,UAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,KAAK,QAAQ;AACzD,cAAM,IAAI,UAAU,sCAAsC;AAAA,MAC5D;AACA,aAAO,MAAM,IAAI,OAAO;AAAA,IAC1B;AAAA,IACA,KAAK,OAAO,KAAaA,QAAe,QAAgB;AACtD,YAAM,IAAI,CAAC,OAAO,KAAKA,QAAO,MAAM,GAAG,GAAG,CAAC,CAAC;AAAA,IAC9C;AAAA,IACA,QAAQ,OAAO,QAAgB;AAC7B,YAAM,IAAI,CAAC,OAAO,GAAG,CAAC;AAAA,IACxB;AAAA,IACA,MAAM,OAAO,KAAa,QAAgB;AACxC,YAAM,QAAQ,OAAO,WAAW;AAChC,YAAM,QAAQ,MAAM,IAAI,CAAC,OAAO,KAAK,OAAO,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC;AAChE,aAAO,UAAU,OAAO,QAAQ;AAAA,IAClC;AAAA,IACA,QAAQ,OAAO,KAAa,UAAkB;AAC5C,YAAM,IAAI,CAAC,QAAQ,QAAQ,KAAK,KAAK,KAAK,CAAC;AAAA,IAC7C;AAAA,EACF,CAAC;AACH;","names":["value"]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Where the cache keeps its entries: Redis, Upstash, Cloudflare KV,
3
+ * memory… Values are strings, serialized by the cache.
4
+ */
5
+ interface CacheStore {
6
+ /** Name of the store, in logs and errors. */
7
+ readonly name: string;
8
+ /**
9
+ * Longest key accepted, in bytes (UTF-8). Longer keys are hashed by the
10
+ * cache. Undefined without limit.
11
+ */
12
+ readonly maxKeyLength?: number | undefined;
13
+ /** Value of a key, or undefined when it is missing or expired. */
14
+ get(key: string): Promise<string | undefined>;
15
+ /**
16
+ * Values of several keys, in their order: one round trip for the tags of
17
+ * an entry. Optional: the cache calls `get` for each key without it.
18
+ */
19
+ getMany?(keys: readonly string[]): Promise<(string | undefined)[]>;
20
+ /** Sets a value that expires after `ttl` milliseconds (at least 1). */
21
+ set(key: string, value: string, ttl: number): Promise<void>;
22
+ /** Removes a key; missing keys are ignored. */
23
+ delete(key: string): Promise<void>;
24
+ /**
25
+ * Takes a lock for `ttl` milliseconds: returns its token, or undefined
26
+ * when another holder has it. Optional: the `lock` option of the cache
27
+ * needs it.
28
+ */
29
+ lock?(key: string, ttl: number): Promise<string | undefined>;
30
+ /** Releases a lock, if the token still holds it. */
31
+ unlock?(key: string, token: string): Promise<void>;
32
+ }
33
+ /** Options of `createMemoryStore()`. */
34
+ interface MemoryStoreOptions {
35
+ /**
36
+ * Entries kept at most: the least recently used ones are removed
37
+ * first. Defaults to 10,000.
38
+ */
39
+ maxEntries?: number | undefined;
40
+ }
41
+ /** Store in the memory of the process: for one instance, and for tests. */
42
+ interface MemoryStore extends CacheStore {
43
+ /** Number of entries, expired ones included until they are read. */
44
+ readonly size: number;
45
+ /** Removes every entry. */
46
+ clear(): void;
47
+ }
48
+ /**
49
+ * Creates a store in the memory of the process, least recently used
50
+ * entries removed first. Each instance has its own: use a shared store
51
+ * (Redis…) for several instances.
52
+ */
53
+ declare const createMemoryStore: (options?: MemoryStoreOptions) => MemoryStore;
54
+
55
+ export { createMemoryStore as c };
56
+ export type { CacheStore as C, MemoryStore as M, MemoryStoreOptions as a };
@@ -0,0 +1,30 @@
1
+ import { CacheStore } from './index.js';
2
+
3
+ /** Options of `createUpstashStore()`. */
4
+ interface UpstashStoreOptions {
5
+ /** REST URL of the database (`UPSTASH_REDIS_REST_URL`). */
6
+ url: string;
7
+ /** REST token of the database (`UPSTASH_REDIS_REST_TOKEN`), not read-only. */
8
+ token: string;
9
+ /** `fetch` implementation. Defaults to the global `fetch`. */
10
+ fetch?: typeof fetch | undefined;
11
+ /** Name of the store, in logs and errors. Defaults to `upstash`. */
12
+ name?: string | undefined;
13
+ }
14
+ /**
15
+ * Creates a store on [Upstash Redis](https://upstash.com/docs/redis), over
16
+ * its REST API with `fetch`: no dependency, and it runs on every runtime
17
+ * (Cloudflare Workers, Vercel, Deno, Node.js…). It supports `getMany` and
18
+ * the `lock` option of the cache.
19
+ *
20
+ * ```ts
21
+ * const store = createUpstashStore({
22
+ * url: env.UPSTASH_REDIS_REST_URL,
23
+ * token: env.UPSTASH_REDIS_REST_TOKEN,
24
+ * })
25
+ * ```
26
+ */
27
+ declare const createUpstashStore: (options: UpstashStoreOptions) => CacheStore;
28
+
29
+ export { createUpstashStore };
30
+ export type { UpstashStoreOptions };
@@ -0,0 +1,88 @@
1
+ // src/stores/upstash.ts
2
+ var UNLOCK = `if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end`;
3
+ var valueOf = (result) => {
4
+ if (result === null) return void 0;
5
+ if (typeof result === "string") return result;
6
+ throw new TypeError(`Unexpected result from Upstash (${typeof result}).`);
7
+ };
8
+ var createUpstashStore = (options) => {
9
+ const label = "createUpstashStore()";
10
+ if (typeof options !== "object" || options === null) {
11
+ throw new TypeError(`${label}: expected an object of options.`);
12
+ }
13
+ for (const key of Object.keys(options)) {
14
+ if (!["url", "token", "fetch", "name"].includes(key)) {
15
+ throw new TypeError(`${label}: unknown option "${key}".`);
16
+ }
17
+ }
18
+ const {
19
+ url,
20
+ token,
21
+ fetch: fetchFn = globalThis.fetch,
22
+ name = "upstash"
23
+ } = options;
24
+ if (typeof url !== "string" || !/^https?:\/\/[^/]/.test(url)) {
25
+ throw new TypeError(
26
+ `${label}: url must be the REST URL of the database (UPSTASH_REDIS_REST_URL).`
27
+ );
28
+ }
29
+ if (typeof token !== "string" || token === "") {
30
+ throw new TypeError(
31
+ `${label}: token must be the REST token of the database (UPSTASH_REDIS_REST_TOKEN).`
32
+ );
33
+ }
34
+ if (typeof fetchFn !== "function") {
35
+ throw new TypeError(`${label}: fetch must be a function.`);
36
+ }
37
+ if (typeof name !== "string" || name === "") {
38
+ throw new TypeError(`${label}: name must be a non-empty string.`);
39
+ }
40
+ const endpoint = url.replace(/\/+$/, "");
41
+ const run = async (command) => {
42
+ const response = await fetchFn(endpoint, {
43
+ method: "POST",
44
+ headers: {
45
+ authorization: `Bearer ${token}`,
46
+ "content-type": "application/json"
47
+ },
48
+ body: JSON.stringify(command)
49
+ });
50
+ const body = await response.json().catch(() => void 0);
51
+ if (!response.ok || typeof body?.error === "string" || !body) {
52
+ const detail = typeof body?.error === "string" ? body.error : response.statusText;
53
+ throw new Error(`Upstash: ${response.status} ${detail}`.trim());
54
+ }
55
+ return body.result;
56
+ };
57
+ const px = (ttl) => String(Math.ceil(ttl));
58
+ return Object.freeze({
59
+ name,
60
+ get: async (key) => valueOf(await run(["GET", key])),
61
+ getMany: async (keys) => {
62
+ if (keys.length === 0) return [];
63
+ const result = await run(["MGET", ...keys]);
64
+ if (!Array.isArray(result) || result.length !== keys.length) {
65
+ throw new TypeError("Unexpected result from Upstash to MGET.");
66
+ }
67
+ return result.map(valueOf);
68
+ },
69
+ set: async (key, value, ttl) => {
70
+ await run(["SET", key, value, "PX", px(ttl)]);
71
+ },
72
+ delete: async (key) => {
73
+ await run(["DEL", key]);
74
+ },
75
+ lock: async (key, ttl) => {
76
+ const lockToken = crypto.randomUUID();
77
+ const result = await run(["SET", key, lockToken, "NX", "PX", px(ttl)]);
78
+ return result === "OK" ? lockToken : void 0;
79
+ },
80
+ unlock: async (key, lockToken) => {
81
+ await run(["EVAL", UNLOCK, "1", key, lockToken]);
82
+ }
83
+ });
84
+ };
85
+ export {
86
+ createUpstashStore
87
+ };
88
+ //# sourceMappingURL=upstash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stores/upstash.ts"],"sourcesContent":["import type { CacheStore } from '../store.js'\n\n/** Options of `createUpstashStore()`. */\nexport interface UpstashStoreOptions {\n /** REST URL of the database (`UPSTASH_REDIS_REST_URL`). */\n url: string\n /** REST token of the database (`UPSTASH_REDIS_REST_TOKEN`), not read-only. */\n token: string\n /** `fetch` implementation. Defaults to the global `fetch`. */\n fetch?: typeof fetch | undefined\n /** Name of the store, in logs and errors. Defaults to `upstash`. */\n name?: string | undefined\n}\n\n// Releases a lock only if it is still held with this token.\nconst UNLOCK = `if redis.call(\"get\", KEYS[1]) == ARGV[1] then return redis.call(\"del\", KEYS[1]) else return 0 end`\n\n/** A value of a GET or MGET result: a string, or undefined for null. */\nconst valueOf = (result: unknown) => {\n if (result === null) return undefined\n if (typeof result === 'string') return result\n throw new TypeError(`Unexpected result from Upstash (${typeof result}).`)\n}\n\n/**\n * Creates a store on [Upstash Redis](https://upstash.com/docs/redis), over\n * its REST API with `fetch`: no dependency, and it runs on every runtime\n * (Cloudflare Workers, Vercel, Deno, Node.js…). It supports `getMany` and\n * the `lock` option of the cache.\n *\n * ```ts\n * const store = createUpstashStore({\n * url: env.UPSTASH_REDIS_REST_URL,\n * token: env.UPSTASH_REDIS_REST_TOKEN,\n * })\n * ```\n */\nexport const createUpstashStore = (\n options: UpstashStoreOptions\n): CacheStore => {\n const label = 'createUpstashStore()'\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: expected an object of options.`)\n }\n for (const key of Object.keys(options)) {\n if (!['url', 'token', 'fetch', 'name'].includes(key)) {\n throw new TypeError(`${label}: unknown option \"${key}\".`)\n }\n }\n const {\n url,\n token,\n fetch: fetchFn = globalThis.fetch,\n name = 'upstash',\n } = options\n if (typeof url !== 'string' || !/^https?:\\/\\/[^/]/.test(url)) {\n throw new TypeError(\n `${label}: url must be the REST URL of the database (UPSTASH_REDIS_REST_URL).`\n )\n }\n if (typeof token !== 'string' || token === '') {\n throw new TypeError(\n `${label}: token must be the REST token of the database (UPSTASH_REDIS_REST_TOKEN).`\n )\n }\n if (typeof fetchFn !== 'function') {\n throw new TypeError(`${label}: fetch must be a function.`)\n }\n if (typeof name !== 'string' || name === '') {\n throw new TypeError(`${label}: name must be a non-empty string.`)\n }\n const endpoint = url.replace(/\\/+$/, '')\n\n /** Runs a command: its result, or an error with the message of Upstash. */\n const run = async (command: string[]) => {\n const response = await fetchFn(endpoint, {\n method: 'POST',\n headers: {\n authorization: `Bearer ${token}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(command),\n })\n const body = (await response.json().catch(() => undefined)) as\n { result?: unknown; error?: unknown } | undefined\n if (!response.ok || typeof body?.error === 'string' || !body) {\n const detail =\n typeof body?.error === 'string' ? body.error : response.statusText\n throw new Error(`Upstash: ${response.status} ${detail}`.trim())\n }\n return body.result\n }\n\n const px = (ttl: number) => String(Math.ceil(ttl))\n\n return Object.freeze({\n name,\n get: async (key: string) => valueOf(await run(['GET', key])),\n getMany: async (keys: readonly string[]) => {\n if (keys.length === 0) return []\n const result = await run(['MGET', ...keys])\n if (!Array.isArray(result) || result.length !== keys.length) {\n throw new TypeError('Unexpected result from Upstash to MGET.')\n }\n return result.map(valueOf)\n },\n set: async (key: string, value: string, ttl: number) => {\n await run(['SET', key, value, 'PX', px(ttl)])\n },\n delete: async (key: string) => {\n await run(['DEL', key])\n },\n lock: async (key: string, ttl: number) => {\n const lockToken = crypto.randomUUID()\n const result = await run(['SET', key, lockToken, 'NX', 'PX', px(ttl)])\n return result === 'OK' ? lockToken : undefined\n },\n unlock: async (key: string, lockToken: string) => {\n await run(['EVAL', UNLOCK, '1', key, lockToken])\n },\n })\n}\n"],"mappings":"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","names":[]}