@enterstellar-ai/cache 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/dist/index.js ADDED
@@ -0,0 +1,368 @@
1
+ import { z } from 'zod';
2
+ import { EnterstellarError } from '@enterstellar-ai/types';
3
+
4
+ // src/create-render-cache.ts
5
+
6
+ // src/cache-key.ts
7
+ var CACHE_KEY_SEPARATOR = "::";
8
+ function buildCacheKey(intentHash, componentName) {
9
+ return `${intentHash}${CACHE_KEY_SEPARATOR}${componentName}`;
10
+ }
11
+ function extractComponentName(cacheKey) {
12
+ const separatorIndex = cacheKey.indexOf(CACHE_KEY_SEPARATOR);
13
+ if (separatorIndex === -1) {
14
+ return void 0;
15
+ }
16
+ return cacheKey.substring(separatorIndex + CACHE_KEY_SEPARATOR.length);
17
+ }
18
+
19
+ // src/lru-cache.ts
20
+ var LRUCache = class {
21
+ /** Maximum number of entries before LRU eviction. */
22
+ capacity;
23
+ /** Key → Node lookup map for O(1) access. */
24
+ map;
25
+ /** Most recently used node. */
26
+ head;
27
+ /** Least recently used node (eviction candidate). */
28
+ tail;
29
+ /** Optional callback invoked on eviction. */
30
+ onEvict;
31
+ /**
32
+ * Creates a new LRU cache.
33
+ *
34
+ * @param capacity - Maximum number of entries. Must be ≥ 1.
35
+ * @param onEvict - Optional callback invoked when an entry is evicted.
36
+ */
37
+ constructor(capacity, onEvict) {
38
+ this.capacity = capacity;
39
+ this.map = /* @__PURE__ */ new Map();
40
+ this.head = void 0;
41
+ this.tail = void 0;
42
+ this.onEvict = onEvict;
43
+ }
44
+ /**
45
+ * Retrieves the value for a key and promotes it to most-recently-used.
46
+ *
47
+ * @param key - The cache key.
48
+ * @returns The cached value, or `undefined` if not found.
49
+ */
50
+ get(key) {
51
+ const node = this.map.get(key);
52
+ if (node === void 0) {
53
+ return void 0;
54
+ }
55
+ this.moveToHead(node);
56
+ return node.value;
57
+ }
58
+ /**
59
+ * Stores or updates a key-value pair. Promotes to most-recently-used.
60
+ *
61
+ * If the key already exists, its value is updated in-place.
62
+ * If the cache is at capacity, the least-recently-used entry is evicted.
63
+ *
64
+ * @param key - The cache key.
65
+ * @param value - The value to store.
66
+ */
67
+ set(key, value) {
68
+ const existing = this.map.get(key);
69
+ if (existing !== void 0) {
70
+ existing.value = value;
71
+ this.moveToHead(existing);
72
+ return;
73
+ }
74
+ const node = {
75
+ key,
76
+ value,
77
+ prev: void 0,
78
+ next: void 0
79
+ };
80
+ this.map.set(key, node);
81
+ this.addToHead(node);
82
+ if (this.map.size > this.capacity) {
83
+ this.evictTail();
84
+ }
85
+ }
86
+ /**
87
+ * Deletes an entry by key.
88
+ *
89
+ * @param key - The cache key to delete.
90
+ * @returns `true` if the entry was found and deleted, `false` otherwise.
91
+ */
92
+ delete(key) {
93
+ const node = this.map.get(key);
94
+ if (node === void 0) {
95
+ return false;
96
+ }
97
+ this.removeNode(node);
98
+ this.map.delete(key);
99
+ return true;
100
+ }
101
+ /**
102
+ * Checks whether a key exists in the cache.
103
+ * Does NOT promote the entry (peek semantics).
104
+ *
105
+ * @param key - The cache key to check.
106
+ * @returns `true` if the key exists.
107
+ */
108
+ has(key) {
109
+ return this.map.has(key);
110
+ }
111
+ /**
112
+ * Clears all entries from the cache.
113
+ * Does NOT invoke the eviction callback for cleared entries.
114
+ */
115
+ clear() {
116
+ this.map.clear();
117
+ this.head = void 0;
118
+ this.tail = void 0;
119
+ }
120
+ /**
121
+ * Returns the current number of entries in the cache.
122
+ */
123
+ get size() {
124
+ return this.map.size;
125
+ }
126
+ /**
127
+ * Iterates over all entries in access order (most recent first).
128
+ * The callback receives the key and value for each entry.
129
+ *
130
+ * @param callback - Function called for each entry.
131
+ */
132
+ forEach(callback) {
133
+ let current = this.head;
134
+ while (current !== void 0) {
135
+ callback(current.key, current.value);
136
+ current = current.next;
137
+ }
138
+ }
139
+ /**
140
+ * Returns all keys in access order (most recent first).
141
+ *
142
+ * @returns Array of cache keys.
143
+ */
144
+ keys() {
145
+ const result = [];
146
+ let current = this.head;
147
+ while (current !== void 0) {
148
+ result.push(current.key);
149
+ current = current.next;
150
+ }
151
+ return result;
152
+ }
153
+ // -----------------------------------------------------------------------
154
+ // Private: Linked List Operations
155
+ // -----------------------------------------------------------------------
156
+ /**
157
+ * Adds a node to the head of the linked list (most recently used position).
158
+ */
159
+ addToHead(node) {
160
+ node.prev = void 0;
161
+ node.next = this.head;
162
+ if (this.head !== void 0) {
163
+ this.head.prev = node;
164
+ }
165
+ this.head = node;
166
+ this.tail ??= node;
167
+ }
168
+ /**
169
+ * Removes a node from its current position in the linked list.
170
+ */
171
+ removeNode(node) {
172
+ if (node.prev !== void 0) {
173
+ node.prev.next = node.next;
174
+ } else {
175
+ this.head = node.next;
176
+ }
177
+ if (node.next !== void 0) {
178
+ node.next.prev = node.prev;
179
+ } else {
180
+ this.tail = node.prev;
181
+ }
182
+ node.prev = void 0;
183
+ node.next = void 0;
184
+ }
185
+ /**
186
+ * Moves an existing node to the head (most recently used position).
187
+ */
188
+ moveToHead(node) {
189
+ if (node === this.head) {
190
+ return;
191
+ }
192
+ this.removeNode(node);
193
+ this.addToHead(node);
194
+ }
195
+ /**
196
+ * Evicts the tail node (least recently used) and invokes the callback.
197
+ */
198
+ evictTail() {
199
+ if (this.tail === void 0) {
200
+ return;
201
+ }
202
+ const evicted = this.tail;
203
+ this.removeNode(evicted);
204
+ this.map.delete(evicted.key);
205
+ if (this.onEvict !== void 0) {
206
+ this.onEvict(evicted.key, evicted.value);
207
+ }
208
+ }
209
+ };
210
+
211
+ // src/create-render-cache.ts
212
+ var RenderCacheConfigSchema = z.object({
213
+ strategy: z.literal("lru"),
214
+ maxEntries: z.number().int("maxEntries must be an integer.").min(1, "maxEntries must be at least 1."),
215
+ ttl: z.number().int("ttl must be an integer.").min(1, "ttl must be at least 1 second.")
216
+ });
217
+ var DEFAULT_CONFIG = {
218
+ strategy: "lru",
219
+ maxEntries: 1e3,
220
+ ttl: 3600
221
+ };
222
+ function createRenderCache(config) {
223
+ const merged = {
224
+ ...DEFAULT_CONFIG,
225
+ ...config
226
+ };
227
+ const parseResult = RenderCacheConfigSchema.safeParse(merged);
228
+ if (!parseResult.success) {
229
+ const firstIssue = parseResult.error.issues[0];
230
+ const message = firstIssue !== void 0 ? `Invalid RenderCache config: ${firstIssue.message}` : "Invalid RenderCache config.";
231
+ throw new EnterstellarError(
232
+ "ENS-3001",
233
+ "cache",
234
+ message,
235
+ false
236
+ // Not recoverable — dev error
237
+ );
238
+ }
239
+ const resolvedConfig = merged;
240
+ let hits = 0;
241
+ let misses = 0;
242
+ const lru = new LRUCache(
243
+ resolvedConfig.maxEntries,
244
+ (key, _value) => {
245
+ if (resolvedConfig.onEvict !== void 0) {
246
+ resolvedConfig.onEvict(key, "capacity");
247
+ }
248
+ }
249
+ );
250
+ function isExpired(entry) {
251
+ return Date.now() >= entry.expiresAt;
252
+ }
253
+ function notifyEvict(key, reason) {
254
+ if (resolvedConfig.onEvict !== void 0) {
255
+ resolvedConfig.onEvict(key, reason);
256
+ }
257
+ }
258
+ const renderCache = {
259
+ get(key) {
260
+ const entry = lru.get(key);
261
+ if (entry === void 0) {
262
+ misses++;
263
+ return void 0;
264
+ }
265
+ if (isExpired(entry)) {
266
+ lru.delete(key);
267
+ misses++;
268
+ notifyEvict(key, "expired");
269
+ return void 0;
270
+ }
271
+ hits++;
272
+ return entry;
273
+ },
274
+ set(key, render) {
275
+ lru.set(key, render);
276
+ },
277
+ invalidate(key) {
278
+ const deleted = lru.delete(key);
279
+ if (deleted) {
280
+ notifyEvict(key, "manual");
281
+ }
282
+ return deleted;
283
+ },
284
+ invalidateByComponent(componentName) {
285
+ const keysToEvict = [];
286
+ lru.forEach((key, value) => {
287
+ const nameFromKey = extractComponentName(key);
288
+ if (nameFromKey === componentName) {
289
+ keysToEvict.push(key);
290
+ return;
291
+ }
292
+ if (value.compilationResult.componentName === componentName) {
293
+ keysToEvict.push(key);
294
+ }
295
+ });
296
+ for (const key of keysToEvict) {
297
+ lru.delete(key);
298
+ notifyEvict(key, "component-update");
299
+ }
300
+ return keysToEvict.length;
301
+ },
302
+ invalidateAll() {
303
+ lru.clear();
304
+ hits = 0;
305
+ misses = 0;
306
+ if (resolvedConfig.onEvict !== void 0) {
307
+ resolvedConfig.onEvict("*", "manual");
308
+ }
309
+ },
310
+ getStats() {
311
+ const total = hits + misses;
312
+ return {
313
+ hits,
314
+ misses,
315
+ entries: lru.size,
316
+ hitRate: total === 0 ? 0 : hits / total
317
+ };
318
+ },
319
+ async warmup(entries, compile) {
320
+ for (const entry of entries) {
321
+ try {
322
+ const compilationResult = await compile(entry.intent);
323
+ if (compilationResult.status === "fail") {
324
+ continue;
325
+ }
326
+ const now = Date.now();
327
+ const cachedRender = {
328
+ compiledIntent: entry.intent,
329
+ compilationResult,
330
+ cachedAt: now,
331
+ expiresAt: now + resolvedConfig.ttl * 1e3
332
+ };
333
+ const key = buildCacheKey(entry.intent.component, compilationResult.componentName);
334
+ lru.set(key, cachedRender);
335
+ } catch {
336
+ continue;
337
+ }
338
+ }
339
+ },
340
+ get size() {
341
+ return lru.size;
342
+ }
343
+ };
344
+ return renderCache;
345
+ }
346
+
347
+ // src/with-registry-invalidation.ts
348
+ function withRegistryInvalidation(cache, registry) {
349
+ let disposed = false;
350
+ const handleInvalidation = (contract) => {
351
+ cache.invalidateByComponent(contract.name);
352
+ };
353
+ const unsubUpdate = registry.on("update", handleInvalidation);
354
+ const unsubUnregister = registry.on("unregister", handleInvalidation);
355
+ const dispose = () => {
356
+ if (disposed) {
357
+ return;
358
+ }
359
+ disposed = true;
360
+ unsubUpdate();
361
+ unsubUnregister();
362
+ };
363
+ return { cache, dispose };
364
+ }
365
+
366
+ export { buildCacheKey, createRenderCache, withRegistryInvalidation };
367
+ //# sourceMappingURL=index.js.map
368
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cache-key.ts","../src/lru-cache.ts","../src/create-render-cache.ts","../src/with-registry-invalidation.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @module @enterstellar-ai/cache/cache-key\n * @description Cache key construction utility.\n *\n * Builds deterministic cache keys from an intent hash and resolved component\n * name. The key format follows Design Choice CA1: the cache key is based on\n * the *decision* (which component for which intent), NOT on prop variations.\n *\n * **Rationale (CA1):** Hashing props misses the cache if the LLM changes a\n * trivial field (timestamp, request ID). The decision to use `PatientVitals`\n * for \"show patient vitals\" is stable regardless of prop variations.\n *\n * **L15 compliance:** Zero framework imports. Pure TypeScript.\n *\n * @see Design Choice CA1 — intent hash + resolved component name.\n */\n\n// ---------------------------------------------------------------------------\n// Key Separator\n// ---------------------------------------------------------------------------\n\n/**\n * Separator between intent hash and component name in the cache key.\n * Using `::` ensures no collision with PascalCase names or hex hashes.\n *\n * @internal\n */\nconst CACHE_KEY_SEPARATOR = '::';\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Builds a deterministic cache key from an intent hash and component name.\n *\n * The intent hash is typically a SHA-256 of the raw intent string (produced\n * by `@enterstellar-ai/telemetry`). The component name is the PascalCase name of the\n * resolved component from the registry.\n *\n * @param intentHash - Hash of the raw intent string (e.g., SHA-256 hex).\n * @param componentName - PascalCase name of the resolved component.\n * @returns A deterministic cache key string.\n *\n * @see Design Choice CA1 — key = intentHash + componentName, NOT props.\n *\n * @example\n * ```ts\n * import { buildCacheKey } from '@enterstellar-ai/cache';\n *\n * const key = buildCacheKey(\n * 'a1b2c3d4e5f6...', // SHA-256 of \"show patient vitals\"\n * 'PatientVitals',\n * );\n * // => \"a1b2c3d4e5f6...::PatientVitals\"\n * ```\n */\nexport function buildCacheKey(\n intentHash: string,\n componentName: string,\n): string {\n return `${intentHash}${CACHE_KEY_SEPARATOR}${componentName}`;\n}\n\n/**\n * Extracts the component name from a cache key.\n *\n * Useful for `invalidateByComponent()` — allows scanning cache keys\n * without parsing the full `CachedRender` value.\n *\n * @param cacheKey - A key previously produced by `buildCacheKey()`.\n * @returns The component name portion, or `undefined` if the key format\n * is invalid (no separator found).\n *\n * @internal\n */\nexport function extractComponentName(cacheKey: string): string | undefined {\n const separatorIndex = cacheKey.indexOf(CACHE_KEY_SEPARATOR);\n if (separatorIndex === -1) {\n return undefined;\n }\n return cacheKey.substring(separatorIndex + CACHE_KEY_SEPARATOR.length);\n}\n","/**\n * @module @enterstellar-ai/cache/lru-cache\n * @description Internal LRU (Least Recently Used) cache data structure.\n *\n * Implements a doubly-linked list + `Map` for O(1) get, set, delete, and\n * eviction operations. This is an internal module — NOT exported from the\n * `@enterstellar-ai/cache` barrel.\n *\n * **Why custom:** The LRU is ~100 lines. Avoids an external dependency\n * (`lru-cache` npm), keeps the package zero-dep (excluding peer deps), and\n * gives full control over the eviction callback (needed for stats tracking\n * and DevTools integration).\n *\n * **L15 compliance:** Zero framework imports. Pure TypeScript.\n *\n * @internal\n */\n\n// ---------------------------------------------------------------------------\n// Linked List Node\n// ---------------------------------------------------------------------------\n\n/**\n * A node in the doubly-linked list.\n * Each node holds a key-value pair and pointers to its neighbours.\n *\n * @internal\n */\ntype LRUNode<T> = {\n /** Cache key for reverse-lookup during eviction. */\n readonly key: string;\n /** The cached value. */\n value: T;\n /** Pointer to the more-recently-used node (towards head). */\n prev: LRUNode<T> | undefined;\n /** Pointer to the less-recently-used node (towards tail). */\n next: LRUNode<T> | undefined;\n};\n\n// ---------------------------------------------------------------------------\n// Eviction Callback\n// ---------------------------------------------------------------------------\n\n/**\n * Callback invoked when an entry is evicted from the LRU.\n * Receives the evicted key and value.\n *\n * @internal\n */\nexport type OnEvictCallback<T> = (key: string, value: T) => void;\n\n// ---------------------------------------------------------------------------\n// LRU Cache Class\n// ---------------------------------------------------------------------------\n\n/**\n * A generic LRU cache with O(1) get, set, and delete operations.\n *\n * **Data structure:** Doubly-linked list (access order) + `Map` (key lookup).\n * - Head = most recently used\n * - Tail = least recently used (eviction candidate)\n *\n * @typeParam T - The type of values stored in the cache.\n *\n * @internal\n */\nexport class LRUCache<T> {\n /** Maximum number of entries before LRU eviction. */\n private readonly capacity: number;\n\n /** Key → Node lookup map for O(1) access. */\n private readonly map: Map<string, LRUNode<T>>;\n\n /** Most recently used node. */\n private head: LRUNode<T> | undefined;\n\n /** Least recently used node (eviction candidate). */\n private tail: LRUNode<T> | undefined;\n\n /** Optional callback invoked on eviction. */\n private readonly onEvict: OnEvictCallback<T> | undefined;\n\n /**\n * Creates a new LRU cache.\n *\n * @param capacity - Maximum number of entries. Must be ≥ 1.\n * @param onEvict - Optional callback invoked when an entry is evicted.\n */\n constructor(capacity: number, onEvict?: OnEvictCallback<T>) {\n this.capacity = capacity;\n this.map = new Map<string, LRUNode<T>>();\n this.head = undefined;\n this.tail = undefined;\n this.onEvict = onEvict;\n }\n\n /**\n * Retrieves the value for a key and promotes it to most-recently-used.\n *\n * @param key - The cache key.\n * @returns The cached value, or `undefined` if not found.\n */\n get(key: string): T | undefined {\n const node = this.map.get(key);\n if (node === undefined) {\n return undefined;\n }\n // Promote to head (most recently used)\n this.moveToHead(node);\n return node.value;\n }\n\n /**\n * Stores or updates a key-value pair. Promotes to most-recently-used.\n *\n * If the key already exists, its value is updated in-place.\n * If the cache is at capacity, the least-recently-used entry is evicted.\n *\n * @param key - The cache key.\n * @param value - The value to store.\n */\n set(key: string, value: T): void {\n const existing = this.map.get(key);\n\n if (existing !== undefined) {\n // Update existing entry and promote\n existing.value = value;\n this.moveToHead(existing);\n return;\n }\n\n // Create new node\n const node: LRUNode<T> = {\n key,\n value,\n prev: undefined,\n next: undefined,\n };\n\n // Add to map and promote to head\n this.map.set(key, node);\n this.addToHead(node);\n\n // Evict tail if over capacity\n if (this.map.size > this.capacity) {\n this.evictTail();\n }\n }\n\n /**\n * Deletes an entry by key.\n *\n * @param key - The cache key to delete.\n * @returns `true` if the entry was found and deleted, `false` otherwise.\n */\n delete(key: string): boolean {\n const node = this.map.get(key);\n if (node === undefined) {\n return false;\n }\n this.removeNode(node);\n this.map.delete(key);\n return true;\n }\n\n /**\n * Checks whether a key exists in the cache.\n * Does NOT promote the entry (peek semantics).\n *\n * @param key - The cache key to check.\n * @returns `true` if the key exists.\n */\n has(key: string): boolean {\n return this.map.has(key);\n }\n\n /**\n * Clears all entries from the cache.\n * Does NOT invoke the eviction callback for cleared entries.\n */\n clear(): void {\n this.map.clear();\n this.head = undefined;\n this.tail = undefined;\n }\n\n /**\n * Returns the current number of entries in the cache.\n */\n get size(): number {\n return this.map.size;\n }\n\n /**\n * Iterates over all entries in access order (most recent first).\n * The callback receives the key and value for each entry.\n *\n * @param callback - Function called for each entry.\n */\n forEach(callback: (key: string, value: T) => void): void {\n let current = this.head;\n while (current !== undefined) {\n callback(current.key, current.value);\n current = current.next;\n }\n }\n\n /**\n * Returns all keys in access order (most recent first).\n *\n * @returns Array of cache keys.\n */\n keys(): string[] {\n const result: string[] = [];\n let current = this.head;\n while (current !== undefined) {\n result.push(current.key);\n current = current.next;\n }\n return result;\n }\n\n // -----------------------------------------------------------------------\n // Private: Linked List Operations\n // -----------------------------------------------------------------------\n\n /**\n * Adds a node to the head of the linked list (most recently used position).\n */\n private addToHead(node: LRUNode<T>): void {\n node.prev = undefined;\n node.next = this.head;\n\n if (this.head !== undefined) {\n this.head.prev = node;\n }\n\n this.head = node;\n\n this.tail ??= node;\n }\n\n /**\n * Removes a node from its current position in the linked list.\n */\n private removeNode(node: LRUNode<T>): void {\n if (node.prev !== undefined) {\n node.prev.next = node.next;\n } else {\n // Node is head\n this.head = node.next;\n }\n\n if (node.next !== undefined) {\n node.next.prev = node.prev;\n } else {\n // Node is tail\n this.tail = node.prev;\n }\n\n node.prev = undefined;\n node.next = undefined;\n }\n\n /**\n * Moves an existing node to the head (most recently used position).\n */\n private moveToHead(node: LRUNode<T>): void {\n if (node === this.head) {\n return; // Already at head\n }\n this.removeNode(node);\n this.addToHead(node);\n }\n\n /**\n * Evicts the tail node (least recently used) and invokes the callback.\n */\n private evictTail(): void {\n if (this.tail === undefined) {\n return;\n }\n\n const evicted = this.tail;\n this.removeNode(evicted);\n this.map.delete(evicted.key);\n\n if (this.onEvict !== undefined) {\n this.onEvict(evicted.key, evicted.value);\n }\n }\n}\n","/**\n * @module @enterstellar-ai/cache/create-render-cache\n * @description Factory function for creating an Enterstellar Render Cache.\n *\n * Returns a plain object with closures (per R1 — no class instance, no\n * prototype chain). The cache uses an internal LRU data structure for O(1)\n * get/set/eviction, with lazy TTL expiry on `get()`.\n *\n * **Configuration defaults:**\n * - `strategy: 'lru'`\n * - `maxEntries: 1000`\n * - `ttl: 3600` (1 hour in seconds)\n *\n * **L15 compliance:** Zero framework imports. Pure TypeScript.\n *\n * @see Implementation Bible §4.6\n * @see Design Choices CA1–CA7\n */\n\nimport { z } from 'zod';\n\nimport { EnterstellarError } from '@enterstellar-ai/types';\nimport type { CompilationResult } from '@enterstellar-ai/types';\n\nimport { buildCacheKey, extractComponentName } from './cache-key.js';\nimport { LRUCache } from './lru-cache.js';\nimport type {\n CachedRender,\n CacheStats,\n CompileFn,\n EvictionReason,\n RenderCache,\n RenderCacheConfig,\n WarmupEntry,\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Config Schema (Zod validation at factory creation time)\n// ---------------------------------------------------------------------------\n\n/**\n * Zod schema for validating the data fields of `RenderCacheConfig`.\n *\n * **Note:** The `onEvict` callback is NOT validated by Zod — Zod's\n * `z.function()` produces incompatible types under `exactOptionalPropertyTypes`.\n * The callback is already type-safe at the TypeScript level; Zod validates\n * only the serializable data fields (strategy, maxEntries, ttl).\n *\n * @internal\n */\nconst RenderCacheConfigSchema = z.object({\n strategy: z.literal('lru'),\n maxEntries: z\n .number()\n .int('maxEntries must be an integer.')\n .min(1, 'maxEntries must be at least 1.'),\n ttl: z\n .number()\n .int('ttl must be an integer.')\n .min(1, 'ttl must be at least 1 second.'),\n});\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\n/**\n * Default configuration values for the render cache.\n *\n * @internal\n */\nconst DEFAULT_CONFIG: RenderCacheConfig = {\n strategy: 'lru',\n maxEntries: 1000,\n ttl: 3600,\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an Enterstellar Render Cache for compiled intents.\n *\n * The cache stores `CompilationResult` entries (per CA2 — NOT rendered React\n * trees) in a global LRU (per CA3 — no zone partitioning). Cache keys are\n * `intentHash + componentName` (per CA1 — NOT prop hashes).\n *\n * Configuration is validated with Zod at creation time (fail-fast). Invalid\n * config throws `EnterstellarError` with code `ENS-3001`.\n *\n * @param config - Optional partial configuration. Unspecified fields use defaults.\n * @returns A `RenderCache` instance (plain object with closures per R1).\n * @throws {EnterstellarError} If the configuration is invalid (`ENS-3001`).\n *\n * @see Implementation Bible §4.6\n * @see Design Choice CA1 — cache key = intentHash + componentName.\n * @see Design Choice CA2 — cache `CompilationResult` only.\n * @see Design Choice CA3 — global cache, no zone partitioning.\n *\n * @example\n * ```ts\n * import { createRenderCache, buildCacheKey } from '@enterstellar-ai/cache';\n *\n * const cache = createRenderCache({ maxEntries: 500, ttl: 1800 });\n * const key = buildCacheKey(intentHash, 'PatientVitals');\n *\n * cache.set(key, {\n * compiledIntent: intent,\n * compilationResult: result,\n * cachedAt: Date.now(),\n * expiresAt: Date.now() + 1800 * 1000,\n * });\n *\n * const cached = cache.get(key); // CachedRender | undefined\n * ```\n */\nexport function createRenderCache(config?: Partial<RenderCacheConfig>): RenderCache {\n // -----------------------------------------------------------------------\n // Configuration validation (fail-fast per R5 pattern)\n // -----------------------------------------------------------------------\n const merged: RenderCacheConfig = {\n ...DEFAULT_CONFIG,\n ...config,\n };\n\n // Validate serializable data fields via Zod (fail-fast per R5)\n const parseResult = RenderCacheConfigSchema.safeParse(merged);\n if (!parseResult.success) {\n const firstIssue = parseResult.error.issues[0];\n const message = firstIssue !== undefined\n ? `Invalid RenderCache config: ${firstIssue.message}`\n : 'Invalid RenderCache config.';\n throw new EnterstellarError(\n 'ENS-3001',\n 'cache',\n message,\n false, // Not recoverable — dev error\n );\n }\n\n // Use merged config (includes onEvict callback, which Zod doesn't validate)\n const resolvedConfig = merged;\n\n // -----------------------------------------------------------------------\n // Internal state\n // -----------------------------------------------------------------------\n\n /** Cache hit counter. */\n let hits = 0;\n /** Cache miss counter. */\n let misses = 0;\n\n /**\n * LRU cache instance with eviction callback for stats tracking.\n * The eviction callback forwards to the consumer's `onEvict` hook.\n */\n const lru = new LRUCache<CachedRender>(\n resolvedConfig.maxEntries,\n (key: string, _value: CachedRender) => {\n if (resolvedConfig.onEvict !== undefined) {\n resolvedConfig.onEvict(key, 'capacity');\n }\n },\n );\n\n // -----------------------------------------------------------------------\n // Helper: TTL check\n // -----------------------------------------------------------------------\n\n /**\n * Checks if a cached entry has expired.\n *\n * @param entry - The cached render entry.\n * @returns `true` if the entry's `expiresAt` has passed.\n */\n function isExpired(entry: CachedRender): boolean {\n return Date.now() >= entry.expiresAt;\n }\n\n // -----------------------------------------------------------------------\n // Helper: Notify eviction\n // -----------------------------------------------------------------------\n\n /**\n * Notifies the consumer's `onEvict` callback if configured.\n *\n * @param key - The evicted cache key.\n * @param reason - Why the entry was evicted.\n */\n function notifyEvict(key: string, reason: EvictionReason): void {\n if (resolvedConfig.onEvict !== undefined) {\n resolvedConfig.onEvict(key, reason);\n }\n }\n\n // -----------------------------------------------------------------------\n // RenderCache implementation (plain object with closures per R1)\n // -----------------------------------------------------------------------\n\n const renderCache: RenderCache = {\n get(key: string): CachedRender | undefined {\n const entry = lru.get(key);\n\n if (entry === undefined) {\n misses++;\n return undefined;\n }\n\n // Lazy TTL expiry (CA4)\n if (isExpired(entry)) {\n lru.delete(key);\n misses++;\n notifyEvict(key, 'expired');\n return undefined;\n }\n\n hits++;\n return entry;\n },\n\n set(key: string, render: CachedRender): void {\n lru.set(key, render);\n },\n\n invalidate(key: string): boolean {\n const deleted = lru.delete(key);\n if (deleted) {\n notifyEvict(key, 'manual');\n }\n return deleted;\n },\n\n invalidateByComponent(componentName: string): number {\n // Collect keys to evict (cannot modify LRU during iteration)\n const keysToEvict: string[] = [];\n\n lru.forEach((key: string, value: CachedRender) => {\n // Fast path: extract component name from key directly\n const nameFromKey = extractComponentName(key);\n if (nameFromKey === componentName) {\n keysToEvict.push(key);\n return;\n }\n\n // Fallback: check the compilationResult for the component name\n if (value.compilationResult.componentName === componentName) {\n keysToEvict.push(key);\n }\n });\n\n // Evict collected keys\n for (const key of keysToEvict) {\n lru.delete(key);\n notifyEvict(key, 'component-update');\n }\n\n return keysToEvict.length;\n },\n\n invalidateAll(): void {\n lru.clear();\n hits = 0;\n misses = 0;\n if (resolvedConfig.onEvict !== undefined) {\n resolvedConfig.onEvict('*', 'manual');\n }\n },\n\n getStats(): CacheStats {\n const total = hits + misses;\n return {\n hits,\n misses,\n entries: lru.size,\n hitRate: total === 0 ? 0 : hits / total,\n };\n },\n\n async warmup(entries: readonly WarmupEntry[], compile: CompileFn): Promise<void> {\n for (const entry of entries) {\n try {\n const compilationResult: CompilationResult = await compile(entry.intent);\n\n // Only cache successful compilations\n if (compilationResult.status === 'fail') {\n continue;\n }\n\n const now = Date.now();\n const cachedRender: CachedRender = {\n compiledIntent: entry.intent,\n compilationResult,\n cachedAt: now,\n expiresAt: now + resolvedConfig.ttl * 1000,\n };\n\n // Build cache key via buildCacheKey() for consistent format\n // with runtime lookups (CA1). During warmup, the intent's\n // component name serves as the intentHash stand-in — this\n // is the same value Zone uses for cache key construction\n // when the intent arrives for the first time.\n const key = buildCacheKey(entry.intent.component, compilationResult.componentName);\n lru.set(key, cachedRender);\n } catch {\n // Warmup failures are silently skipped (CA7 — never blocking).\n // The warmup is best-effort: if a compile call fails,\n // we continue with the next entry.\n continue;\n }\n }\n },\n\n get size(): number {\n return lru.size;\n },\n };\n\n return renderCache;\n}\n","/**\n * @module @enterstellar-ai/cache/with-registry-invalidation\n * @description Wires registry events to cache invalidation.\n *\n * Higher-order function that subscribes to `EnterstellarRegistry` events (`update`,\n * `unregister`) and automatically calls `cache.invalidateByComponent()` for\n * the affected component. New component registrations (`register` events)\n * do NOT trigger invalidation — new components don't affect existing cache\n * entries.\n *\n * **Dependency model:** `EnterstellarRegistry` is imported as a **type-only** import.\n * The registry instance is injected at runtime by the consumer. There is NO\n * hard dependency on `@enterstellar-ai/registry` in `package.json`. This avoids\n * circular dependencies and keeps the cache self-contained.\n *\n * **L15 compliance:** Zero framework imports. Pure TypeScript.\n *\n * @see Design Choice CA4 — registry update is one of four invalidation triggers.\n * @see Design Choice CA5 — evict ALL entries for a changed component.\n */\n\nimport type { ComponentContract } from '@enterstellar-ai/types';\n\nimport type { RenderCache } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Registry Shim Interface\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal interface required from an `EnterstellarRegistry` for cache invalidation.\n *\n * This avoids importing the full `EnterstellarRegistry` type from `@enterstellar-ai/registry`,\n * keeping `@enterstellar-ai/cache` decoupled. Any object that satisfies this interface\n * (including the real `EnterstellarRegistry`) can be used.\n *\n * @see Design Choice R18 — registry emits `register`, `unregister`, `update` events.\n */\nexport interface CacheInvalidationSource {\n /**\n * Subscribes to a registry event.\n *\n * @param event - The event type to listen for.\n * @param handler - Callback receiving the affected contract.\n * @returns An unsubscribe function.\n */\n on(\n event: 'register' | 'unregister' | 'update',\n handler: (contract: ComponentContract) => void,\n ): () => void;\n}\n\n// ---------------------------------------------------------------------------\n// Result Type\n// ---------------------------------------------------------------------------\n\n/**\n * Result of wiring registry invalidation to a cache.\n * Contains the cache (unchanged) and a dispose function for cleanup.\n */\nexport type RegistryInvalidationBinding = {\n /** The same `RenderCache` instance passed in (for chaining convenience). */\n readonly cache: RenderCache;\n\n /**\n * Unsubscribes all registry event listeners.\n * Safe to call multiple times — subsequent calls are no-ops.\n */\n readonly dispose: () => void;\n};\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Wires registry events to cache invalidation.\n *\n * Subscribes to `update` and `unregister` events on the provided registry\n * (or any object implementing `CacheInvalidationSource`). When a component\n * is updated or removed, all cache entries for that component are evicted\n * (per CA5 — evict ALL entries for the changed component).\n *\n * The `register` event is intentionally ignored — new component registrations\n * do not invalidate existing cache entries.\n *\n * Returns a `dispose()` function that unsubscribes all listeners. This should\n * be called during app teardown or when the cache is no longer needed.\n *\n * @param cache - The `RenderCache` to wire invalidation to.\n * @param registry - An `EnterstellarRegistry` (or compatible `CacheInvalidationSource`).\n * @returns A `RegistryInvalidationBinding` with the cache and a dispose function.\n *\n * @see Design Choice CA4 — registry update triggers cache invalidation.\n * @see Design Choice CA5 — ALL entries for a changed component are evicted.\n *\n * @example\n * ```ts\n * import { createRenderCache, withRegistryInvalidation } from '@enterstellar-ai/cache';\n * import { createRegistry } from '@enterstellar-ai/registry';\n *\n * const cache = createRenderCache();\n * const registry = createRegistry({ components: [...] });\n *\n * const { dispose } = withRegistryInvalidation(cache, registry);\n *\n * // When a component is updated in the registry, the cache auto-evicts\n * // all entries for that component.\n *\n * // Cleanup on app teardown:\n * dispose();\n * ```\n */\nexport function withRegistryInvalidation(\n cache: RenderCache,\n registry: CacheInvalidationSource,\n): RegistryInvalidationBinding {\n /** Whether dispose has already been called. */\n let disposed = false;\n\n /**\n * Handler for `update` and `unregister` events.\n * Evicts all cache entries for the affected component.\n *\n * @param contract - The component contract that was updated or removed.\n */\n const handleInvalidation = (contract: ComponentContract): void => {\n cache.invalidateByComponent(contract.name);\n };\n\n // Subscribe to invalidation-triggering events\n const unsubUpdate = registry.on('update', handleInvalidation);\n const unsubUnregister = registry.on('unregister', handleInvalidation);\n\n /**\n * Unsubscribes all event listeners.\n * Safe to call multiple times — subsequent calls are no-ops.\n */\n const dispose = (): void => {\n if (disposed) {\n return;\n }\n disposed = true;\n unsubUpdate();\n unsubUnregister();\n };\n\n return { cache, dispose };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@enterstellar-ai/cache",
3
+ "description": "LRU render cache for compiled intents — deterministic zone acceleration.",
4
+ "version": "0.1.0",
5
+ "author": "Enterstellar",
6
+ "license": "Apache-2.0",
7
+ "homepage": "https://enterstellar.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/enterstellar-ai/enterstellar.git",
11
+ "directory": "packages/cache"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/enterstellar-ai/enterstellar/issues"
15
+ },
16
+ "keywords": [
17
+ "cache",
18
+ "lru",
19
+ "render-cache",
20
+ "generative-ui",
21
+ "enterstellar"
22
+ ],
23
+ "type": "module",
24
+ "private": false,
25
+ "sideEffects": false,
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ }
43
+ },
44
+ "files": [
45
+ "LICENSE",
46
+ "NOTICE",
47
+ "dist"
48
+ ],
49
+ "peerDependencies": {
50
+ "zod": "^4.3.6",
51
+ "@enterstellar-ai/types": "0.1.0"
52
+ },
53
+ "devDependencies": {
54
+ "zod": "^4.3.6",
55
+ "@enterstellar-ai/types": "0.1.0"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup",
59
+ "typecheck": "tsc --noEmit",
60
+ "lint": "eslint src/",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest"
63
+ }
64
+ }