@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 +7 -0
- package/LICENSE +21 -0
- package/dist/index.cjs +68 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.mjs +68 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +23 -12
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 = {
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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,
|
package/dist/index.mjs.map
CHANGED
|
@@ -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":";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,EAEjB,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,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,MACvB;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;AAE9B,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;AAED,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;AACF;","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":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodellmcache/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Shared interfaces, types, and utilities for NodeLLMCache — AI memory infrastructure for Node.js",
|
|
5
|
-
"keywords": [
|
|
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": [
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
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
|
+
}
|