@nodellmcache/core 0.1.0 → 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ # @nodellmcache/core
2
+
3
+ ## 1.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a2633d8: Initial release of `@nodellmcache/core`: shared interfaces and types (`StorageAdapter`, `VectorStoreAdapter`, `CompressionEngine`, `CacheEntry`, `MetricsSink`, `CacheType`, `LLMProvider`, `CompressionAlgo`), the `KeyBuilder`, `TTLManager`, `JsonSerializer`, the `BaseCacheManager` cache-aside base class, and the typed error hierarchy. Zero external dependencies.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NodeLLMCache contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs CHANGED
@@ -143,6 +143,8 @@ var BaseCacheManager = class {
143
143
  metrics;
144
144
  hits = 0;
145
145
  misses = 0;
146
+ /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */
147
+ refreshing = /* @__PURE__ */ new Set();
146
148
  constructor(options) {
147
149
  this.adapter = options.adapter;
148
150
  this.defaultTTL = options.defaultTTL;
@@ -166,6 +168,7 @@ var BaseCacheManager = class {
166
168
  buildEntry(key, value, options) {
167
169
  const createdAt = Date.now();
168
170
  const ttl = options?.ttl ?? this.defaultTTL;
171
+ const staleTtl = options?.staleTtl;
169
172
  return {
170
173
  key,
171
174
  value,
@@ -177,7 +180,8 @@ var BaseCacheManager = class {
177
180
  cacheType: this.cacheType,
178
181
  provider: options?.provider,
179
182
  model: options?.model,
180
- tokenCount: options?.tokenCount
183
+ tokenCount: options?.tokenCount,
184
+ staleAt: staleTtl !== void 0 && staleTtl > 0 ? createdAt + staleTtl : void 0
181
185
  }
182
186
  };
183
187
  }
@@ -212,15 +216,48 @@ var BaseCacheManager = class {
212
216
  model: options?.model
213
217
  });
214
218
  const value = await generator();
215
- const ttl = options?.ttl ?? this.defaultTTL;
216
- const entry = this.buildEntry(key, value, options);
217
- await this.adapter.set(key, entry, ttl);
218
- this.metrics.emit("cache.set", {
219
+ await this.persist(key, value, options, start);
220
+ return value;
221
+ }
222
+ /**
223
+ * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a
224
+ * cached entry is *stale* (past its `staleTtl`/`staleAt` but not yet expired)
225
+ * it is returned immediately and a fresh value is fetched in the background.
226
+ * Concurrent stale hits for the same key trigger at most one background
227
+ * refresh. A failed background refresh is swallowed and the stale value stands
228
+ * until it fully expires.
229
+ */
230
+ async getOrRevalidate(input, generator, options) {
231
+ if (options?.cache === false) {
232
+ return generator();
233
+ }
234
+ const key = this.buildKey(input, options);
235
+ const start = Date.now();
236
+ const cached = await this.adapter.get(key);
237
+ if (cached && !TTLManager.isExpired(cached)) {
238
+ this.hits++;
239
+ this.metrics.emit("cache.hit", {
240
+ cacheType: this.cacheType,
241
+ latencyMs: Date.now() - start,
242
+ tokensSaved: cached.metadata.tokenCount,
243
+ provider: options?.provider,
244
+ model: options?.model
245
+ });
246
+ const staleAt = cached.metadata.staleAt;
247
+ if (staleAt !== void 0 && staleAt <= Date.now()) {
248
+ this.revalidate(key, generator, options);
249
+ }
250
+ return cached.value;
251
+ }
252
+ this.misses++;
253
+ this.metrics.emit("cache.miss", {
219
254
  cacheType: this.cacheType,
220
255
  latencyMs: Date.now() - start,
221
256
  provider: options?.provider,
222
257
  model: options?.model
223
258
  });
259
+ const value = await generator();
260
+ await this.persist(key, value, options, start);
224
261
  return value;
225
262
  }
226
263
  /**
@@ -242,6 +279,32 @@ var BaseCacheManager = class {
242
279
  entryCount: adapterStats.entryCount
243
280
  };
244
281
  }
282
+ /** Builds an entry, writes it through the adapter, and emits `cache.set`. */
283
+ async persist(key, value, options, start) {
284
+ const ttl = options?.ttl ?? this.defaultTTL;
285
+ const entry = this.buildEntry(key, value, options);
286
+ await this.adapter.set(key, entry, ttl);
287
+ this.metrics.emit("cache.set", {
288
+ cacheType: this.cacheType,
289
+ latencyMs: Date.now() - start,
290
+ provider: options?.provider,
291
+ model: options?.model
292
+ });
293
+ }
294
+ /** Fire-and-forget background refresh for a stale entry, coalesced per key. */
295
+ revalidate(key, generator, options) {
296
+ if (this.refreshing.has(key)) return;
297
+ this.refreshing.add(key);
298
+ void (async () => {
299
+ try {
300
+ const value = await generator();
301
+ await this.persist(key, value, options, Date.now());
302
+ } catch {
303
+ } finally {
304
+ this.refreshing.delete(key);
305
+ }
306
+ })();
307
+ }
245
308
  };
246
309
  // Annotate the CommonJS export names for ESM import in node:
247
310
  0 && (module.exports = {
@@ -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\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 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 },\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\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 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"],"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\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":[]}
package/dist/index.d.cts CHANGED
@@ -32,6 +32,13 @@ interface CacheMetadata {
32
32
  provider?: LLMProvider;
33
33
  model?: string;
34
34
  tokenCount?: number;
35
+ /**
36
+ * Absolute epoch ms after which the entry is considered *stale* but still
37
+ * usable. Used by stale-while-revalidate: a stale hit is served immediately
38
+ * while a fresh value is fetched in the background. Always earlier than
39
+ * `expiresAt`.
40
+ */
41
+ staleAt?: number;
35
42
  }
36
43
  /**
37
44
  * A single cached value with its bookkeeping. `expiresAt` is an absolute epoch
@@ -145,6 +152,11 @@ interface CacheOptions {
145
152
  model?: string;
146
153
  /** Relative TTL in milliseconds; overrides the manager default. */
147
154
  ttl?: number;
155
+ /**
156
+ * Relative ms after which an entry is *stale* but still servable via
157
+ * `getOrRevalidate` (stale-while-revalidate). Should be less than `ttl`.
158
+ */
159
+ staleTtl?: number;
148
160
  /** When false, bypasses the cache entirely (read and write). */
149
161
  cache?: boolean;
150
162
  tokenCount?: number;
@@ -287,6 +299,8 @@ declare abstract class BaseCacheManager<T> {
287
299
  protected readonly metrics: MetricsSink;
288
300
  private hits;
289
301
  private misses;
302
+ /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */
303
+ private readonly refreshing;
290
304
  constructor(options: BaseCacheManagerOptions<T>);
291
305
  /**
292
306
  * Builds the storage key for an input. Defaults to the canonical
@@ -303,6 +317,15 @@ declare abstract class BaseCacheManager<T> {
303
317
  * `options.cache = false` to bypass the cache for both read and write.
304
318
  */
305
319
  getOrGenerate(input: string, generator: () => Promise<T>, options?: CacheOptions): Promise<T>;
320
+ /**
321
+ * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a
322
+ * cached entry is *stale* (past its `staleTtl`/`staleAt` but not yet expired)
323
+ * it is returned immediately and a fresh value is fetched in the background.
324
+ * Concurrent stale hits for the same key trigger at most one background
325
+ * refresh. A failed background refresh is swallowed and the stale value stands
326
+ * until it fully expires.
327
+ */
328
+ getOrRevalidate(input: string, generator: () => Promise<T>, options?: CacheOptions): Promise<T>;
306
329
  /**
307
330
  * Removes a single entry by its input (and namespacing options).
308
331
  */
@@ -311,6 +334,10 @@ declare abstract class BaseCacheManager<T> {
311
334
  * Returns hit/miss accounting for this manager plus the adapter's entry count.
312
335
  */
313
336
  stats(): Promise<CacheStats>;
337
+ /** Builds an entry, writes it through the adapter, and emits `cache.set`. */
338
+ private persist;
339
+ /** Fire-and-forget background refresh for a stale entry, coalesced per key. */
340
+ private revalidate;
314
341
  }
315
342
 
316
343
  export { type AdapterStats, BaseCacheManager, type BaseCacheManagerOptions, CacheAdapterError, type CacheEntry, type CacheMetadata, type CacheOptions, type CacheStats, type CacheType, type CompressedResult, type CompressionAlgo, type CompressionEngine, CompressionError, type CompressionStats, type DataHint, JsonSerializer, KeyBuilder, type LLMProvider, type MetricData, type MetricEvent, type MetricsSink, NodeLLMCacheError, SerializationError, type Serializer, type StorageAdapter, TTLManager, ValidationError, type VectorMatch, type VectorStoreAdapter, noopMetrics };
package/dist/index.d.ts CHANGED
@@ -32,6 +32,13 @@ interface CacheMetadata {
32
32
  provider?: LLMProvider;
33
33
  model?: string;
34
34
  tokenCount?: number;
35
+ /**
36
+ * Absolute epoch ms after which the entry is considered *stale* but still
37
+ * usable. Used by stale-while-revalidate: a stale hit is served immediately
38
+ * while a fresh value is fetched in the background. Always earlier than
39
+ * `expiresAt`.
40
+ */
41
+ staleAt?: number;
35
42
  }
36
43
  /**
37
44
  * A single cached value with its bookkeeping. `expiresAt` is an absolute epoch
@@ -145,6 +152,11 @@ interface CacheOptions {
145
152
  model?: string;
146
153
  /** Relative TTL in milliseconds; overrides the manager default. */
147
154
  ttl?: number;
155
+ /**
156
+ * Relative ms after which an entry is *stale* but still servable via
157
+ * `getOrRevalidate` (stale-while-revalidate). Should be less than `ttl`.
158
+ */
159
+ staleTtl?: number;
148
160
  /** When false, bypasses the cache entirely (read and write). */
149
161
  cache?: boolean;
150
162
  tokenCount?: number;
@@ -287,6 +299,8 @@ declare abstract class BaseCacheManager<T> {
287
299
  protected readonly metrics: MetricsSink;
288
300
  private hits;
289
301
  private misses;
302
+ /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */
303
+ private readonly refreshing;
290
304
  constructor(options: BaseCacheManagerOptions<T>);
291
305
  /**
292
306
  * Builds the storage key for an input. Defaults to the canonical
@@ -303,6 +317,15 @@ declare abstract class BaseCacheManager<T> {
303
317
  * `options.cache = false` to bypass the cache for both read and write.
304
318
  */
305
319
  getOrGenerate(input: string, generator: () => Promise<T>, options?: CacheOptions): Promise<T>;
320
+ /**
321
+ * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a
322
+ * cached entry is *stale* (past its `staleTtl`/`staleAt` but not yet expired)
323
+ * it is returned immediately and a fresh value is fetched in the background.
324
+ * Concurrent stale hits for the same key trigger at most one background
325
+ * refresh. A failed background refresh is swallowed and the stale value stands
326
+ * until it fully expires.
327
+ */
328
+ getOrRevalidate(input: string, generator: () => Promise<T>, options?: CacheOptions): Promise<T>;
306
329
  /**
307
330
  * Removes a single entry by its input (and namespacing options).
308
331
  */
@@ -311,6 +334,10 @@ declare abstract class BaseCacheManager<T> {
311
334
  * Returns hit/miss accounting for this manager plus the adapter's entry count.
312
335
  */
313
336
  stats(): Promise<CacheStats>;
337
+ /** Builds an entry, writes it through the adapter, and emits `cache.set`. */
338
+ private persist;
339
+ /** Fire-and-forget background refresh for a stale entry, coalesced per key. */
340
+ private revalidate;
314
341
  }
315
342
 
316
343
  export { type AdapterStats, BaseCacheManager, type BaseCacheManagerOptions, CacheAdapterError, type CacheEntry, type CacheMetadata, type CacheOptions, type CacheStats, type CacheType, type CompressedResult, type CompressionAlgo, type CompressionEngine, CompressionError, type CompressionStats, type DataHint, JsonSerializer, KeyBuilder, type LLMProvider, type MetricData, type MetricEvent, type MetricsSink, NodeLLMCacheError, SerializationError, type Serializer, type StorageAdapter, TTLManager, ValidationError, type VectorMatch, type VectorStoreAdapter, noopMetrics };
package/dist/index.mjs CHANGED
@@ -108,6 +108,8 @@ var BaseCacheManager = class {
108
108
  metrics;
109
109
  hits = 0;
110
110
  misses = 0;
111
+ /** Keys with an in-flight background revalidation, to avoid duplicate refreshes. */
112
+ refreshing = /* @__PURE__ */ new Set();
111
113
  constructor(options) {
112
114
  this.adapter = options.adapter;
113
115
  this.defaultTTL = options.defaultTTL;
@@ -131,6 +133,7 @@ var BaseCacheManager = class {
131
133
  buildEntry(key, value, options) {
132
134
  const createdAt = Date.now();
133
135
  const ttl = options?.ttl ?? this.defaultTTL;
136
+ const staleTtl = options?.staleTtl;
134
137
  return {
135
138
  key,
136
139
  value,
@@ -142,7 +145,8 @@ var BaseCacheManager = class {
142
145
  cacheType: this.cacheType,
143
146
  provider: options?.provider,
144
147
  model: options?.model,
145
- tokenCount: options?.tokenCount
148
+ tokenCount: options?.tokenCount,
149
+ staleAt: staleTtl !== void 0 && staleTtl > 0 ? createdAt + staleTtl : void 0
146
150
  }
147
151
  };
148
152
  }
@@ -177,15 +181,48 @@ var BaseCacheManager = class {
177
181
  model: options?.model
178
182
  });
179
183
  const value = await generator();
180
- const ttl = options?.ttl ?? this.defaultTTL;
181
- const entry = this.buildEntry(key, value, options);
182
- await this.adapter.set(key, entry, ttl);
183
- this.metrics.emit("cache.set", {
184
+ await this.persist(key, value, options, start);
185
+ return value;
186
+ }
187
+ /**
188
+ * Stale-while-revalidate read-through. Like {@link getOrGenerate}, but when a
189
+ * cached entry is *stale* (past its `staleTtl`/`staleAt` but not yet expired)
190
+ * it is returned immediately and a fresh value is fetched in the background.
191
+ * Concurrent stale hits for the same key trigger at most one background
192
+ * refresh. A failed background refresh is swallowed and the stale value stands
193
+ * until it fully expires.
194
+ */
195
+ async getOrRevalidate(input, generator, options) {
196
+ if (options?.cache === false) {
197
+ return generator();
198
+ }
199
+ const key = this.buildKey(input, options);
200
+ const start = Date.now();
201
+ const cached = await this.adapter.get(key);
202
+ if (cached && !TTLManager.isExpired(cached)) {
203
+ this.hits++;
204
+ this.metrics.emit("cache.hit", {
205
+ cacheType: this.cacheType,
206
+ latencyMs: Date.now() - start,
207
+ tokensSaved: cached.metadata.tokenCount,
208
+ provider: options?.provider,
209
+ model: options?.model
210
+ });
211
+ const staleAt = cached.metadata.staleAt;
212
+ if (staleAt !== void 0 && staleAt <= Date.now()) {
213
+ this.revalidate(key, generator, options);
214
+ }
215
+ return cached.value;
216
+ }
217
+ this.misses++;
218
+ this.metrics.emit("cache.miss", {
184
219
  cacheType: this.cacheType,
185
220
  latencyMs: Date.now() - start,
186
221
  provider: options?.provider,
187
222
  model: options?.model
188
223
  });
224
+ const value = await generator();
225
+ await this.persist(key, value, options, start);
189
226
  return value;
190
227
  }
191
228
  /**
@@ -207,6 +244,32 @@ var BaseCacheManager = class {
207
244
  entryCount: adapterStats.entryCount
208
245
  };
209
246
  }
247
+ /** Builds an entry, writes it through the adapter, and emits `cache.set`. */
248
+ async persist(key, value, options, start) {
249
+ const ttl = options?.ttl ?? this.defaultTTL;
250
+ const entry = this.buildEntry(key, value, options);
251
+ await this.adapter.set(key, entry, ttl);
252
+ this.metrics.emit("cache.set", {
253
+ cacheType: this.cacheType,
254
+ latencyMs: Date.now() - start,
255
+ provider: options?.provider,
256
+ model: options?.model
257
+ });
258
+ }
259
+ /** Fire-and-forget background refresh for a stale entry, coalesced per key. */
260
+ revalidate(key, generator, options) {
261
+ if (this.refreshing.has(key)) return;
262
+ this.refreshing.add(key);
263
+ void (async () => {
264
+ try {
265
+ const value = await generator();
266
+ await this.persist(key, value, options, Date.now());
267
+ } catch {
268
+ } finally {
269
+ this.refreshing.delete(key);
270
+ }
271
+ })();
272
+ }
210
273
  };
211
274
  export {
212
275
  BaseCacheManager,
@@ -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\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 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 },\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\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 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"],"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\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":";AAIO,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YAAY,SAAiB,SAAwB;AACnD,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO,WAAW;AAEvB,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AACF;AAGO,IAAM,oBAAN,cAAgC,kBAAkB;AAAC;AAGnD,IAAM,mBAAN,cAA+B,kBAAkB;AAAC;AAGlD,IAAM,qBAAN,cAAiC,kBAAkB;AAAC;AAGpD,IAAM,kBAAN,cAA8B,kBAAkB;AAAC;;;ACvBxD,SAAS,kBAAkB;AAYpB,IAAM,aAAN,MAAM,YAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtB,OAAO,UAAU,MAAsB;AACrC,WAAO,KAAK,KAAK,EAAE,YAAY,EAAE,QAAQ,QAAQ,GAAG;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,KAAK,MAAsB;AAChC,WAAO,WAAW,QAAQ,EAAE,OAAO,YAAW,UAAU,IAAI,CAAC,EAAE,OAAO,KAAK;AAAA,EAC7E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAO,MACL,MACA,UACA,OACA,MACQ;AACR,WAAO,GAAG,IAAI,IAAI,QAAQ,IAAI,KAAK,IAAI,YAAW,KAAK,IAAI,CAAC;AAAA,EAC9D;AACF;;;ACrCO,IAAM,aAAN,MAAM,YAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtB,OAAO,iBAAiB,WAAmB,KAAkC;AAC3E,QAAI,QAAQ,UAAa,OAAO,EAAG,QAAO;AAC1C,WAAO,YAAY;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,UAAU,OAA+C,MAAc,KAAK,IAAI,GAAY;AACjG,WAAO,MAAM,cAAc,UAAa,MAAM,aAAa;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,UAAU,OAA+C,MAAc,KAAK,IAAI,GAAW;AAChG,QAAI,MAAM,cAAc,OAAW,QAAO;AAC1C,WAAO,KAAK,IAAI,GAAG,MAAM,YAAY,GAAG;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,MAAM,KAAc,MAAc,KAAK,IAAI,GAAuB;AACvE,WAAO,YAAW,iBAAiB,KAAK,GAAG;AAAA,EAC7C;AACF;;;ACtBO,IAAM,iBAAN,MAA2C;AAAA,EAChD,UAAa,OAAkB;AAC7B,QAAI;AACF,aAAO,OAAO,KAAK,KAAK,UAAU,KAAK,GAAG,MAAM;AAAA,IAClD,SAAS,OAAO;AACd,YAAM,IAAI,mBAAmB,qCAAqC,EAAE,MAAM,CAAC;AAAA,IAC7E;AAAA,EACF;AAAA,EAEA,YAAe,MAAiB;AAC9B,QAAI;AACF,aAAO,KAAK,MAAM,KAAK,SAAS,MAAM,CAAC;AAAA,IACzC,SAAS,OAAO;AACd,YAAM,IAAI,mBAAmB,oCAAoC,EAAE,MAAM,CAAC;AAAA,IAC5E;AAAA,EACF;AACF;;;ACxBO,IAAM,cAA2B;AAAA,EACtC,OAAO;AAAA,EAEP;AACF;AAoBO,IAAe,mBAAf,MAAmC;AAAA,EAIrB;AAAA,EACA;AAAA,EACA;AAAA,EAEX,OAAO;AAAA,EACP,SAAS;AAAA;AAAA,EAEA,aAAa,oBAAI,IAAY;AAAA,EAE9C,YAAY,SAAqC;AAC/C,SAAK,UAAU,QAAQ;AACvB,SAAK,aAAa,QAAQ;AAC1B,SAAK,UAAU,QAAQ,WAAW;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMU,SAAS,OAAe,SAAgC;AAChE,WAAO,WAAW;AAAA,MAChB,KAAK;AAAA,MACL,SAAS,YAAY;AAAA,MACrB,SAAS,SAAS;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKU,WAAW,KAAa,OAAU,SAAuC;AACjF,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,MAAM,SAAS,OAAO,KAAK;AACjC,UAAM,WAAW,SAAS;AAC1B,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,WAAW,iBAAiB,WAAW,GAAG;AAAA,MACrD,UAAU;AAAA,QACR,YAAY;AAAA,QACZ,cAAc;AAAA,QACd,WAAW,KAAK;AAAA,QAChB,UAAU,SAAS;AAAA,QACnB,OAAO,SAAS;AAAA,QAChB,YAAY,SAAS;AAAA,QACrB,SACE,aAAa,UAAa,WAAW,IAAI,YAAY,WAAW;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,cACJ,OACA,WACA,SACY;AAGZ,QAAI,SAAS,UAAU,OAAO;AAC5B,aAAO,UAAU;AAAA,IACnB;AAEA,UAAM,MAAM,KAAK,SAAS,OAAO,OAAO;AACxC,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,SAAS,MAAM,KAAK,QAAQ,IAAI,GAAG;AACzC,QAAI,UAAU,CAAC,WAAW,UAAU,MAAM,GAAG;AAC3C,WAAK;AACL,WAAK,QAAQ,KAAK,aAAa;AAAA,QAC7B,WAAW,KAAK;AAAA,QAChB,WAAW,KAAK,IAAI,IAAI;AAAA,QACxB,aAAa,OAAO,SAAS;AAAA,QAC7B,UAAU,SAAS;AAAA,QACnB,OAAO,SAAS;AAAA,MAClB,CAAC;AACD,aAAO,OAAO;AAAA,IAChB;AAEA,SAAK;AACL,SAAK,QAAQ,KAAK,cAAc;AAAA,MAC9B,WAAW,KAAK;AAAA,MAChB,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,UAAU,SAAS;AAAA,MACnB,OAAO,SAAS;AAAA,IAClB,CAAC;AAED,UAAM,QAAQ,MAAM,UAAU;AAC9B,UAAM,KAAK,QAAQ,KAAK,OAAO,SAAS,KAAK;AAC7C,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,gBACJ,OACA,WACA,SACY;AACZ,QAAI,SAAS,UAAU,OAAO;AAC5B,aAAO,UAAU;AAAA,IACnB;AAEA,UAAM,MAAM,KAAK,SAAS,OAAO,OAAO;AACxC,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,SAAS,MAAM,KAAK,QAAQ,IAAI,GAAG;AACzC,QAAI,UAAU,CAAC,WAAW,UAAU,MAAM,GAAG;AAC3C,WAAK;AACL,WAAK,QAAQ,KAAK,aAAa;AAAA,QAC7B,WAAW,KAAK;AAAA,QAChB,WAAW,KAAK,IAAI,IAAI;AAAA,QACxB,aAAa,OAAO,SAAS;AAAA,QAC7B,UAAU,SAAS;AAAA,QACnB,OAAO,SAAS;AAAA,MAClB,CAAC;AACD,YAAM,UAAU,OAAO,SAAS;AAChC,UAAI,YAAY,UAAa,WAAW,KAAK,IAAI,GAAG;AAClD,aAAK,WAAW,KAAK,WAAW,OAAO;AAAA,MACzC;AACA,aAAO,OAAO;AAAA,IAChB;AAEA,SAAK;AACL,SAAK,QAAQ,KAAK,cAAc;AAAA,MAC9B,WAAW,KAAK;AAAA,MAChB,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,UAAU,SAAS;AAAA,MACnB,OAAO,SAAS;AAAA,IAClB,CAAC;AAED,UAAM,QAAQ,MAAM,UAAU;AAC9B,UAAM,KAAK,QAAQ,KAAK,OAAO,SAAS,KAAK;AAC7C,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAAW,OAAe,SAAuC;AACrE,UAAM,KAAK,QAAQ,OAAO,KAAK,SAAS,OAAO,OAAO,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAA6B;AACjC,UAAM,QAAQ,KAAK,OAAO,KAAK;AAC/B,UAAM,eAAe,MAAM,KAAK,QAAQ,MAAM;AAC9C,WAAO;AAAA,MACL,MAAM,KAAK;AAAA,MACX,QAAQ,KAAK;AAAA,MACb,SAAS,UAAU,IAAI,IAAI,KAAK,OAAO;AAAA,MACvC,YAAY,aAAa;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,QACZ,KACA,OACA,SACA,OACe;AACf,UAAM,MAAM,SAAS,OAAO,KAAK;AACjC,UAAM,QAAQ,KAAK,WAAW,KAAK,OAAO,OAAO;AACjD,UAAM,KAAK,QAAQ,IAAI,KAAK,OAAO,GAAG;AACtC,SAAK,QAAQ,KAAK,aAAa;AAAA,MAC7B,WAAW,KAAK;AAAA,MAChB,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,UAAU,SAAS;AAAA,MACnB,OAAO,SAAS;AAAA,IAClB,CAAC;AAAA,EACH;AAAA;AAAA,EAGQ,WAAW,KAAa,WAA6B,SAA8B;AACzF,QAAI,KAAK,WAAW,IAAI,GAAG,EAAG;AAC9B,SAAK,WAAW,IAAI,GAAG;AACvB,UAAM,YAAY;AAChB,UAAI;AACF,cAAM,QAAQ,MAAM,UAAU;AAC9B,cAAM,KAAK,QAAQ,KAAK,OAAO,SAAS,KAAK,IAAI,CAAC;AAAA,MACpD,QAAQ;AAAA,MAER,UAAE;AACA,aAAK,WAAW,OAAO,GAAG;AAAA,MAC5B;AAAA,IACF,GAAG;AAAA,EACL;AACF;","names":[]}
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@nodellmcache/core",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Shared interfaces, types, and utilities for NodeLLMCache — AI memory infrastructure for Node.js",
5
- "keywords": ["llm", "cache", "ai", "memory", "nodejs", "embeddings"],
5
+ "keywords": [
6
+ "llm",
7
+ "cache",
8
+ "ai",
9
+ "memory",
10
+ "nodejs",
11
+ "embeddings"
12
+ ],
6
13
  "license": "MIT",
7
14
  "repository": {
8
15
  "type": "git",
@@ -22,15 +29,11 @@
22
29
  "require": "./dist/index.cjs"
23
30
  }
24
31
  },
25
- "files": ["dist", "README.md", "CHANGELOG.md"],
26
- "scripts": {
27
- "build": "tsup",
28
- "test": "vitest run",
29
- "test:watch": "vitest",
30
- "test:coverage": "vitest run --coverage",
31
- "typecheck": "tsc --noEmit",
32
- "lint": "echo \"no lint configured\""
33
- },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "CHANGELOG.md"
36
+ ],
34
37
  "devDependencies": {
35
38
  "@types/node": "^20.0.0",
36
39
  "tsup": "^8.0.0",
@@ -40,5 +43,13 @@
40
43
  },
41
44
  "publishConfig": {
42
45
  "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "test:coverage": "vitest run --coverage",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint": "echo \"no lint configured\""
43
54
  }
44
- }
55
+ }