@nodellmcache/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # @nodellmcache/core
2
+
3
+ Shared interfaces, types, and utilities for [NodeLLMCache](https://github.com/mdmax007/node-llm-cache) — AI memory infrastructure for Node.js.
4
+
5
+ This is the central package every other `@nodellmcache/*` package depends on. It has **zero external dependencies** and exports only contracts and small, pure utilities. You rarely install it directly; a feature package (e.g. `@nodellmcache/prompt-cache`) pulls it in.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @nodellmcache/core
11
+ ```
12
+
13
+ ## What's inside
14
+
15
+ - **Interfaces** — `StorageAdapter`, `VectorStoreAdapter`, `CompressionEngine`, `CacheEntry`, `CacheMetadata`, `MetricsSink`, `CacheOptions`
16
+ - **Types** — `CacheType`, `LLMProvider`, `CompressionAlgo`, `DataHint`
17
+ - **`KeyBuilder`** — deterministic, hashed cache keys
18
+ - **`TTLManager`** — expiry arithmetic and sliding windows
19
+ - **`Serializer` / `JsonSerializer`** — pluggable value encoding
20
+ - **`BaseCacheManager`** — the cache-aside base class for every feature cache
21
+ - **Error hierarchy** — `NodeLLMCacheError` and typed subclasses
22
+
23
+ ## Quick start
24
+
25
+ ### Building keys
26
+
27
+ Keys follow the format `{type}:{provider}:{model}:{sha256}`. The raw input is normalized (`trim` → `toLowerCase` → collapse whitespace) and hashed, so **raw prompt text never appears in a key**.
28
+
29
+ ```ts
30
+ import { KeyBuilder } from '@nodellmcache/core'
31
+
32
+ KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'Hello World')
33
+ // 'prompt:openai:gpt-4o:<sha256-of "hello world">'
34
+ ```
35
+
36
+ ### Implementing a storage adapter
37
+
38
+ ```ts
39
+ import type { StorageAdapter, CacheEntry, AdapterStats } from '@nodellmcache/core'
40
+
41
+ class MyAdapter<T> implements StorageAdapter<T> {
42
+ async get(key: string): Promise<CacheEntry<T> | null> { /* ... */ }
43
+ async set(key: string, entry: CacheEntry<T>, ttl?: number): Promise<void> { /* ... */ }
44
+ async delete(key: string): Promise<void> { /* ... */ }
45
+ async clear(): Promise<void> { /* ... */ }
46
+ async has(key: string): Promise<boolean> { /* ... */ }
47
+ async stats(): Promise<AdapterStats> { /* ... */ }
48
+ }
49
+ ```
50
+
51
+ ### Building a feature cache
52
+
53
+ `BaseCacheManager` provides the cache-aside `getOrGenerate` flow, key building, TTL handling, hit/miss accounting, and metric emission. Subclasses only declare their `cacheType`.
54
+
55
+ ```ts
56
+ import { BaseCacheManager } from '@nodellmcache/core'
57
+
58
+ class PromptCache extends BaseCacheManager<string> {
59
+ protected readonly cacheType = 'prompt' as const
60
+ }
61
+
62
+ const cache = new PromptCache({ adapter: myAdapter, defaultTTL: 3_600_000 })
63
+
64
+ const answer = await cache.getOrGenerate(
65
+ 'Explain Redis in one paragraph',
66
+ () => callTheModel(),
67
+ { provider: 'openai', model: 'gpt-4o' },
68
+ )
69
+ ```
70
+
71
+ ## API summary
72
+
73
+ | Symbol | Kind | Purpose |
74
+ |--------|------|---------|
75
+ | `KeyBuilder.build/normalize/hash` | class (static) | Cache key generation |
76
+ | `TTLManager.computeExpiresAt/isExpired/remaining/slide` | class (static) | TTL arithmetic |
77
+ | `JsonSerializer` | class | Default JSON value codec (implements `Serializer`) |
78
+ | `BaseCacheManager` | abstract class | Cache-aside base for feature caches |
79
+ | `StorageAdapter<T>` | interface | Backend contract |
80
+ | `VectorStoreAdapter<M>` | interface | Vector DB contract |
81
+ | `CompressionEngine` | interface | Compression contract |
82
+ | `MetricsSink` | interface | Metrics emission contract |
83
+ | `NodeLLMCacheError` + subclasses | classes | Typed error hierarchy |
84
+
85
+ ## Notes
86
+
87
+ - The architecture specifies MessagePack as the primary serialization format. To keep `core` dependency-free, this package ships only `JsonSerializer`; a MessagePack `Serializer` can be supplied by an optional package and injected wherever a `Serializer` is accepted.
88
+ - `MetricsSink` defaults to a no-op (`noopMetrics`). Wire in `@nodellmcache/observability` to collect real metrics.
89
+
90
+ ## License
91
+
92
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BaseCacheManager: () => BaseCacheManager,
24
+ CacheAdapterError: () => CacheAdapterError,
25
+ CompressionError: () => CompressionError,
26
+ JsonSerializer: () => JsonSerializer,
27
+ KeyBuilder: () => KeyBuilder,
28
+ NodeLLMCacheError: () => NodeLLMCacheError,
29
+ SerializationError: () => SerializationError,
30
+ TTLManager: () => TTLManager,
31
+ ValidationError: () => ValidationError,
32
+ noopMetrics: () => noopMetrics
33
+ });
34
+ module.exports = __toCommonJS(index_exports);
35
+
36
+ // src/errors.ts
37
+ var NodeLLMCacheError = class extends Error {
38
+ constructor(message, options) {
39
+ super(message, options);
40
+ this.name = new.target.name;
41
+ Object.setPrototypeOf(this, new.target.prototype);
42
+ }
43
+ };
44
+ var CacheAdapterError = class extends NodeLLMCacheError {
45
+ };
46
+ var CompressionError = class extends NodeLLMCacheError {
47
+ };
48
+ var SerializationError = class extends NodeLLMCacheError {
49
+ };
50
+ var ValidationError = class extends NodeLLMCacheError {
51
+ };
52
+
53
+ // src/KeyBuilder.ts
54
+ var import_node_crypto = require("crypto");
55
+ var KeyBuilder = class _KeyBuilder {
56
+ /**
57
+ * Normalizes text before hashing so trivially different inputs collapse to
58
+ * the same key: trims surrounding whitespace, lowercases, and collapses any
59
+ * run of whitespace to a single space.
60
+ */
61
+ static normalize(text) {
62
+ return text.trim().toLowerCase().replace(/\s+/g, " ");
63
+ }
64
+ /**
65
+ * Produces the SHA-256 hex digest of the normalized text.
66
+ */
67
+ static hash(text) {
68
+ return (0, import_node_crypto.createHash)("sha256").update(_KeyBuilder.normalize(text)).digest("hex");
69
+ }
70
+ /**
71
+ * Builds a fully namespaced cache key.
72
+ *
73
+ * @example
74
+ * KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'hello world')
75
+ * // 'prompt:openai:gpt-4o:b94d27b9...'
76
+ */
77
+ static build(type, provider, model, text) {
78
+ return `${type}:${provider}:${model}:${_KeyBuilder.hash(text)}`;
79
+ }
80
+ };
81
+
82
+ // src/TTLManager.ts
83
+ var TTLManager = class _TTLManager {
84
+ /**
85
+ * Computes the absolute expiry timestamp for an entry created at `createdAt`
86
+ * with a relative `ttl`. Returns `undefined` when `ttl` is not a positive
87
+ * number, meaning the entry never expires.
88
+ */
89
+ static computeExpiresAt(createdAt, ttl) {
90
+ if (ttl === void 0 || ttl <= 0) return void 0;
91
+ return createdAt + ttl;
92
+ }
93
+ /**
94
+ * Returns true when the entry has an expiry and that expiry is at or before
95
+ * `now` (defaults to the current time).
96
+ */
97
+ static isExpired(entry, now = Date.now()) {
98
+ return entry.expiresAt !== void 0 && entry.expiresAt <= now;
99
+ }
100
+ /**
101
+ * Milliseconds remaining until the entry expires. Returns `Infinity` for
102
+ * entries with no expiry, and `0` for already-expired entries.
103
+ */
104
+ static remaining(entry, now = Date.now()) {
105
+ if (entry.expiresAt === void 0) return Infinity;
106
+ return Math.max(0, entry.expiresAt - now);
107
+ }
108
+ /**
109
+ * Computes a refreshed expiry for a sliding-window TTL: extends the entry's
110
+ * life by `ttl` from `now`. Returns `undefined` when `ttl` is not positive.
111
+ */
112
+ static slide(ttl, now = Date.now()) {
113
+ return _TTLManager.computeExpiresAt(now, ttl);
114
+ }
115
+ };
116
+
117
+ // src/Serializer.ts
118
+ var JsonSerializer = class {
119
+ serialize(value) {
120
+ try {
121
+ return Buffer.from(JSON.stringify(value), "utf8");
122
+ } catch (cause) {
123
+ throw new SerializationError("Failed to serialize value to JSON", { cause });
124
+ }
125
+ }
126
+ deserialize(data) {
127
+ try {
128
+ return JSON.parse(data.toString("utf8"));
129
+ } catch (cause) {
130
+ throw new SerializationError("Failed to deserialize JSON value", { cause });
131
+ }
132
+ }
133
+ };
134
+
135
+ // src/BaseCacheManager.ts
136
+ var noopMetrics = {
137
+ emit() {
138
+ }
139
+ };
140
+ var BaseCacheManager = class {
141
+ adapter;
142
+ defaultTTL;
143
+ metrics;
144
+ hits = 0;
145
+ misses = 0;
146
+ constructor(options) {
147
+ this.adapter = options.adapter;
148
+ this.defaultTTL = options.defaultTTL;
149
+ this.metrics = options.metrics ?? noopMetrics;
150
+ }
151
+ /**
152
+ * Builds the storage key for an input. Defaults to the canonical
153
+ * `{type}:{provider}:{model}:{hash}` format via {@link KeyBuilder}.
154
+ */
155
+ buildKey(input, options) {
156
+ return KeyBuilder.build(
157
+ this.cacheType,
158
+ options?.provider ?? "unknown",
159
+ options?.model ?? "default",
160
+ input
161
+ );
162
+ }
163
+ /**
164
+ * Wraps a value in a {@link CacheEntry} with computed expiry and metadata.
165
+ */
166
+ buildEntry(key, value, options) {
167
+ const createdAt = Date.now();
168
+ const ttl = options?.ttl ?? this.defaultTTL;
169
+ return {
170
+ key,
171
+ value,
172
+ createdAt,
173
+ expiresAt: TTLManager.computeExpiresAt(createdAt, ttl),
174
+ metadata: {
175
+ compressed: false,
176
+ originalSize: 0,
177
+ cacheType: this.cacheType,
178
+ provider: options?.provider,
179
+ model: options?.model,
180
+ tokenCount: options?.tokenCount
181
+ }
182
+ };
183
+ }
184
+ /**
185
+ * Cache-aside read-through. Returns the cached value on a hit, otherwise
186
+ * invokes `generator`, stores the result, and returns it. Set
187
+ * `options.cache = false` to bypass the cache for both read and write.
188
+ */
189
+ async getOrGenerate(input, generator, options) {
190
+ if (options?.cache === false) {
191
+ return generator();
192
+ }
193
+ const key = this.buildKey(input, options);
194
+ const start = Date.now();
195
+ const cached = await this.adapter.get(key);
196
+ if (cached && !TTLManager.isExpired(cached)) {
197
+ this.hits++;
198
+ this.metrics.emit("cache.hit", {
199
+ cacheType: this.cacheType,
200
+ latencyMs: Date.now() - start,
201
+ tokensSaved: cached.metadata.tokenCount,
202
+ provider: options?.provider,
203
+ model: options?.model
204
+ });
205
+ return cached.value;
206
+ }
207
+ this.misses++;
208
+ this.metrics.emit("cache.miss", {
209
+ cacheType: this.cacheType,
210
+ latencyMs: Date.now() - start,
211
+ provider: options?.provider,
212
+ model: options?.model
213
+ });
214
+ 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
+ cacheType: this.cacheType,
220
+ latencyMs: Date.now() - start,
221
+ provider: options?.provider,
222
+ model: options?.model
223
+ });
224
+ return value;
225
+ }
226
+ /**
227
+ * Removes a single entry by its input (and namespacing options).
228
+ */
229
+ async invalidate(input, options) {
230
+ await this.adapter.delete(this.buildKey(input, options));
231
+ }
232
+ /**
233
+ * Returns hit/miss accounting for this manager plus the adapter's entry count.
234
+ */
235
+ async stats() {
236
+ const total = this.hits + this.misses;
237
+ const adapterStats = await this.adapter.stats();
238
+ return {
239
+ hits: this.hits,
240
+ misses: this.misses,
241
+ hitRate: total === 0 ? 0 : this.hits / total,
242
+ entryCount: adapterStats.entryCount
243
+ };
244
+ }
245
+ };
246
+ // Annotate the CommonJS export names for ESM import in node:
247
+ 0 && (module.exports = {
248
+ BaseCacheManager,
249
+ CacheAdapterError,
250
+ CompressionError,
251
+ JsonSerializer,
252
+ KeyBuilder,
253
+ NodeLLMCacheError,
254
+ SerializationError,
255
+ TTLManager,
256
+ ValidationError,
257
+ noopMetrics
258
+ });
259
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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":[]}
@@ -0,0 +1,316 @@
1
+ /**
2
+ * The category of data a cache stores. Used in key namespacing and metrics
3
+ * breakdowns so every cached value is attributable to a workload.
4
+ */
5
+ type CacheType = 'prompt' | 'embedding' | 'semantic' | 'retrieval' | 'context' | 'agent' | 'tool' | 'conversation';
6
+ /**
7
+ * Canonical LLM provider names. See the providers table in CLAUDE.md for the
8
+ * common models associated with each.
9
+ */
10
+ type LLMProvider = 'openai' | 'anthropic' | 'gemini' | 'deepseek' | 'llama' | 'mistral' | 'ollama';
11
+ /**
12
+ * Supported compression algorithms. `auto` defers the choice to the
13
+ * compression engine's size/hint heuristics; `none` is a passthrough.
14
+ */
15
+ type CompressionAlgo = 'lz4' | 'brotli' | 'zstd' | 'gzip' | 'none' | 'auto';
16
+ /**
17
+ * A hint describing the shape of a payload so the compression engine can pick
18
+ * the best codec (e.g. `embedding` favours lz4, `text` favours brotli).
19
+ */
20
+ type DataHint = 'embedding' | 'text' | 'json' | 'binary';
21
+
22
+ /**
23
+ * Metadata stored alongside every cache entry. Captures how the value was
24
+ * encoded and which LLM workload produced it.
25
+ */
26
+ interface CacheMetadata {
27
+ compressed: boolean;
28
+ compressionAlgo?: CompressionAlgo;
29
+ originalSize: number;
30
+ compressedSize?: number;
31
+ cacheType: CacheType;
32
+ provider?: LLMProvider;
33
+ model?: string;
34
+ tokenCount?: number;
35
+ }
36
+ /**
37
+ * A single cached value with its bookkeeping. `expiresAt` is an absolute epoch
38
+ * timestamp in milliseconds; when omitted the entry never expires.
39
+ */
40
+ interface CacheEntry<T> {
41
+ key: string;
42
+ value: T;
43
+ createdAt: number;
44
+ expiresAt?: number;
45
+ metadata: CacheMetadata;
46
+ }
47
+ /**
48
+ * Aggregate statistics reported by a storage adapter.
49
+ */
50
+ interface AdapterStats {
51
+ /** Number of entries currently held. */
52
+ entryCount: number;
53
+ /** Approximate bytes used by stored entries, if the adapter can measure it. */
54
+ sizeBytes?: number;
55
+ /** Number of evictions performed over the adapter's lifetime. */
56
+ evictions?: number;
57
+ }
58
+ /**
59
+ * The contract every storage backend implements. Application code never
60
+ * imports a concrete adapter in business logic — adapters are injected.
61
+ */
62
+ interface StorageAdapter<T = unknown> {
63
+ get(key: string): Promise<CacheEntry<T> | null>;
64
+ /** `ttl` is a relative duration in milliseconds from now. */
65
+ set(key: string, entry: CacheEntry<T>, ttl?: number): Promise<void>;
66
+ delete(key: string): Promise<void>;
67
+ clear(): Promise<void>;
68
+ has(key: string): Promise<boolean>;
69
+ stats(): Promise<AdapterStats>;
70
+ }
71
+ /**
72
+ * Result of a single compression operation.
73
+ */
74
+ interface CompressedResult {
75
+ data: Buffer;
76
+ algo: CompressionAlgo;
77
+ originalSize: number;
78
+ compressedSize: number;
79
+ ratio: number;
80
+ durationMs: number;
81
+ }
82
+ /**
83
+ * Statistics for a completed compression, suitable for observability rollups.
84
+ */
85
+ interface CompressionStats {
86
+ originalSize: number;
87
+ compressedSize: number;
88
+ ratio: number;
89
+ savedBytes: number;
90
+ savedPercent: number;
91
+ }
92
+ /**
93
+ * The compression engine contract implemented by `@nodellmcache/compression`.
94
+ */
95
+ interface CompressionEngine {
96
+ compress(data: Buffer, algo: CompressionAlgo): Promise<Buffer>;
97
+ decompress(data: Buffer, algo: CompressionAlgo): Promise<Buffer>;
98
+ auto(data: Buffer, hint?: DataHint): Promise<CompressedResult>;
99
+ stats(original: Buffer, compressed: Buffer): CompressionStats;
100
+ }
101
+ /**
102
+ * A vector match returned by a vector store query.
103
+ */
104
+ interface VectorMatch<M = Record<string, unknown>> {
105
+ id: string;
106
+ score: number;
107
+ vector?: number[];
108
+ metadata?: M;
109
+ }
110
+ /**
111
+ * The contract every vector database adapter implements.
112
+ */
113
+ interface VectorStoreAdapter<M = Record<string, unknown>> {
114
+ upsert(id: string, vector: number[], metadata?: M): Promise<void>;
115
+ query(vector: number[], topK: number, filter?: Partial<M>): Promise<VectorMatch<M>[]>;
116
+ delete(id: string): Promise<void>;
117
+ }
118
+ /**
119
+ * The names of metric events emitted by cache managers.
120
+ */
121
+ type MetricEvent = 'cache.hit' | 'cache.miss' | 'cache.set' | 'cache.evict';
122
+ /**
123
+ * Payload accompanying a metric event.
124
+ */
125
+ interface MetricData {
126
+ cacheType: CacheType;
127
+ latencyMs: number;
128
+ tokensSaved?: number;
129
+ estimatedCostUSD?: number;
130
+ provider?: LLMProvider;
131
+ model?: string;
132
+ }
133
+ /**
134
+ * A sink for cache metrics. `@nodellmcache/observability` provides the real
135
+ * implementation; core ships a no-op so it stays dependency-free.
136
+ */
137
+ interface MetricsSink {
138
+ emit(event: MetricEvent, data: MetricData): void;
139
+ }
140
+ /**
141
+ * Per-call options shared across cache managers.
142
+ */
143
+ interface CacheOptions {
144
+ provider?: LLMProvider;
145
+ model?: string;
146
+ /** Relative TTL in milliseconds; overrides the manager default. */
147
+ ttl?: number;
148
+ /** When false, bypasses the cache entirely (read and write). */
149
+ cache?: boolean;
150
+ tokenCount?: number;
151
+ }
152
+ /**
153
+ * Hit/miss statistics reported by a cache manager.
154
+ */
155
+ interface CacheStats {
156
+ hits: number;
157
+ misses: number;
158
+ hitRate: number;
159
+ entryCount: number;
160
+ }
161
+
162
+ /**
163
+ * Base class for every error thrown by NodeLLMCache packages. Catching this
164
+ * catches anything the library throws intentionally.
165
+ */
166
+ declare class NodeLLMCacheError extends Error {
167
+ constructor(message: string, options?: ErrorOptions);
168
+ }
169
+ /** Thrown when a storage adapter operation fails. */
170
+ declare class CacheAdapterError extends NodeLLMCacheError {
171
+ }
172
+ /** Thrown when compression or decompression fails. */
173
+ declare class CompressionError extends NodeLLMCacheError {
174
+ }
175
+ /** Thrown when serialization or deserialization fails. */
176
+ declare class SerializationError extends NodeLLMCacheError {
177
+ }
178
+ /** Thrown when a value fails validation (e.g. malformed configuration). */
179
+ declare class ValidationError extends NodeLLMCacheError {
180
+ }
181
+
182
+ /**
183
+ * Builds deterministic, collision-resistant cache keys.
184
+ *
185
+ * Format: `{type}:{provider}:{model}:{sha256-hash}`
186
+ *
187
+ * The raw input text is normalized and hashed with SHA-256 — keys never
188
+ * contain raw prompt text, which keeps sensitive content out of storage keys
189
+ * and logs.
190
+ */
191
+ declare class KeyBuilder {
192
+ /**
193
+ * Normalizes text before hashing so trivially different inputs collapse to
194
+ * the same key: trims surrounding whitespace, lowercases, and collapses any
195
+ * run of whitespace to a single space.
196
+ */
197
+ static normalize(text: string): string;
198
+ /**
199
+ * Produces the SHA-256 hex digest of the normalized text.
200
+ */
201
+ static hash(text: string): string;
202
+ /**
203
+ * Builds a fully namespaced cache key.
204
+ *
205
+ * @example
206
+ * KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'hello world')
207
+ * // 'prompt:openai:gpt-4o:b94d27b9...'
208
+ */
209
+ static build(type: CacheType, provider: LLMProvider | string, model: string, text: string): string;
210
+ }
211
+
212
+ /**
213
+ * Centralizes time-to-live arithmetic: computing expiry timestamps, checking
214
+ * expiry, and supporting sliding-window refresh. All durations are relative
215
+ * milliseconds; all timestamps are absolute epoch milliseconds.
216
+ */
217
+ declare class TTLManager {
218
+ /**
219
+ * Computes the absolute expiry timestamp for an entry created at `createdAt`
220
+ * with a relative `ttl`. Returns `undefined` when `ttl` is not a positive
221
+ * number, meaning the entry never expires.
222
+ */
223
+ static computeExpiresAt(createdAt: number, ttl?: number): number | undefined;
224
+ /**
225
+ * Returns true when the entry has an expiry and that expiry is at or before
226
+ * `now` (defaults to the current time).
227
+ */
228
+ static isExpired(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now?: number): boolean;
229
+ /**
230
+ * Milliseconds remaining until the entry expires. Returns `Infinity` for
231
+ * entries with no expiry, and `0` for already-expired entries.
232
+ */
233
+ static remaining(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now?: number): number;
234
+ /**
235
+ * Computes a refreshed expiry for a sliding-window TTL: extends the entry's
236
+ * life by `ttl` from `now`. Returns `undefined` when `ttl` is not positive.
237
+ */
238
+ static slide(ttl?: number, now?: number): number | undefined;
239
+ }
240
+
241
+ /**
242
+ * Converts values to and from `Buffer` for storage. Implementations decide the
243
+ * wire format.
244
+ *
245
+ * The architecture calls for MessagePack as the primary format, but
246
+ * `@nodellmcache/core` must stay dependency-free, so it ships only the JSON
247
+ * implementation below. A MessagePack serializer can be supplied by an optional
248
+ * package and injected wherever a `Serializer` is accepted.
249
+ */
250
+ interface Serializer {
251
+ serialize<T>(value: T): Buffer;
252
+ deserialize<T>(data: Buffer): T;
253
+ }
254
+ /**
255
+ * Default JSON-backed serializer. Zero dependencies; handles the common case
256
+ * and wraps failures in {@link SerializationError}.
257
+ */
258
+ declare class JsonSerializer implements Serializer {
259
+ serialize<T>(value: T): Buffer;
260
+ deserialize<T>(data: Buffer): T;
261
+ }
262
+
263
+ /** A metrics sink that discards everything; the default when none is injected. */
264
+ declare const noopMetrics: MetricsSink;
265
+ /**
266
+ * Construction options shared by all cache managers.
267
+ */
268
+ interface BaseCacheManagerOptions<T> {
269
+ adapter: StorageAdapter<T>;
270
+ /** Default relative TTL in milliseconds applied to entries lacking their own. */
271
+ defaultTTL?: number;
272
+ /** Metrics sink; defaults to a no-op so core stays dependency-free. */
273
+ metrics?: MetricsSink;
274
+ }
275
+ /**
276
+ * Shared base for every feature cache (prompt, embedding, semantic, ...).
277
+ *
278
+ * Provides the cache-aside `getOrGenerate` flow, key building, entry
279
+ * construction, invalidation, and hit/miss accounting. Subclasses declare their
280
+ * {@link CacheType} and may override {@link buildKey} for custom namespacing.
281
+ */
282
+ declare abstract class BaseCacheManager<T> {
283
+ /** The workload category for keys and metrics. */
284
+ protected abstract readonly cacheType: CacheType;
285
+ protected readonly adapter: StorageAdapter<T>;
286
+ protected readonly defaultTTL: number | undefined;
287
+ protected readonly metrics: MetricsSink;
288
+ private hits;
289
+ private misses;
290
+ constructor(options: BaseCacheManagerOptions<T>);
291
+ /**
292
+ * Builds the storage key for an input. Defaults to the canonical
293
+ * `{type}:{provider}:{model}:{hash}` format via {@link KeyBuilder}.
294
+ */
295
+ protected buildKey(input: string, options?: CacheOptions): string;
296
+ /**
297
+ * Wraps a value in a {@link CacheEntry} with computed expiry and metadata.
298
+ */
299
+ protected buildEntry(key: string, value: T, options?: CacheOptions): CacheEntry<T>;
300
+ /**
301
+ * Cache-aside read-through. Returns the cached value on a hit, otherwise
302
+ * invokes `generator`, stores the result, and returns it. Set
303
+ * `options.cache = false` to bypass the cache for both read and write.
304
+ */
305
+ getOrGenerate(input: string, generator: () => Promise<T>, options?: CacheOptions): Promise<T>;
306
+ /**
307
+ * Removes a single entry by its input (and namespacing options).
308
+ */
309
+ invalidate(input: string, options?: CacheOptions): Promise<void>;
310
+ /**
311
+ * Returns hit/miss accounting for this manager plus the adapter's entry count.
312
+ */
313
+ stats(): Promise<CacheStats>;
314
+ }
315
+
316
+ 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 };
@@ -0,0 +1,316 @@
1
+ /**
2
+ * The category of data a cache stores. Used in key namespacing and metrics
3
+ * breakdowns so every cached value is attributable to a workload.
4
+ */
5
+ type CacheType = 'prompt' | 'embedding' | 'semantic' | 'retrieval' | 'context' | 'agent' | 'tool' | 'conversation';
6
+ /**
7
+ * Canonical LLM provider names. See the providers table in CLAUDE.md for the
8
+ * common models associated with each.
9
+ */
10
+ type LLMProvider = 'openai' | 'anthropic' | 'gemini' | 'deepseek' | 'llama' | 'mistral' | 'ollama';
11
+ /**
12
+ * Supported compression algorithms. `auto` defers the choice to the
13
+ * compression engine's size/hint heuristics; `none` is a passthrough.
14
+ */
15
+ type CompressionAlgo = 'lz4' | 'brotli' | 'zstd' | 'gzip' | 'none' | 'auto';
16
+ /**
17
+ * A hint describing the shape of a payload so the compression engine can pick
18
+ * the best codec (e.g. `embedding` favours lz4, `text` favours brotli).
19
+ */
20
+ type DataHint = 'embedding' | 'text' | 'json' | 'binary';
21
+
22
+ /**
23
+ * Metadata stored alongside every cache entry. Captures how the value was
24
+ * encoded and which LLM workload produced it.
25
+ */
26
+ interface CacheMetadata {
27
+ compressed: boolean;
28
+ compressionAlgo?: CompressionAlgo;
29
+ originalSize: number;
30
+ compressedSize?: number;
31
+ cacheType: CacheType;
32
+ provider?: LLMProvider;
33
+ model?: string;
34
+ tokenCount?: number;
35
+ }
36
+ /**
37
+ * A single cached value with its bookkeeping. `expiresAt` is an absolute epoch
38
+ * timestamp in milliseconds; when omitted the entry never expires.
39
+ */
40
+ interface CacheEntry<T> {
41
+ key: string;
42
+ value: T;
43
+ createdAt: number;
44
+ expiresAt?: number;
45
+ metadata: CacheMetadata;
46
+ }
47
+ /**
48
+ * Aggregate statistics reported by a storage adapter.
49
+ */
50
+ interface AdapterStats {
51
+ /** Number of entries currently held. */
52
+ entryCount: number;
53
+ /** Approximate bytes used by stored entries, if the adapter can measure it. */
54
+ sizeBytes?: number;
55
+ /** Number of evictions performed over the adapter's lifetime. */
56
+ evictions?: number;
57
+ }
58
+ /**
59
+ * The contract every storage backend implements. Application code never
60
+ * imports a concrete adapter in business logic — adapters are injected.
61
+ */
62
+ interface StorageAdapter<T = unknown> {
63
+ get(key: string): Promise<CacheEntry<T> | null>;
64
+ /** `ttl` is a relative duration in milliseconds from now. */
65
+ set(key: string, entry: CacheEntry<T>, ttl?: number): Promise<void>;
66
+ delete(key: string): Promise<void>;
67
+ clear(): Promise<void>;
68
+ has(key: string): Promise<boolean>;
69
+ stats(): Promise<AdapterStats>;
70
+ }
71
+ /**
72
+ * Result of a single compression operation.
73
+ */
74
+ interface CompressedResult {
75
+ data: Buffer;
76
+ algo: CompressionAlgo;
77
+ originalSize: number;
78
+ compressedSize: number;
79
+ ratio: number;
80
+ durationMs: number;
81
+ }
82
+ /**
83
+ * Statistics for a completed compression, suitable for observability rollups.
84
+ */
85
+ interface CompressionStats {
86
+ originalSize: number;
87
+ compressedSize: number;
88
+ ratio: number;
89
+ savedBytes: number;
90
+ savedPercent: number;
91
+ }
92
+ /**
93
+ * The compression engine contract implemented by `@nodellmcache/compression`.
94
+ */
95
+ interface CompressionEngine {
96
+ compress(data: Buffer, algo: CompressionAlgo): Promise<Buffer>;
97
+ decompress(data: Buffer, algo: CompressionAlgo): Promise<Buffer>;
98
+ auto(data: Buffer, hint?: DataHint): Promise<CompressedResult>;
99
+ stats(original: Buffer, compressed: Buffer): CompressionStats;
100
+ }
101
+ /**
102
+ * A vector match returned by a vector store query.
103
+ */
104
+ interface VectorMatch<M = Record<string, unknown>> {
105
+ id: string;
106
+ score: number;
107
+ vector?: number[];
108
+ metadata?: M;
109
+ }
110
+ /**
111
+ * The contract every vector database adapter implements.
112
+ */
113
+ interface VectorStoreAdapter<M = Record<string, unknown>> {
114
+ upsert(id: string, vector: number[], metadata?: M): Promise<void>;
115
+ query(vector: number[], topK: number, filter?: Partial<M>): Promise<VectorMatch<M>[]>;
116
+ delete(id: string): Promise<void>;
117
+ }
118
+ /**
119
+ * The names of metric events emitted by cache managers.
120
+ */
121
+ type MetricEvent = 'cache.hit' | 'cache.miss' | 'cache.set' | 'cache.evict';
122
+ /**
123
+ * Payload accompanying a metric event.
124
+ */
125
+ interface MetricData {
126
+ cacheType: CacheType;
127
+ latencyMs: number;
128
+ tokensSaved?: number;
129
+ estimatedCostUSD?: number;
130
+ provider?: LLMProvider;
131
+ model?: string;
132
+ }
133
+ /**
134
+ * A sink for cache metrics. `@nodellmcache/observability` provides the real
135
+ * implementation; core ships a no-op so it stays dependency-free.
136
+ */
137
+ interface MetricsSink {
138
+ emit(event: MetricEvent, data: MetricData): void;
139
+ }
140
+ /**
141
+ * Per-call options shared across cache managers.
142
+ */
143
+ interface CacheOptions {
144
+ provider?: LLMProvider;
145
+ model?: string;
146
+ /** Relative TTL in milliseconds; overrides the manager default. */
147
+ ttl?: number;
148
+ /** When false, bypasses the cache entirely (read and write). */
149
+ cache?: boolean;
150
+ tokenCount?: number;
151
+ }
152
+ /**
153
+ * Hit/miss statistics reported by a cache manager.
154
+ */
155
+ interface CacheStats {
156
+ hits: number;
157
+ misses: number;
158
+ hitRate: number;
159
+ entryCount: number;
160
+ }
161
+
162
+ /**
163
+ * Base class for every error thrown by NodeLLMCache packages. Catching this
164
+ * catches anything the library throws intentionally.
165
+ */
166
+ declare class NodeLLMCacheError extends Error {
167
+ constructor(message: string, options?: ErrorOptions);
168
+ }
169
+ /** Thrown when a storage adapter operation fails. */
170
+ declare class CacheAdapterError extends NodeLLMCacheError {
171
+ }
172
+ /** Thrown when compression or decompression fails. */
173
+ declare class CompressionError extends NodeLLMCacheError {
174
+ }
175
+ /** Thrown when serialization or deserialization fails. */
176
+ declare class SerializationError extends NodeLLMCacheError {
177
+ }
178
+ /** Thrown when a value fails validation (e.g. malformed configuration). */
179
+ declare class ValidationError extends NodeLLMCacheError {
180
+ }
181
+
182
+ /**
183
+ * Builds deterministic, collision-resistant cache keys.
184
+ *
185
+ * Format: `{type}:{provider}:{model}:{sha256-hash}`
186
+ *
187
+ * The raw input text is normalized and hashed with SHA-256 — keys never
188
+ * contain raw prompt text, which keeps sensitive content out of storage keys
189
+ * and logs.
190
+ */
191
+ declare class KeyBuilder {
192
+ /**
193
+ * Normalizes text before hashing so trivially different inputs collapse to
194
+ * the same key: trims surrounding whitespace, lowercases, and collapses any
195
+ * run of whitespace to a single space.
196
+ */
197
+ static normalize(text: string): string;
198
+ /**
199
+ * Produces the SHA-256 hex digest of the normalized text.
200
+ */
201
+ static hash(text: string): string;
202
+ /**
203
+ * Builds a fully namespaced cache key.
204
+ *
205
+ * @example
206
+ * KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'hello world')
207
+ * // 'prompt:openai:gpt-4o:b94d27b9...'
208
+ */
209
+ static build(type: CacheType, provider: LLMProvider | string, model: string, text: string): string;
210
+ }
211
+
212
+ /**
213
+ * Centralizes time-to-live arithmetic: computing expiry timestamps, checking
214
+ * expiry, and supporting sliding-window refresh. All durations are relative
215
+ * milliseconds; all timestamps are absolute epoch milliseconds.
216
+ */
217
+ declare class TTLManager {
218
+ /**
219
+ * Computes the absolute expiry timestamp for an entry created at `createdAt`
220
+ * with a relative `ttl`. Returns `undefined` when `ttl` is not a positive
221
+ * number, meaning the entry never expires.
222
+ */
223
+ static computeExpiresAt(createdAt: number, ttl?: number): number | undefined;
224
+ /**
225
+ * Returns true when the entry has an expiry and that expiry is at or before
226
+ * `now` (defaults to the current time).
227
+ */
228
+ static isExpired(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now?: number): boolean;
229
+ /**
230
+ * Milliseconds remaining until the entry expires. Returns `Infinity` for
231
+ * entries with no expiry, and `0` for already-expired entries.
232
+ */
233
+ static remaining(entry: Pick<CacheEntry<unknown>, 'expiresAt'>, now?: number): number;
234
+ /**
235
+ * Computes a refreshed expiry for a sliding-window TTL: extends the entry's
236
+ * life by `ttl` from `now`. Returns `undefined` when `ttl` is not positive.
237
+ */
238
+ static slide(ttl?: number, now?: number): number | undefined;
239
+ }
240
+
241
+ /**
242
+ * Converts values to and from `Buffer` for storage. Implementations decide the
243
+ * wire format.
244
+ *
245
+ * The architecture calls for MessagePack as the primary format, but
246
+ * `@nodellmcache/core` must stay dependency-free, so it ships only the JSON
247
+ * implementation below. A MessagePack serializer can be supplied by an optional
248
+ * package and injected wherever a `Serializer` is accepted.
249
+ */
250
+ interface Serializer {
251
+ serialize<T>(value: T): Buffer;
252
+ deserialize<T>(data: Buffer): T;
253
+ }
254
+ /**
255
+ * Default JSON-backed serializer. Zero dependencies; handles the common case
256
+ * and wraps failures in {@link SerializationError}.
257
+ */
258
+ declare class JsonSerializer implements Serializer {
259
+ serialize<T>(value: T): Buffer;
260
+ deserialize<T>(data: Buffer): T;
261
+ }
262
+
263
+ /** A metrics sink that discards everything; the default when none is injected. */
264
+ declare const noopMetrics: MetricsSink;
265
+ /**
266
+ * Construction options shared by all cache managers.
267
+ */
268
+ interface BaseCacheManagerOptions<T> {
269
+ adapter: StorageAdapter<T>;
270
+ /** Default relative TTL in milliseconds applied to entries lacking their own. */
271
+ defaultTTL?: number;
272
+ /** Metrics sink; defaults to a no-op so core stays dependency-free. */
273
+ metrics?: MetricsSink;
274
+ }
275
+ /**
276
+ * Shared base for every feature cache (prompt, embedding, semantic, ...).
277
+ *
278
+ * Provides the cache-aside `getOrGenerate` flow, key building, entry
279
+ * construction, invalidation, and hit/miss accounting. Subclasses declare their
280
+ * {@link CacheType} and may override {@link buildKey} for custom namespacing.
281
+ */
282
+ declare abstract class BaseCacheManager<T> {
283
+ /** The workload category for keys and metrics. */
284
+ protected abstract readonly cacheType: CacheType;
285
+ protected readonly adapter: StorageAdapter<T>;
286
+ protected readonly defaultTTL: number | undefined;
287
+ protected readonly metrics: MetricsSink;
288
+ private hits;
289
+ private misses;
290
+ constructor(options: BaseCacheManagerOptions<T>);
291
+ /**
292
+ * Builds the storage key for an input. Defaults to the canonical
293
+ * `{type}:{provider}:{model}:{hash}` format via {@link KeyBuilder}.
294
+ */
295
+ protected buildKey(input: string, options?: CacheOptions): string;
296
+ /**
297
+ * Wraps a value in a {@link CacheEntry} with computed expiry and metadata.
298
+ */
299
+ protected buildEntry(key: string, value: T, options?: CacheOptions): CacheEntry<T>;
300
+ /**
301
+ * Cache-aside read-through. Returns the cached value on a hit, otherwise
302
+ * invokes `generator`, stores the result, and returns it. Set
303
+ * `options.cache = false` to bypass the cache for both read and write.
304
+ */
305
+ getOrGenerate(input: string, generator: () => Promise<T>, options?: CacheOptions): Promise<T>;
306
+ /**
307
+ * Removes a single entry by its input (and namespacing options).
308
+ */
309
+ invalidate(input: string, options?: CacheOptions): Promise<void>;
310
+ /**
311
+ * Returns hit/miss accounting for this manager plus the adapter's entry count.
312
+ */
313
+ stats(): Promise<CacheStats>;
314
+ }
315
+
316
+ 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 ADDED
@@ -0,0 +1,223 @@
1
+ // src/errors.ts
2
+ var NodeLLMCacheError = class extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = new.target.name;
6
+ Object.setPrototypeOf(this, new.target.prototype);
7
+ }
8
+ };
9
+ var CacheAdapterError = class extends NodeLLMCacheError {
10
+ };
11
+ var CompressionError = class extends NodeLLMCacheError {
12
+ };
13
+ var SerializationError = class extends NodeLLMCacheError {
14
+ };
15
+ var ValidationError = class extends NodeLLMCacheError {
16
+ };
17
+
18
+ // src/KeyBuilder.ts
19
+ import { createHash } from "crypto";
20
+ var KeyBuilder = class _KeyBuilder {
21
+ /**
22
+ * Normalizes text before hashing so trivially different inputs collapse to
23
+ * the same key: trims surrounding whitespace, lowercases, and collapses any
24
+ * run of whitespace to a single space.
25
+ */
26
+ static normalize(text) {
27
+ return text.trim().toLowerCase().replace(/\s+/g, " ");
28
+ }
29
+ /**
30
+ * Produces the SHA-256 hex digest of the normalized text.
31
+ */
32
+ static hash(text) {
33
+ return createHash("sha256").update(_KeyBuilder.normalize(text)).digest("hex");
34
+ }
35
+ /**
36
+ * Builds a fully namespaced cache key.
37
+ *
38
+ * @example
39
+ * KeyBuilder.build('prompt', 'openai', 'gpt-4o', 'hello world')
40
+ * // 'prompt:openai:gpt-4o:b94d27b9...'
41
+ */
42
+ static build(type, provider, model, text) {
43
+ return `${type}:${provider}:${model}:${_KeyBuilder.hash(text)}`;
44
+ }
45
+ };
46
+
47
+ // src/TTLManager.ts
48
+ var TTLManager = class _TTLManager {
49
+ /**
50
+ * Computes the absolute expiry timestamp for an entry created at `createdAt`
51
+ * with a relative `ttl`. Returns `undefined` when `ttl` is not a positive
52
+ * number, meaning the entry never expires.
53
+ */
54
+ static computeExpiresAt(createdAt, ttl) {
55
+ if (ttl === void 0 || ttl <= 0) return void 0;
56
+ return createdAt + ttl;
57
+ }
58
+ /**
59
+ * Returns true when the entry has an expiry and that expiry is at or before
60
+ * `now` (defaults to the current time).
61
+ */
62
+ static isExpired(entry, now = Date.now()) {
63
+ return entry.expiresAt !== void 0 && entry.expiresAt <= now;
64
+ }
65
+ /**
66
+ * Milliseconds remaining until the entry expires. Returns `Infinity` for
67
+ * entries with no expiry, and `0` for already-expired entries.
68
+ */
69
+ static remaining(entry, now = Date.now()) {
70
+ if (entry.expiresAt === void 0) return Infinity;
71
+ return Math.max(0, entry.expiresAt - now);
72
+ }
73
+ /**
74
+ * Computes a refreshed expiry for a sliding-window TTL: extends the entry's
75
+ * life by `ttl` from `now`. Returns `undefined` when `ttl` is not positive.
76
+ */
77
+ static slide(ttl, now = Date.now()) {
78
+ return _TTLManager.computeExpiresAt(now, ttl);
79
+ }
80
+ };
81
+
82
+ // src/Serializer.ts
83
+ var JsonSerializer = class {
84
+ serialize(value) {
85
+ try {
86
+ return Buffer.from(JSON.stringify(value), "utf8");
87
+ } catch (cause) {
88
+ throw new SerializationError("Failed to serialize value to JSON", { cause });
89
+ }
90
+ }
91
+ deserialize(data) {
92
+ try {
93
+ return JSON.parse(data.toString("utf8"));
94
+ } catch (cause) {
95
+ throw new SerializationError("Failed to deserialize JSON value", { cause });
96
+ }
97
+ }
98
+ };
99
+
100
+ // src/BaseCacheManager.ts
101
+ var noopMetrics = {
102
+ emit() {
103
+ }
104
+ };
105
+ var BaseCacheManager = class {
106
+ adapter;
107
+ defaultTTL;
108
+ metrics;
109
+ hits = 0;
110
+ misses = 0;
111
+ constructor(options) {
112
+ this.adapter = options.adapter;
113
+ this.defaultTTL = options.defaultTTL;
114
+ this.metrics = options.metrics ?? noopMetrics;
115
+ }
116
+ /**
117
+ * Builds the storage key for an input. Defaults to the canonical
118
+ * `{type}:{provider}:{model}:{hash}` format via {@link KeyBuilder}.
119
+ */
120
+ buildKey(input, options) {
121
+ return KeyBuilder.build(
122
+ this.cacheType,
123
+ options?.provider ?? "unknown",
124
+ options?.model ?? "default",
125
+ input
126
+ );
127
+ }
128
+ /**
129
+ * Wraps a value in a {@link CacheEntry} with computed expiry and metadata.
130
+ */
131
+ buildEntry(key, value, options) {
132
+ const createdAt = Date.now();
133
+ const ttl = options?.ttl ?? this.defaultTTL;
134
+ return {
135
+ key,
136
+ value,
137
+ createdAt,
138
+ expiresAt: TTLManager.computeExpiresAt(createdAt, ttl),
139
+ metadata: {
140
+ compressed: false,
141
+ originalSize: 0,
142
+ cacheType: this.cacheType,
143
+ provider: options?.provider,
144
+ model: options?.model,
145
+ tokenCount: options?.tokenCount
146
+ }
147
+ };
148
+ }
149
+ /**
150
+ * Cache-aside read-through. Returns the cached value on a hit, otherwise
151
+ * invokes `generator`, stores the result, and returns it. Set
152
+ * `options.cache = false` to bypass the cache for both read and write.
153
+ */
154
+ async getOrGenerate(input, generator, options) {
155
+ if (options?.cache === false) {
156
+ return generator();
157
+ }
158
+ const key = this.buildKey(input, options);
159
+ const start = Date.now();
160
+ const cached = await this.adapter.get(key);
161
+ if (cached && !TTLManager.isExpired(cached)) {
162
+ this.hits++;
163
+ this.metrics.emit("cache.hit", {
164
+ cacheType: this.cacheType,
165
+ latencyMs: Date.now() - start,
166
+ tokensSaved: cached.metadata.tokenCount,
167
+ provider: options?.provider,
168
+ model: options?.model
169
+ });
170
+ return cached.value;
171
+ }
172
+ this.misses++;
173
+ this.metrics.emit("cache.miss", {
174
+ cacheType: this.cacheType,
175
+ latencyMs: Date.now() - start,
176
+ provider: options?.provider,
177
+ model: options?.model
178
+ });
179
+ 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
+ cacheType: this.cacheType,
185
+ latencyMs: Date.now() - start,
186
+ provider: options?.provider,
187
+ model: options?.model
188
+ });
189
+ return value;
190
+ }
191
+ /**
192
+ * Removes a single entry by its input (and namespacing options).
193
+ */
194
+ async invalidate(input, options) {
195
+ await this.adapter.delete(this.buildKey(input, options));
196
+ }
197
+ /**
198
+ * Returns hit/miss accounting for this manager plus the adapter's entry count.
199
+ */
200
+ async stats() {
201
+ const total = this.hits + this.misses;
202
+ const adapterStats = await this.adapter.stats();
203
+ return {
204
+ hits: this.hits,
205
+ misses: this.misses,
206
+ hitRate: total === 0 ? 0 : this.hits / total,
207
+ entryCount: adapterStats.entryCount
208
+ };
209
+ }
210
+ };
211
+ export {
212
+ BaseCacheManager,
213
+ CacheAdapterError,
214
+ CompressionError,
215
+ JsonSerializer,
216
+ KeyBuilder,
217
+ NodeLLMCacheError,
218
+ SerializationError,
219
+ TTLManager,
220
+ ValidationError,
221
+ noopMetrics
222
+ };
223
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@nodellmcache/core",
3
+ "version": "0.1.0",
4
+ "description": "Shared interfaces, types, and utilities for NodeLLMCache — AI memory infrastructure for Node.js",
5
+ "keywords": ["llm", "cache", "ai", "memory", "nodejs", "embeddings"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mdmax007/node-llm-cache.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "homepage": "https://github.com/mdmax007/node-llm-cache/tree/main/packages/core#readme",
13
+ "bugs": "https://github.com/mdmax007/node-llm-cache/issues",
14
+ "type": "module",
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.mjs",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.mjs",
22
+ "require": "./dist/index.cjs"
23
+ }
24
+ },
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
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.0.0",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.5.0",
38
+ "vitest": "^2.0.0",
39
+ "@vitest/coverage-v8": "^2.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }