UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

136 lines (122 loc) 3.36 kB
/** * Options for storing a cache value. */ export interface CacheSetOptions { /** * Time-to-live in seconds. Omit this for a value that does not expire. */ ttlSeconds?: number; } /** * App-facing string cache port. * * Values are intentionally strings so adapters can map cleanly to Redis, * Upstash, and other key/value stores. Serialize structured values at the app * boundary. */ export interface CachePort { /** * Return a fresh value for `key`, or `null` when missing or expired. */ get(key: string): Promise<string | null>; /** * Store a string value. */ set(key: string, value: string, options?: CacheSetOptions): Promise<void>; /** * Delete a cache key. * * @returns `true` when the key existed. */ delete(key: string): Promise<boolean>; /** * Return whether a fresh value exists for `key`. */ has(key: string): Promise<boolean>; /** * Return a cached value or compute, store, and return a new value. * * Implementations are not required to provide single-flight behavior. If * concurrent cache fills matter, choose an adapter that documents that * guarantee or protect the factory at the application layer. */ remember( key: string, factory: () => Promise<string>, options?: CacheSetOptions, ): Promise<string>; } type MemoryCacheEntry = { value: string; expiresAt: number | null; }; function resolveExpiresAt(options: CacheSetOptions | undefined): number | null { if (options?.ttlSeconds == null) { return null; } if (options.ttlSeconds <= 0) { return Date.now(); } return Date.now() + options.ttlSeconds * 1000; } function isExpired(entry: MemoryCacheEntry): boolean { return entry.expiresAt != null && entry.expiresAt <= Date.now(); } /** * Create an in-memory cache for tests, examples, and single-process * development. * * This adapter is not durable or distributed. Values are lost when the process * exits and are not shared across workers, regions, or serverless invocations. * * @param initialValues - Optional initial string values without TTL. * @returns A cache port backed by a local `Map`. */ export function createMemoryCache( initialValues: Record<string, string> = {}, ): CachePort { const values = new Map<string, MemoryCacheEntry>( Object.entries(initialValues).map(([key, value]) => [ key, { value, expiresAt: null }, ]), ); async function getFreshEntry(key: string): Promise<MemoryCacheEntry | null> { const entry = values.get(key); if (!entry) { return null; } if (isExpired(entry)) { values.delete(key); return null; } return entry; } const cache: CachePort = { async get(key) { return (await getFreshEntry(key))?.value ?? null; }, async set(key, value, options) { values.set(key, { value, expiresAt: resolveExpiresAt(options), }); }, async delete(key) { return values.delete(key); }, async has(key) { return (await getFreshEntry(key)) != null; }, async remember(key, factory, options) { const cached = await cache.get(key); if (cached != null) { return cached; } const value = await factory(); await cache.set(key, value, options); return value; }, }; return cache; }