@nodellmcache/core 1.0.0 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @nodellmcache/core
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add stale-while-revalidate to `BaseCacheManager` via `getOrRevalidate(input, generator, { ttl, staleTtl })`: a stale-but-unexpired entry is served immediately while a fresh value is fetched in the background, with concurrent stale hits coalesced into a single refresh. Adds `staleAt` to `CacheMetadata` and `staleTtl` to `CacheOptions`.
8
+ - Add request coalescing (singleflight): concurrent misses for the same key run the generator once instead of N times. Applies to both `getOrGenerate` and `getOrRevalidate`. Inherited by every cache manager.
9
+
3
10
  ## 1.0.0
4
11
 
5
12
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -145,6 +145,8 @@ var BaseCacheManager = class {
145
145
  misses = 0;
146
146
  /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */
147
147
  refreshing = /* @__PURE__ */ new Set();
148
+ /** In-flight generations, so concurrent misses for the same key run the generator once. */
149
+ inflight = /* @__PURE__ */ new Map();
148
150
  constructor(options) {
149
151
  this.adapter = options.adapter;
150
152
  this.defaultTTL = options.defaultTTL;
@@ -215,9 +217,7 @@ var BaseCacheManager = class {
215
217
  provider: options?.provider,
216
218
  model: options?.model
217
219
  });
218
- const value = await generator();
219
- await this.persist(key, value, options, start);
220
- return value;
220
+ return this.coalescedGenerate(key, generator, options, start);
221
221
  }
222
222
  /**
223
223
  * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a
@@ -256,9 +256,7 @@ var BaseCacheManager = class {
256
256
  provider: options?.provider,
257
257
  model: options?.model
258
258
  });
259
- const value = await generator();
260
- await this.persist(key, value, options, start);
261
- return value;
259
+ return this.coalescedGenerate(key, generator, options, start);
262
260
  }
263
261
  /**
264
262
  * Removes a single entry by its input (and namespacing options).
@@ -279,6 +277,23 @@ var BaseCacheManager = class {
279
277
  entryCount: adapterStats.entryCount
280
278
  };
281
279
  }
280
+ /**
281
+ * Runs the generator and persists the result, deduplicating concurrent calls
282
+ * for the same key (singleflight): while one generation is in flight, other
283
+ * callers join it instead of invoking the generator again. Prevents a
284
+ * thundering herd of identical API calls on a cold key.
285
+ */
286
+ coalescedGenerate(key, generator, options, start) {
287
+ const existing = this.inflight.get(key);
288
+ if (existing) return existing;
289
+ const promise = (async () => {
290
+ const value = await generator();
291
+ await this.persist(key, value, options, start);
292
+ return value;
293
+ })();
294
+ this.inflight.set(key, promise);
295
+ return promise.finally(() => this.inflight.delete(key));
296
+ }
282
297
  /** Builds an entry, writes it through the adapter, and emits `cache.set`. */
283
298
  async persist(key, value, options, start) {
284
299
  const ttl = options?.ttl ?? this.defaultTTL;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/KeyBuilder.ts","../src/TTLManager.ts","../src/Serializer.ts","../src/BaseCacheManager.ts"],"sourcesContent":["export * from './types.js'\nexport * from './interfaces.js'\nexport * from './errors.js'\nexport { KeyBuilder } from './KeyBuilder.js'\nexport { TTLManager } from './TTLManager.js'\nexport { type Serializer, JsonSerializer } from './Serializer.js'\nexport {\n BaseCacheManager,\n noopMetrics,\n type BaseCacheManagerOptions,\n} from './BaseCacheManager.js'\n","/**\n * Base class for every error thrown by NodeLLMCache packages. Catching this\n * catches anything the library throws intentionally.\n */\nexport class NodeLLMCacheError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = new.target.name\n // Restore prototype chain for instanceof across the ES5 transpile boundary.\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\n/** Thrown when a storage adapter operation fails. */\nexport class CacheAdapterError extends NodeLLMCacheError {}\n\n/** Thrown when compression or decompression fails. */\nexport class CompressionError extends NodeLLMCacheError {}\n\n/** Thrown when serialization or deserialization fails. */\nexport class SerializationError extends NodeLLMCacheError {}\n\n/** Thrown when a value fails validation (e.g. malformed configuration). */\nexport class ValidationError extends NodeLLMCacheError {}\n","import { createHash } from 'node:crypto'\nimport type { CacheType, LLMProvider } from './types.js'\n\n/**\n * Builds deterministic, collision-resistant cache keys.\n *\n * Format: `{type}:{provider}:{model}:{sha256-hash}`\n *\n * The raw input text is normalized and hashed with SHA-256 — keys never\n * contain raw prompt text, which keeps sensitive content out of storage keys\n * and logs.\n */\nexport class KeyBuilder {\n /**\n * Normalizes text before hashing so trivially different inputs collapse to\n * the same key: trims surrounding whitespace, lowercases, and collapses any\n * run of whitespace to a single space.\n */\n static normalize(text: string): string {\n return text.trim().toLowerCase().replace(/\\s+/g, ' ')\n }\n\n /**\n * Produces the SHA-256 hex digest of the normalized text.\n */\n static hash(text: string): string {\n return createHash('sha256').update(KeyBuilder.normalize(text)).digest('hex')\n }\n\n /**\n * Builds a fully namespaced cache key.\n *\n * @example\n * KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'hello world')\n * // 'prompt:openai:gpt-4o:b94d27b9...'\n */\n static build(\n type: CacheType,\n provider: LLMProvider | string,\n model: string,\n text: string,\n ): string {\n return `${type}:${provider}:${model}:${KeyBuilder.hash(text)}`\n }\n}\n","import type { CacheEntry } from './interfaces.js'\n\n/**\n * Centralizes time-to-live arithmetic: computing expiry timestamps, checking\n * expiry, and supporting sliding-window refresh. All durations are relative\n * milliseconds; all timestamps are absolute epoch milliseconds.\n */\nexport class TTLManager {\n /**\n * Computes the absolute expiry timestamp for an entry created at `createdAt`\n * with a relative `ttl`. Returns `undefined` when `ttl` is not a positive\n * number, meaning the entry never expires.\n */\n static computeExpiresAt(createdAt: number, ttl?: number): number | undefined {\n if (ttl === undefined || ttl <= 0) return undefined\n return createdAt + ttl\n }\n\n /**\n * Returns true when the entry has an expiry and that expiry is at or before\n * `now` (defaults to the current time).\n */\n static isExpired(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now: number = Date.now()): boolean {\n return entry.expiresAt !== undefined && entry.expiresAt <= now\n }\n\n /**\n * Milliseconds remaining until the entry expires. Returns `Infinity` for\n * entries with no expiry, and `0` for already-expired entries.\n */\n static remaining(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now: number = Date.now()): number {\n if (entry.expiresAt === undefined) return Infinity\n return Math.max(0, entry.expiresAt - now)\n }\n\n /**\n * Computes a refreshed expiry for a sliding-window TTL: extends the entry's\n * life by `ttl` from `now`. Returns `undefined` when `ttl` is not positive.\n */\n static slide(ttl?: number, now: number = Date.now()): number | undefined {\n return TTLManager.computeExpiresAt(now, ttl)\n }\n}\n","import { SerializationError } from './errors.js'\n\n/**\n * Converts values to and from `Buffer` for storage. Implementations decide the\n * wire format.\n *\n * The architecture calls for MessagePack as the primary format, but\n * `@nodellmcache/core` must stay dependency-free, so it ships only the JSON\n * implementation below. A MessagePack serializer can be supplied by an optional\n * package and injected wherever a `Serializer` is accepted.\n */\nexport interface Serializer {\n serialize<T>(value: T): Buffer\n deserialize<T>(data: Buffer): T\n}\n\n/**\n * Default JSON-backed serializer. Zero dependencies; handles the common case\n * and wraps failures in {@link SerializationError}.\n */\nexport class JsonSerializer implements Serializer {\n serialize<T>(value: T): Buffer {\n try {\n return Buffer.from(JSON.stringify(value), 'utf8')\n } catch (cause) {\n throw new SerializationError('Failed to serialize value to JSON', { cause })\n }\n }\n\n deserialize<T>(data: Buffer): T {\n try {\n return JSON.parse(data.toString('utf8')) as T\n } catch (cause) {\n throw new SerializationError('Failed to deserialize JSON value', { cause })\n }\n }\n}\n","import { KeyBuilder } from './KeyBuilder.js'\nimport { TTLManager } from './TTLManager.js'\nimport type {\n CacheEntry,\n CacheOptions,\n CacheStats,\n MetricsSink,\n StorageAdapter,\n} from './interfaces.js'\nimport type { CacheType } from './types.js'\n\n/** A metrics sink that discards everything; the default when none is injected. */\nexport const noopMetrics: MetricsSink = {\n emit() {\n // intentionally empty\n },\n}\n\n/**\n * Construction options shared by all cache managers.\n */\nexport interface BaseCacheManagerOptions<T> {\n adapter: StorageAdapter<T>\n /** Default relative TTL in milliseconds applied to entries lacking their own. */\n defaultTTL?: number\n /** Metrics sink; defaults to a no-op so core stays dependency-free. */\n metrics?: MetricsSink\n}\n\n/**\n * Shared base for every feature cache (prompt, embedding, semantic, ...).\n *\n * Provides the cache-aside `getOrGenerate` flow, key building, entry\n * construction, invalidation, and hit/miss accounting. Subclasses declare their\n * {@link CacheType} and may override {@link buildKey} for custom namespacing.\n */\nexport abstract class BaseCacheManager<T> {\n /** The workload category for keys and metrics. */\n protected abstract readonly cacheType: CacheType\n\n protected readonly adapter: StorageAdapter<T>\n protected readonly defaultTTL: number | undefined\n protected readonly metrics: MetricsSink\n\n private hits = 0\n private misses = 0\n /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */\n private readonly refreshing = new Set<string>()\n\n constructor(options: BaseCacheManagerOptions<T>) {\n this.adapter = options.adapter\n this.defaultTTL = options.defaultTTL\n this.metrics = options.metrics ?? noopMetrics\n }\n\n /**\n * Builds the storage key for an input. Defaults to the canonical\n * `{type}:{provider}:{model}:{hash}` format via {@link KeyBuilder}.\n */\n protected buildKey(input: string, options?: CacheOptions): string {\n return KeyBuilder.build(\n this.cacheType,\n options?.provider ?? 'unknown',\n options?.model ?? 'default',\n input,\n )\n }\n\n /**\n * Wraps a value in a {@link CacheEntry} with computed expiry and metadata.\n */\n protected buildEntry(key: string, value: T, options?: CacheOptions): CacheEntry<T> {\n const createdAt = Date.now()\n const ttl = options?.ttl ?? this.defaultTTL\n const staleTtl = options?.staleTtl\n return {\n key,\n value,\n createdAt,\n expiresAt: TTLManager.computeExpiresAt(createdAt, ttl),\n metadata: {\n compressed: false,\n originalSize: 0,\n cacheType: this.cacheType,\n provider: options?.provider,\n model: options?.model,\n tokenCount: options?.tokenCount,\n staleAt:\n staleTtl !== undefined && staleTtl > 0 ? createdAt + staleTtl : undefined,\n },\n }\n }\n\n /**\n * Cache-aside read-through. Returns the cached value on a hit, otherwise\n * invokes `generator`, stores the result, and returns it. Set\n * `options.cache = false` to bypass the cache for both read and write.\n */\n async getOrGenerate(\n input: string,\n generator: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n // A deliberate bypass is neither a hit nor a miss — skip the cache and\n // metrics entirely so accounting reflects only real cache consultations.\n if (options?.cache === false) {\n return generator()\n }\n\n const key = this.buildKey(input, options)\n const start = Date.now()\n\n const cached = await this.adapter.get(key)\n if (cached && !TTLManager.isExpired(cached)) {\n this.hits++\n this.metrics.emit('cache.hit', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n tokensSaved: cached.metadata.tokenCount,\n provider: options?.provider,\n model: options?.model,\n })\n return cached.value\n }\n\n this.misses++\n this.metrics.emit('cache.miss', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n\n const value = await generator()\n await this.persist(key, value, options, start)\n return value\n }\n\n /**\n * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a\n * cached entry is *stale* (past its `staleTtl`/`staleAt` but not yet expired)\n * it is returned immediately and a fresh value is fetched in the background.\n * Concurrent stale hits for the same key trigger at most one background\n * refresh. A failed background refresh is swallowed and the stale value stands\n * until it fully expires.\n */\n async getOrRevalidate(\n input: string,\n generator: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n if (options?.cache === false) {\n return generator()\n }\n\n const key = this.buildKey(input, options)\n const start = Date.now()\n\n const cached = await this.adapter.get(key)\n if (cached && !TTLManager.isExpired(cached)) {\n this.hits++\n this.metrics.emit('cache.hit', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n tokensSaved: cached.metadata.tokenCount,\n provider: options?.provider,\n model: options?.model,\n })\n const staleAt = cached.metadata.staleAt\n if (staleAt !== undefined && staleAt <= Date.now()) {\n this.revalidate(key, generator, options)\n }\n return cached.value\n }\n\n this.misses++\n this.metrics.emit('cache.miss', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n\n const value = await generator()\n await this.persist(key, value, options, start)\n return value\n }\n\n /**\n * Removes a single entry by its input (and namespacing options).\n */\n async invalidate(input: string, options?: CacheOptions): Promise<void> {\n await this.adapter.delete(this.buildKey(input, options))\n }\n\n /**\n * Returns hit/miss accounting for this manager plus the adapter's entry count.\n */\n async stats(): Promise<CacheStats> {\n const total = this.hits + this.misses\n const adapterStats = await this.adapter.stats()\n return {\n hits: this.hits,\n misses: this.misses,\n hitRate: total === 0 ? 0 : this.hits / total,\n entryCount: adapterStats.entryCount,\n }\n }\n\n /** Builds an entry, writes it through the adapter, and emits `cache.set`. */\n private async persist(\n key: string,\n value: T,\n options: CacheOptions | undefined,\n start: number,\n ): Promise<void> {\n const ttl = options?.ttl ?? this.defaultTTL\n const entry = this.buildEntry(key, value, options)\n await this.adapter.set(key, entry, ttl)\n this.metrics.emit('cache.set', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n }\n\n /** Fire-and-forget background refresh for a stale entry, coalesced per key. */\n private revalidate(key: string, generator: () => Promise<T>, options?: CacheOptions): void {\n if (this.refreshing.has(key)) return\n this.refreshing.add(key)\n void (async () => {\n try {\n const value = await generator()\n await this.persist(key, value, options, Date.now())\n } catch {\n // Swallow background-refresh failures: the stale value stands until expiry.\n } finally {\n this.refreshing.delete(key)\n }\n })()\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/KeyBuilder.ts","../src/TTLManager.ts","../src/Serializer.ts","../src/BaseCacheManager.ts"],"sourcesContent":["export * from './types.js'\nexport * from './interfaces.js'\nexport * from './errors.js'\nexport { KeyBuilder } from './KeyBuilder.js'\nexport { TTLManager } from './TTLManager.js'\nexport { type Serializer, JsonSerializer } from './Serializer.js'\nexport {\n BaseCacheManager,\n noopMetrics,\n type BaseCacheManagerOptions,\n} from './BaseCacheManager.js'\n","/**\n * Base class for every error thrown by NodeLLMCache packages. Catching this\n * catches anything the library throws intentionally.\n */\nexport class NodeLLMCacheError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = new.target.name\n // Restore prototype chain for instanceof across the ES5 transpile boundary.\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\n/** Thrown when a storage adapter operation fails. */\nexport class CacheAdapterError extends NodeLLMCacheError {}\n\n/** Thrown when compression or decompression fails. */\nexport class CompressionError extends NodeLLMCacheError {}\n\n/** Thrown when serialization or deserialization fails. */\nexport class SerializationError extends NodeLLMCacheError {}\n\n/** Thrown when a value fails validation (e.g. malformed configuration). */\nexport class ValidationError extends NodeLLMCacheError {}\n","import { createHash } from 'node:crypto'\nimport type { CacheType, LLMProvider } from './types.js'\n\n/**\n * Builds deterministic, collision-resistant cache keys.\n *\n * Format: `{type}:{provider}:{model}:{sha256-hash}`\n *\n * The raw input text is normalized and hashed with SHA-256 — keys never\n * contain raw prompt text, which keeps sensitive content out of storage keys\n * and logs.\n */\nexport class KeyBuilder {\n /**\n * Normalizes text before hashing so trivially different inputs collapse to\n * the same key: trims surrounding whitespace, lowercases, and collapses any\n * run of whitespace to a single space.\n */\n static normalize(text: string): string {\n return text.trim().toLowerCase().replace(/\\s+/g, ' ')\n }\n\n /**\n * Produces the SHA-256 hex digest of the normalized text.\n */\n static hash(text: string): string {\n return createHash('sha256').update(KeyBuilder.normalize(text)).digest('hex')\n }\n\n /**\n * Builds a fully namespaced cache key.\n *\n * @example\n * KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'hello world')\n * // 'prompt:openai:gpt-4o:b94d27b9...'\n */\n static build(\n type: CacheType,\n provider: LLMProvider | string,\n model: string,\n text: string,\n ): string {\n return `${type}:${provider}:${model}:${KeyBuilder.hash(text)}`\n }\n}\n","import type { CacheEntry } from './interfaces.js'\n\n/**\n * Centralizes time-to-live arithmetic: computing expiry timestamps, checking\n * expiry, and supporting sliding-window refresh. All durations are relative\n * milliseconds; all timestamps are absolute epoch milliseconds.\n */\nexport class TTLManager {\n /**\n * Computes the absolute expiry timestamp for an entry created at `createdAt`\n * with a relative `ttl`. Returns `undefined` when `ttl` is not a positive\n * number, meaning the entry never expires.\n */\n static computeExpiresAt(createdAt: number, ttl?: number): number | undefined {\n if (ttl === undefined || ttl <= 0) return undefined\n return createdAt + ttl\n }\n\n /**\n * Returns true when the entry has an expiry and that expiry is at or before\n * `now` (defaults to the current time).\n */\n static isExpired(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now: number = Date.now()): boolean {\n return entry.expiresAt !== undefined && entry.expiresAt <= now\n }\n\n /**\n * Milliseconds remaining until the entry expires. Returns `Infinity` for\n * entries with no expiry, and `0` for already-expired entries.\n */\n static remaining(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now: number = Date.now()): number {\n if (entry.expiresAt === undefined) return Infinity\n return Math.max(0, entry.expiresAt - now)\n }\n\n /**\n * Computes a refreshed expiry for a sliding-window TTL: extends the entry's\n * life by `ttl` from `now`. Returns `undefined` when `ttl` is not positive.\n */\n static slide(ttl?: number, now: number = Date.now()): number | undefined {\n return TTLManager.computeExpiresAt(now, ttl)\n }\n}\n","import { SerializationError } from './errors.js'\n\n/**\n * Converts values to and from `Buffer` for storage. Implementations decide the\n * wire format.\n *\n * The architecture calls for MessagePack as the primary format, but\n * `@nodellmcache/core` must stay dependency-free, so it ships only the JSON\n * implementation below. A MessagePack serializer can be supplied by an optional\n * package and injected wherever a `Serializer` is accepted.\n */\nexport interface Serializer {\n serialize<T>(value: T): Buffer\n deserialize<T>(data: Buffer): T\n}\n\n/**\n * Default JSON-backed serializer. Zero dependencies; handles the common case\n * and wraps failures in {@link SerializationError}.\n */\nexport class JsonSerializer implements Serializer {\n serialize<T>(value: T): Buffer {\n try {\n return Buffer.from(JSON.stringify(value), 'utf8')\n } catch (cause) {\n throw new SerializationError('Failed to serialize value to JSON', { cause })\n }\n }\n\n deserialize<T>(data: Buffer): T {\n try {\n return JSON.parse(data.toString('utf8')) as T\n } catch (cause) {\n throw new SerializationError('Failed to deserialize JSON value', { cause })\n }\n }\n}\n","import { KeyBuilder } from './KeyBuilder.js'\nimport { TTLManager } from './TTLManager.js'\nimport type {\n CacheEntry,\n CacheOptions,\n CacheStats,\n MetricsSink,\n StorageAdapter,\n} from './interfaces.js'\nimport type { CacheType } from './types.js'\n\n/** A metrics sink that discards everything; the default when none is injected. */\nexport const noopMetrics: MetricsSink = {\n emit() {\n // intentionally empty\n },\n}\n\n/**\n * Construction options shared by all cache managers.\n */\nexport interface BaseCacheManagerOptions<T> {\n adapter: StorageAdapter<T>\n /** Default relative TTL in milliseconds applied to entries lacking their own. */\n defaultTTL?: number\n /** Metrics sink; defaults to a no-op so core stays dependency-free. */\n metrics?: MetricsSink\n}\n\n/**\n * Shared base for every feature cache (prompt, embedding, semantic, ...).\n *\n * Provides the cache-aside `getOrGenerate` flow, key building, entry\n * construction, invalidation, and hit/miss accounting. Subclasses declare their\n * {@link CacheType} and may override {@link buildKey} for custom namespacing.\n */\nexport abstract class BaseCacheManager<T> {\n /** The workload category for keys and metrics. */\n protected abstract readonly cacheType: CacheType\n\n protected readonly adapter: StorageAdapter<T>\n protected readonly defaultTTL: number | undefined\n protected readonly metrics: MetricsSink\n\n private hits = 0\n private misses = 0\n /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */\n private readonly refreshing = new Set<string>()\n /** In-flight generations, so concurrent misses for the same key run the generator once. */\n private readonly inflight = new Map<string, Promise<T>>()\n\n constructor(options: BaseCacheManagerOptions<T>) {\n this.adapter = options.adapter\n this.defaultTTL = options.defaultTTL\n this.metrics = options.metrics ?? noopMetrics\n }\n\n /**\n * Builds the storage key for an input. Defaults to the canonical\n * `{type}:{provider}:{model}:{hash}` format via {@link KeyBuilder}.\n */\n protected buildKey(input: string, options?: CacheOptions): string {\n return KeyBuilder.build(\n this.cacheType,\n options?.provider ?? 'unknown',\n options?.model ?? 'default',\n input,\n )\n }\n\n /**\n * Wraps a value in a {@link CacheEntry} with computed expiry and metadata.\n */\n protected buildEntry(key: string, value: T, options?: CacheOptions): CacheEntry<T> {\n const createdAt = Date.now()\n const ttl = options?.ttl ?? this.defaultTTL\n const staleTtl = options?.staleTtl\n return {\n key,\n value,\n createdAt,\n expiresAt: TTLManager.computeExpiresAt(createdAt, ttl),\n metadata: {\n compressed: false,\n originalSize: 0,\n cacheType: this.cacheType,\n provider: options?.provider,\n model: options?.model,\n tokenCount: options?.tokenCount,\n staleAt:\n staleTtl !== undefined && staleTtl > 0 ? createdAt + staleTtl : undefined,\n },\n }\n }\n\n /**\n * Cache-aside read-through. Returns the cached value on a hit, otherwise\n * invokes `generator`, stores the result, and returns it. Set\n * `options.cache = false` to bypass the cache for both read and write.\n */\n async getOrGenerate(\n input: string,\n generator: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n // A deliberate bypass is neither a hit nor a miss — skip the cache and\n // metrics entirely so accounting reflects only real cache consultations.\n if (options?.cache === false) {\n return generator()\n }\n\n const key = this.buildKey(input, options)\n const start = Date.now()\n\n const cached = await this.adapter.get(key)\n if (cached && !TTLManager.isExpired(cached)) {\n this.hits++\n this.metrics.emit('cache.hit', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n tokensSaved: cached.metadata.tokenCount,\n provider: options?.provider,\n model: options?.model,\n })\n return cached.value\n }\n\n this.misses++\n this.metrics.emit('cache.miss', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n\n return this.coalescedGenerate(key, generator, options, start)\n }\n\n /**\n * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a\n * cached entry is *stale* (past its `staleTtl`/`staleAt` but not yet expired)\n * it is returned immediately and a fresh value is fetched in the background.\n * Concurrent stale hits for the same key trigger at most one background\n * refresh. A failed background refresh is swallowed and the stale value stands\n * until it fully expires.\n */\n async getOrRevalidate(\n input: string,\n generator: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n if (options?.cache === false) {\n return generator()\n }\n\n const key = this.buildKey(input, options)\n const start = Date.now()\n\n const cached = await this.adapter.get(key)\n if (cached && !TTLManager.isExpired(cached)) {\n this.hits++\n this.metrics.emit('cache.hit', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n tokensSaved: cached.metadata.tokenCount,\n provider: options?.provider,\n model: options?.model,\n })\n const staleAt = cached.metadata.staleAt\n if (staleAt !== undefined && staleAt <= Date.now()) {\n this.revalidate(key, generator, options)\n }\n return cached.value\n }\n\n this.misses++\n this.metrics.emit('cache.miss', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n\n return this.coalescedGenerate(key, generator, options, start)\n }\n\n /**\n * Removes a single entry by its input (and namespacing options).\n */\n async invalidate(input: string, options?: CacheOptions): Promise<void> {\n await this.adapter.delete(this.buildKey(input, options))\n }\n\n /**\n * Returns hit/miss accounting for this manager plus the adapter's entry count.\n */\n async stats(): Promise<CacheStats> {\n const total = this.hits + this.misses\n const adapterStats = await this.adapter.stats()\n return {\n hits: this.hits,\n misses: this.misses,\n hitRate: total === 0 ? 0 : this.hits / total,\n entryCount: adapterStats.entryCount,\n }\n }\n\n /**\n * Runs the generator and persists the result, deduplicating concurrent calls\n * for the same key (singleflight): while one generation is in flight, other\n * callers join it instead of invoking the generator again. Prevents a\n * thundering herd of identical API calls on a cold key.\n */\n private coalescedGenerate(\n key: string,\n generator: () => Promise<T>,\n options: CacheOptions | undefined,\n start: number,\n ): Promise<T> {\n const existing = this.inflight.get(key)\n if (existing) return existing\n\n const promise = (async () => {\n const value = await generator()\n await this.persist(key, value, options, start)\n return value\n })()\n this.inflight.set(key, promise)\n return promise.finally(() => this.inflight.delete(key))\n }\n\n /** Builds an entry, writes it through the adapter, and emits `cache.set`. */\n private async persist(\n key: string,\n value: T,\n options: CacheOptions | undefined,\n start: number,\n ): Promise<void> {\n const ttl = options?.ttl ?? this.defaultTTL\n const entry = this.buildEntry(key, value, options)\n await this.adapter.set(key, entry, ttl)\n this.metrics.emit('cache.set', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n }\n\n /** Fire-and-forget background refresh for a stale entry, coalesced per key. */\n private revalidate(key: string, generator: () => Promise<T>, options?: CacheOptions): void {\n if (this.refreshing.has(key)) return\n this.refreshing.add(key)\n void (async () => {\n try {\n const value = await generator()\n await this.persist(key, value, options, Date.now())\n } catch {\n // Swallow background-refresh failures: the stale value stands until expiry.\n } finally {\n this.refreshing.delete(key)\n }\n })()\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -301,6 +301,8 @@ declare abstract class BaseCacheManager<T> {
301
301
  private misses;
302
302
  /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */
303
303
  private readonly refreshing;
304
+ /** In-flight generations, so concurrent misses for the same key run the generator once. */
305
+ private readonly inflight;
304
306
  constructor(options: BaseCacheManagerOptions<T>);
305
307
  /**
306
308
  * Builds the storage key for an input. Defaults to the canonical
@@ -334,6 +336,13 @@ declare abstract class BaseCacheManager<T> {
334
336
  * Returns hit/miss accounting for this manager plus the adapter's entry count.
335
337
  */
336
338
  stats(): Promise<CacheStats>;
339
+ /**
340
+ * Runs the generator and persists the result, deduplicating concurrent calls
341
+ * for the same key (singleflight): while one generation is in flight, other
342
+ * callers join it instead of invoking the generator again. Prevents a
343
+ * thundering herd of identical API calls on a cold key.
344
+ */
345
+ private coalescedGenerate;
337
346
  /** Builds an entry, writes it through the adapter, and emits `cache.set`. */
338
347
  private persist;
339
348
  /** Fire-and-forget background refresh for a stale entry, coalesced per key. */
package/dist/index.d.ts CHANGED
@@ -301,6 +301,8 @@ declare abstract class BaseCacheManager<T> {
301
301
  private misses;
302
302
  /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */
303
303
  private readonly refreshing;
304
+ /** In-flight generations, so concurrent misses for the same key run the generator once. */
305
+ private readonly inflight;
304
306
  constructor(options: BaseCacheManagerOptions<T>);
305
307
  /**
306
308
  * Builds the storage key for an input. Defaults to the canonical
@@ -334,6 +336,13 @@ declare abstract class BaseCacheManager<T> {
334
336
  * Returns hit/miss accounting for this manager plus the adapter's entry count.
335
337
  */
336
338
  stats(): Promise<CacheStats>;
339
+ /**
340
+ * Runs the generator and persists the result, deduplicating concurrent calls
341
+ * for the same key (singleflight): while one generation is in flight, other
342
+ * callers join it instead of invoking the generator again. Prevents a
343
+ * thundering herd of identical API calls on a cold key.
344
+ */
345
+ private coalescedGenerate;
337
346
  /** Builds an entry, writes it through the adapter, and emits `cache.set`. */
338
347
  private persist;
339
348
  /** Fire-and-forget background refresh for a stale entry, coalesced per key. */
package/dist/index.mjs CHANGED
@@ -110,6 +110,8 @@ var BaseCacheManager = class {
110
110
  misses = 0;
111
111
  /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */
112
112
  refreshing = /* @__PURE__ */ new Set();
113
+ /** In-flight generations, so concurrent misses for the same key run the generator once. */
114
+ inflight = /* @__PURE__ */ new Map();
113
115
  constructor(options) {
114
116
  this.adapter = options.adapter;
115
117
  this.defaultTTL = options.defaultTTL;
@@ -180,9 +182,7 @@ var BaseCacheManager = class {
180
182
  provider: options?.provider,
181
183
  model: options?.model
182
184
  });
183
- const value = await generator();
184
- await this.persist(key, value, options, start);
185
- return value;
185
+ return this.coalescedGenerate(key, generator, options, start);
186
186
  }
187
187
  /**
188
188
  * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a
@@ -221,9 +221,7 @@ var BaseCacheManager = class {
221
221
  provider: options?.provider,
222
222
  model: options?.model
223
223
  });
224
- const value = await generator();
225
- await this.persist(key, value, options, start);
226
- return value;
224
+ return this.coalescedGenerate(key, generator, options, start);
227
225
  }
228
226
  /**
229
227
  * Removes a single entry by its input (and namespacing options).
@@ -244,6 +242,23 @@ var BaseCacheManager = class {
244
242
  entryCount: adapterStats.entryCount
245
243
  };
246
244
  }
245
+ /**
246
+ * Runs the generator and persists the result, deduplicating concurrent calls
247
+ * for the same key (singleflight): while one generation is in flight, other
248
+ * callers join it instead of invoking the generator again. Prevents a
249
+ * thundering herd of identical API calls on a cold key.
250
+ */
251
+ coalescedGenerate(key, generator, options, start) {
252
+ const existing = this.inflight.get(key);
253
+ if (existing) return existing;
254
+ const promise = (async () => {
255
+ const value = await generator();
256
+ await this.persist(key, value, options, start);
257
+ return value;
258
+ })();
259
+ this.inflight.set(key, promise);
260
+ return promise.finally(() => this.inflight.delete(key));
261
+ }
247
262
  /** Builds an entry, writes it through the adapter, and emits `cache.set`. */
248
263
  async persist(key, value, options, start) {
249
264
  const ttl = options?.ttl ?? this.defaultTTL;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/KeyBuilder.ts","../src/TTLManager.ts","../src/Serializer.ts","../src/BaseCacheManager.ts"],"sourcesContent":["/**\n * Base class for every error thrown by NodeLLMCache packages. Catching this\n * catches anything the library throws intentionally.\n */\nexport class NodeLLMCacheError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = new.target.name\n // Restore prototype chain for instanceof across the ES5 transpile boundary.\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\n/** Thrown when a storage adapter operation fails. */\nexport class CacheAdapterError extends NodeLLMCacheError {}\n\n/** Thrown when compression or decompression fails. */\nexport class CompressionError extends NodeLLMCacheError {}\n\n/** Thrown when serialization or deserialization fails. */\nexport class SerializationError extends NodeLLMCacheError {}\n\n/** Thrown when a value fails validation (e.g. malformed configuration). */\nexport class ValidationError extends NodeLLMCacheError {}\n","import { createHash } from 'node:crypto'\nimport type { CacheType, LLMProvider } from './types.js'\n\n/**\n * Builds deterministic, collision-resistant cache keys.\n *\n * Format: `{type}:{provider}:{model}:{sha256-hash}`\n *\n * The raw input text is normalized and hashed with SHA-256 — keys never\n * contain raw prompt text, which keeps sensitive content out of storage keys\n * and logs.\n */\nexport class KeyBuilder {\n /**\n * Normalizes text before hashing so trivially different inputs collapse to\n * the same key: trims surrounding whitespace, lowercases, and collapses any\n * run of whitespace to a single space.\n */\n static normalize(text: string): string {\n return text.trim().toLowerCase().replace(/\\s+/g, ' ')\n }\n\n /**\n * Produces the SHA-256 hex digest of the normalized text.\n */\n static hash(text: string): string {\n return createHash('sha256').update(KeyBuilder.normalize(text)).digest('hex')\n }\n\n /**\n * Builds a fully namespaced cache key.\n *\n * @example\n * KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'hello world')\n * // 'prompt:openai:gpt-4o:b94d27b9...'\n */\n static build(\n type: CacheType,\n provider: LLMProvider | string,\n model: string,\n text: string,\n ): string {\n return `${type}:${provider}:${model}:${KeyBuilder.hash(text)}`\n }\n}\n","import type { CacheEntry } from './interfaces.js'\n\n/**\n * Centralizes time-to-live arithmetic: computing expiry timestamps, checking\n * expiry, and supporting sliding-window refresh. All durations are relative\n * milliseconds; all timestamps are absolute epoch milliseconds.\n */\nexport class TTLManager {\n /**\n * Computes the absolute expiry timestamp for an entry created at `createdAt`\n * with a relative `ttl`. Returns `undefined` when `ttl` is not a positive\n * number, meaning the entry never expires.\n */\n static computeExpiresAt(createdAt: number, ttl?: number): number | undefined {\n if (ttl === undefined || ttl <= 0) return undefined\n return createdAt + ttl\n }\n\n /**\n * Returns true when the entry has an expiry and that expiry is at or before\n * `now` (defaults to the current time).\n */\n static isExpired(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now: number = Date.now()): boolean {\n return entry.expiresAt !== undefined && entry.expiresAt <= now\n }\n\n /**\n * Milliseconds remaining until the entry expires. Returns `Infinity` for\n * entries with no expiry, and `0` for already-expired entries.\n */\n static remaining(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now: number = Date.now()): number {\n if (entry.expiresAt === undefined) return Infinity\n return Math.max(0, entry.expiresAt - now)\n }\n\n /**\n * Computes a refreshed expiry for a sliding-window TTL: extends the entry's\n * life by `ttl` from `now`. Returns `undefined` when `ttl` is not positive.\n */\n static slide(ttl?: number, now: number = Date.now()): number | undefined {\n return TTLManager.computeExpiresAt(now, ttl)\n }\n}\n","import { SerializationError } from './errors.js'\n\n/**\n * Converts values to and from `Buffer` for storage. Implementations decide the\n * wire format.\n *\n * The architecture calls for MessagePack as the primary format, but\n * `@nodellmcache/core` must stay dependency-free, so it ships only the JSON\n * implementation below. A MessagePack serializer can be supplied by an optional\n * package and injected wherever a `Serializer` is accepted.\n */\nexport interface Serializer {\n serialize<T>(value: T): Buffer\n deserialize<T>(data: Buffer): T\n}\n\n/**\n * Default JSON-backed serializer. Zero dependencies; handles the common case\n * and wraps failures in {@link SerializationError}.\n */\nexport class JsonSerializer implements Serializer {\n serialize<T>(value: T): Buffer {\n try {\n return Buffer.from(JSON.stringify(value), 'utf8')\n } catch (cause) {\n throw new SerializationError('Failed to serialize value to JSON', { cause })\n }\n }\n\n deserialize<T>(data: Buffer): T {\n try {\n return JSON.parse(data.toString('utf8')) as T\n } catch (cause) {\n throw new SerializationError('Failed to deserialize JSON value', { cause })\n }\n }\n}\n","import { KeyBuilder } from './KeyBuilder.js'\nimport { TTLManager } from './TTLManager.js'\nimport type {\n CacheEntry,\n CacheOptions,\n CacheStats,\n MetricsSink,\n StorageAdapter,\n} from './interfaces.js'\nimport type { CacheType } from './types.js'\n\n/** A metrics sink that discards everything; the default when none is injected. */\nexport const noopMetrics: MetricsSink = {\n emit() {\n // intentionally empty\n },\n}\n\n/**\n * Construction options shared by all cache managers.\n */\nexport interface BaseCacheManagerOptions<T> {\n adapter: StorageAdapter<T>\n /** Default relative TTL in milliseconds applied to entries lacking their own. */\n defaultTTL?: number\n /** Metrics sink; defaults to a no-op so core stays dependency-free. */\n metrics?: MetricsSink\n}\n\n/**\n * Shared base for every feature cache (prompt, embedding, semantic, ...).\n *\n * Provides the cache-aside `getOrGenerate` flow, key building, entry\n * construction, invalidation, and hit/miss accounting. Subclasses declare their\n * {@link CacheType} and may override {@link buildKey} for custom namespacing.\n */\nexport abstract class BaseCacheManager<T> {\n /** The workload category for keys and metrics. */\n protected abstract readonly cacheType: CacheType\n\n protected readonly adapter: StorageAdapter<T>\n protected readonly defaultTTL: number | undefined\n protected readonly metrics: MetricsSink\n\n private hits = 0\n private misses = 0\n /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */\n private readonly refreshing = new Set<string>()\n\n constructor(options: BaseCacheManagerOptions<T>) {\n this.adapter = options.adapter\n this.defaultTTL = options.defaultTTL\n this.metrics = options.metrics ?? noopMetrics\n }\n\n /**\n * Builds the storage key for an input. Defaults to the canonical\n * `{type}:{provider}:{model}:{hash}` format via {@link KeyBuilder}.\n */\n protected buildKey(input: string, options?: CacheOptions): string {\n return KeyBuilder.build(\n this.cacheType,\n options?.provider ?? 'unknown',\n options?.model ?? 'default',\n input,\n )\n }\n\n /**\n * Wraps a value in a {@link CacheEntry} with computed expiry and metadata.\n */\n protected buildEntry(key: string, value: T, options?: CacheOptions): CacheEntry<T> {\n const createdAt = Date.now()\n const ttl = options?.ttl ?? this.defaultTTL\n const staleTtl = options?.staleTtl\n return {\n key,\n value,\n createdAt,\n expiresAt: TTLManager.computeExpiresAt(createdAt, ttl),\n metadata: {\n compressed: false,\n originalSize: 0,\n cacheType: this.cacheType,\n provider: options?.provider,\n model: options?.model,\n tokenCount: options?.tokenCount,\n staleAt:\n staleTtl !== undefined && staleTtl > 0 ? createdAt + staleTtl : undefined,\n },\n }\n }\n\n /**\n * Cache-aside read-through. Returns the cached value on a hit, otherwise\n * invokes `generator`, stores the result, and returns it. Set\n * `options.cache = false` to bypass the cache for both read and write.\n */\n async getOrGenerate(\n input: string,\n generator: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n // A deliberate bypass is neither a hit nor a miss — skip the cache and\n // metrics entirely so accounting reflects only real cache consultations.\n if (options?.cache === false) {\n return generator()\n }\n\n const key = this.buildKey(input, options)\n const start = Date.now()\n\n const cached = await this.adapter.get(key)\n if (cached && !TTLManager.isExpired(cached)) {\n this.hits++\n this.metrics.emit('cache.hit', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n tokensSaved: cached.metadata.tokenCount,\n provider: options?.provider,\n model: options?.model,\n })\n return cached.value\n }\n\n this.misses++\n this.metrics.emit('cache.miss', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n\n const value = await generator()\n await this.persist(key, value, options, start)\n return value\n }\n\n /**\n * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a\n * cached entry is *stale* (past its `staleTtl`/`staleAt` but not yet expired)\n * it is returned immediately and a fresh value is fetched in the background.\n * Concurrent stale hits for the same key trigger at most one background\n * refresh. A failed background refresh is swallowed and the stale value stands\n * until it fully expires.\n */\n async getOrRevalidate(\n input: string,\n generator: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n if (options?.cache === false) {\n return generator()\n }\n\n const key = this.buildKey(input, options)\n const start = Date.now()\n\n const cached = await this.adapter.get(key)\n if (cached && !TTLManager.isExpired(cached)) {\n this.hits++\n this.metrics.emit('cache.hit', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n tokensSaved: cached.metadata.tokenCount,\n provider: options?.provider,\n model: options?.model,\n })\n const staleAt = cached.metadata.staleAt\n if (staleAt !== undefined && staleAt <= Date.now()) {\n this.revalidate(key, generator, options)\n }\n return cached.value\n }\n\n this.misses++\n this.metrics.emit('cache.miss', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n\n const value = await generator()\n await this.persist(key, value, options, start)\n return value\n }\n\n /**\n * Removes a single entry by its input (and namespacing options).\n */\n async invalidate(input: string, options?: CacheOptions): Promise<void> {\n await this.adapter.delete(this.buildKey(input, options))\n }\n\n /**\n * Returns hit/miss accounting for this manager plus the adapter's entry count.\n */\n async stats(): Promise<CacheStats> {\n const total = this.hits + this.misses\n const adapterStats = await this.adapter.stats()\n return {\n hits: this.hits,\n misses: this.misses,\n hitRate: total === 0 ? 0 : this.hits / total,\n entryCount: adapterStats.entryCount,\n }\n }\n\n /** Builds an entry, writes it through the adapter, and emits `cache.set`. */\n private async persist(\n key: string,\n value: T,\n options: CacheOptions | undefined,\n start: number,\n ): Promise<void> {\n const ttl = options?.ttl ?? this.defaultTTL\n const entry = this.buildEntry(key, value, options)\n await this.adapter.set(key, entry, ttl)\n this.metrics.emit('cache.set', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n }\n\n /** Fire-and-forget background refresh for a stale entry, coalesced per key. */\n private revalidate(key: string, generator: () => Promise<T>, options?: CacheOptions): void {\n if (this.refreshing.has(key)) return\n this.refreshing.add(key)\n void (async () => {\n try {\n const value = await generator()\n await this.persist(key, value, options, Date.now())\n } catch {\n // Swallow background-refresh failures: the stale value stands until expiry.\n } finally {\n this.refreshing.delete(key)\n }\n })()\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/KeyBuilder.ts","../src/TTLManager.ts","../src/Serializer.ts","../src/BaseCacheManager.ts"],"sourcesContent":["/**\n * Base class for every error thrown by NodeLLMCache packages. Catching this\n * catches anything the library throws intentionally.\n */\nexport class NodeLLMCacheError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = new.target.name\n // Restore prototype chain for instanceof across the ES5 transpile boundary.\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\n/** Thrown when a storage adapter operation fails. */\nexport class CacheAdapterError extends NodeLLMCacheError {}\n\n/** Thrown when compression or decompression fails. */\nexport class CompressionError extends NodeLLMCacheError {}\n\n/** Thrown when serialization or deserialization fails. */\nexport class SerializationError extends NodeLLMCacheError {}\n\n/** Thrown when a value fails validation (e.g. malformed configuration). */\nexport class ValidationError extends NodeLLMCacheError {}\n","import { createHash } from 'node:crypto'\nimport type { CacheType, LLMProvider } from './types.js'\n\n/**\n * Builds deterministic, collision-resistant cache keys.\n *\n * Format: `{type}:{provider}:{model}:{sha256-hash}`\n *\n * The raw input text is normalized and hashed with SHA-256 — keys never\n * contain raw prompt text, which keeps sensitive content out of storage keys\n * and logs.\n */\nexport class KeyBuilder {\n /**\n * Normalizes text before hashing so trivially different inputs collapse to\n * the same key: trims surrounding whitespace, lowercases, and collapses any\n * run of whitespace to a single space.\n */\n static normalize(text: string): string {\n return text.trim().toLowerCase().replace(/\\s+/g, ' ')\n }\n\n /**\n * Produces the SHA-256 hex digest of the normalized text.\n */\n static hash(text: string): string {\n return createHash('sha256').update(KeyBuilder.normalize(text)).digest('hex')\n }\n\n /**\n * Builds a fully namespaced cache key.\n *\n * @example\n * KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'hello world')\n * // 'prompt:openai:gpt-4o:b94d27b9...'\n */\n static build(\n type: CacheType,\n provider: LLMProvider | string,\n model: string,\n text: string,\n ): string {\n return `${type}:${provider}:${model}:${KeyBuilder.hash(text)}`\n }\n}\n","import type { CacheEntry } from './interfaces.js'\n\n/**\n * Centralizes time-to-live arithmetic: computing expiry timestamps, checking\n * expiry, and supporting sliding-window refresh. All durations are relative\n * milliseconds; all timestamps are absolute epoch milliseconds.\n */\nexport class TTLManager {\n /**\n * Computes the absolute expiry timestamp for an entry created at `createdAt`\n * with a relative `ttl`. Returns `undefined` when `ttl` is not a positive\n * number, meaning the entry never expires.\n */\n static computeExpiresAt(createdAt: number, ttl?: number): number | undefined {\n if (ttl === undefined || ttl <= 0) return undefined\n return createdAt + ttl\n }\n\n /**\n * Returns true when the entry has an expiry and that expiry is at or before\n * `now` (defaults to the current time).\n */\n static isExpired(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now: number = Date.now()): boolean {\n return entry.expiresAt !== undefined && entry.expiresAt <= now\n }\n\n /**\n * Milliseconds remaining until the entry expires. Returns `Infinity` for\n * entries with no expiry, and `0` for already-expired entries.\n */\n static remaining(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now: number = Date.now()): number {\n if (entry.expiresAt === undefined) return Infinity\n return Math.max(0, entry.expiresAt - now)\n }\n\n /**\n * Computes a refreshed expiry for a sliding-window TTL: extends the entry's\n * life by `ttl` from `now`. Returns `undefined` when `ttl` is not positive.\n */\n static slide(ttl?: number, now: number = Date.now()): number | undefined {\n return TTLManager.computeExpiresAt(now, ttl)\n }\n}\n","import { SerializationError } from './errors.js'\n\n/**\n * Converts values to and from `Buffer` for storage. Implementations decide the\n * wire format.\n *\n * The architecture calls for MessagePack as the primary format, but\n * `@nodellmcache/core` must stay dependency-free, so it ships only the JSON\n * implementation below. A MessagePack serializer can be supplied by an optional\n * package and injected wherever a `Serializer` is accepted.\n */\nexport interface Serializer {\n serialize<T>(value: T): Buffer\n deserialize<T>(data: Buffer): T\n}\n\n/**\n * Default JSON-backed serializer. Zero dependencies; handles the common case\n * and wraps failures in {@link SerializationError}.\n */\nexport class JsonSerializer implements Serializer {\n serialize<T>(value: T): Buffer {\n try {\n return Buffer.from(JSON.stringify(value), 'utf8')\n } catch (cause) {\n throw new SerializationError('Failed to serialize value to JSON', { cause })\n }\n }\n\n deserialize<T>(data: Buffer): T {\n try {\n return JSON.parse(data.toString('utf8')) as T\n } catch (cause) {\n throw new SerializationError('Failed to deserialize JSON value', { cause })\n }\n }\n}\n","import { KeyBuilder } from './KeyBuilder.js'\nimport { TTLManager } from './TTLManager.js'\nimport type {\n CacheEntry,\n CacheOptions,\n CacheStats,\n MetricsSink,\n StorageAdapter,\n} from './interfaces.js'\nimport type { CacheType } from './types.js'\n\n/** A metrics sink that discards everything; the default when none is injected. */\nexport const noopMetrics: MetricsSink = {\n emit() {\n // intentionally empty\n },\n}\n\n/**\n * Construction options shared by all cache managers.\n */\nexport interface BaseCacheManagerOptions<T> {\n adapter: StorageAdapter<T>\n /** Default relative TTL in milliseconds applied to entries lacking their own. */\n defaultTTL?: number\n /** Metrics sink; defaults to a no-op so core stays dependency-free. */\n metrics?: MetricsSink\n}\n\n/**\n * Shared base for every feature cache (prompt, embedding, semantic, ...).\n *\n * Provides the cache-aside `getOrGenerate` flow, key building, entry\n * construction, invalidation, and hit/miss accounting. Subclasses declare their\n * {@link CacheType} and may override {@link buildKey} for custom namespacing.\n */\nexport abstract class BaseCacheManager<T> {\n /** The workload category for keys and metrics. */\n protected abstract readonly cacheType: CacheType\n\n protected readonly adapter: StorageAdapter<T>\n protected readonly defaultTTL: number | undefined\n protected readonly metrics: MetricsSink\n\n private hits = 0\n private misses = 0\n /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */\n private readonly refreshing = new Set<string>()\n /** In-flight generations, so concurrent misses for the same key run the generator once. */\n private readonly inflight = new Map<string, Promise<T>>()\n\n constructor(options: BaseCacheManagerOptions<T>) {\n this.adapter = options.adapter\n this.defaultTTL = options.defaultTTL\n this.metrics = options.metrics ?? noopMetrics\n }\n\n /**\n * Builds the storage key for an input. Defaults to the canonical\n * `{type}:{provider}:{model}:{hash}` format via {@link KeyBuilder}.\n */\n protected buildKey(input: string, options?: CacheOptions): string {\n return KeyBuilder.build(\n this.cacheType,\n options?.provider ?? 'unknown',\n options?.model ?? 'default',\n input,\n )\n }\n\n /**\n * Wraps a value in a {@link CacheEntry} with computed expiry and metadata.\n */\n protected buildEntry(key: string, value: T, options?: CacheOptions): CacheEntry<T> {\n const createdAt = Date.now()\n const ttl = options?.ttl ?? this.defaultTTL\n const staleTtl = options?.staleTtl\n return {\n key,\n value,\n createdAt,\n expiresAt: TTLManager.computeExpiresAt(createdAt, ttl),\n metadata: {\n compressed: false,\n originalSize: 0,\n cacheType: this.cacheType,\n provider: options?.provider,\n model: options?.model,\n tokenCount: options?.tokenCount,\n staleAt:\n staleTtl !== undefined && staleTtl > 0 ? createdAt + staleTtl : undefined,\n },\n }\n }\n\n /**\n * Cache-aside read-through. Returns the cached value on a hit, otherwise\n * invokes `generator`, stores the result, and returns it. Set\n * `options.cache = false` to bypass the cache for both read and write.\n */\n async getOrGenerate(\n input: string,\n generator: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n // A deliberate bypass is neither a hit nor a miss — skip the cache and\n // metrics entirely so accounting reflects only real cache consultations.\n if (options?.cache === false) {\n return generator()\n }\n\n const key = this.buildKey(input, options)\n const start = Date.now()\n\n const cached = await this.adapter.get(key)\n if (cached && !TTLManager.isExpired(cached)) {\n this.hits++\n this.metrics.emit('cache.hit', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n tokensSaved: cached.metadata.tokenCount,\n provider: options?.provider,\n model: options?.model,\n })\n return cached.value\n }\n\n this.misses++\n this.metrics.emit('cache.miss', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n\n return this.coalescedGenerate(key, generator, options, start)\n }\n\n /**\n * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a\n * cached entry is *stale* (past its `staleTtl`/`staleAt` but not yet expired)\n * it is returned immediately and a fresh value is fetched in the background.\n * Concurrent stale hits for the same key trigger at most one background\n * refresh. A failed background refresh is swallowed and the stale value stands\n * until it fully expires.\n */\n async getOrRevalidate(\n input: string,\n generator: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n if (options?.cache === false) {\n return generator()\n }\n\n const key = this.buildKey(input, options)\n const start = Date.now()\n\n const cached = await this.adapter.get(key)\n if (cached && !TTLManager.isExpired(cached)) {\n this.hits++\n this.metrics.emit('cache.hit', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n tokensSaved: cached.metadata.tokenCount,\n provider: options?.provider,\n model: options?.model,\n })\n const staleAt = cached.metadata.staleAt\n if (staleAt !== undefined && staleAt <= Date.now()) {\n this.revalidate(key, generator, options)\n }\n return cached.value\n }\n\n this.misses++\n this.metrics.emit('cache.miss', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n\n return this.coalescedGenerate(key, generator, options, start)\n }\n\n /**\n * Removes a single entry by its input (and namespacing options).\n */\n async invalidate(input: string, options?: CacheOptions): Promise<void> {\n await this.adapter.delete(this.buildKey(input, options))\n }\n\n /**\n * Returns hit/miss accounting for this manager plus the adapter's entry count.\n */\n async stats(): Promise<CacheStats> {\n const total = this.hits + this.misses\n const adapterStats = await this.adapter.stats()\n return {\n hits: this.hits,\n misses: this.misses,\n hitRate: total === 0 ? 0 : this.hits / total,\n entryCount: adapterStats.entryCount,\n }\n }\n\n /**\n * Runs the generator and persists the result, deduplicating concurrent calls\n * for the same key (singleflight): while one generation is in flight, other\n * callers join it instead of invoking the generator again. Prevents a\n * thundering herd of identical API calls on a cold key.\n */\n private coalescedGenerate(\n key: string,\n generator: () => Promise<T>,\n options: CacheOptions | undefined,\n start: number,\n ): Promise<T> {\n const existing = this.inflight.get(key)\n if (existing) return existing\n\n const promise = (async () => {\n const value = await generator()\n await this.persist(key, value, options, start)\n return value\n })()\n this.inflight.set(key, promise)\n return promise.finally(() => this.inflight.delete(key))\n }\n\n /** Builds an entry, writes it through the adapter, and emits `cache.set`. */\n private async persist(\n key: string,\n value: T,\n options: CacheOptions | undefined,\n start: number,\n ): Promise<void> {\n const ttl = options?.ttl ?? this.defaultTTL\n const entry = this.buildEntry(key, value, options)\n await this.adapter.set(key, entry, ttl)\n this.metrics.emit('cache.set', {\n cacheType: this.cacheType,\n latencyMs: Date.now() - start,\n provider: options?.provider,\n model: options?.model,\n })\n }\n\n /** Fire-and-forget background refresh for a stale entry, coalesced per key. */\n private revalidate(key: string, generator: () => Promise<T>, options?: CacheOptions): void {\n if (this.refreshing.has(key)) return\n this.refreshing.add(key)\n void (async () => {\n try {\n const value = await generator()\n await this.persist(key, value, options, Date.now())\n } catch {\n // Swallow background-refresh failures: the stale value stands until expiry.\n } finally {\n this.refreshing.delete(key)\n }\n })()\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodellmcache/core",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared interfaces, types, and utilities for NodeLLMCache — AI memory infrastructure for Node.js",
5
5
  "keywords": [
6
6
  "llm",