@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.
@@ -0,0 +1,433 @@
1
+ import { ComponentIntent, CompilationResult, ComponentContract } from '@enterstellar-ai/types';
2
+
3
+ /**
4
+ * @module @enterstellar-ai/cache/types
5
+ * @description Cache-local type definitions.
6
+ *
7
+ * This file declares the `RenderCache` interface (the public API surface),
8
+ * `RenderCacheConfig` (factory configuration), `CachedRender` (cached entry
9
+ * shape), `CacheStats` (observability), and warmup-related types.
10
+ *
11
+ * **Naming:** Interface for the object with methods (`RenderCache`), types for
12
+ * data shapes (`CachedRender`, `CacheStats`, etc.) — per Design Choice T1.
13
+ *
14
+ * **L15 compliance:** Zero framework imports. This module is platform-agnostic.
15
+ *
16
+ * @see Implementation Bible §4.6
17
+ * @see Design Choices CA1–CA7
18
+ */
19
+
20
+ /**
21
+ * Configuration for `createRenderCache()`.
22
+ *
23
+ * @see Implementation Bible §4.6
24
+ * @see Design Choice CA2 — cache `CompilationResult` only.
25
+ */
26
+ type RenderCacheConfig = {
27
+ /**
28
+ * Cache eviction strategy.
29
+ * Currently only LRU is supported — future strategies may include LFU.
30
+ */
31
+ readonly strategy: 'lru';
32
+ /**
33
+ * Maximum number of entries in the cache.
34
+ * When exceeded, the least-recently-used entry is evicted.
35
+ *
36
+ * @default 1000
37
+ * @see Design Choice CA3 — global cache, no zone partitioning.
38
+ */
39
+ readonly maxEntries: number;
40
+ /**
41
+ * Time-to-live for cached entries, in seconds.
42
+ * Entries older than TTL are lazily evicted on next `get()`.
43
+ *
44
+ * @default 3600
45
+ * @see Design Choice CA4 — TTL expiry is one of four invalidation triggers.
46
+ */
47
+ readonly ttl: number;
48
+ /**
49
+ * Optional callback invoked whenever an entry is invalidated or evicted.
50
+ * Useful for DevTools Cache Dashboard integration.
51
+ *
52
+ * @param key - The cache key that was invalidated.
53
+ * @param reason - The reason for invalidation.
54
+ */
55
+ readonly onEvict?: (key: string, reason: EvictionReason) => void;
56
+ };
57
+ /**
58
+ * Reason an entry was evicted from the cache.
59
+ *
60
+ * - `'expired'` — TTL exceeded (lazy eviction on `get()`).
61
+ * - `'capacity'` — LRU eviction due to `maxEntries` limit.
62
+ * - `'manual'` — Explicitly invalidated via `invalidate()` or `invalidateAll()`.
63
+ * - `'component-update'` — Registry component update/unregister (CA5).
64
+ */
65
+ type EvictionReason = 'expired' | 'capacity' | 'manual' | 'component-update';
66
+ /**
67
+ * A cached render entry — the value stored in the cache.
68
+ *
69
+ * Contains the compiled intent and its compilation result (per CA2 — only
70
+ * `CompilationResult` is cached, NOT rendered React trees).
71
+ *
72
+ * @see Design Choice CA2 — `CompilationResult` only, not rendered React tree.
73
+ */
74
+ type CachedRender = {
75
+ /** The original compiled intent that produced this result. */
76
+ readonly compiledIntent: ComponentIntent;
77
+ /** The compilation result from the compiler pipeline. */
78
+ readonly compilationResult: CompilationResult;
79
+ /** Timestamp (ms since epoch) when this entry was cached. */
80
+ readonly cachedAt: number;
81
+ /** Timestamp (ms since epoch) when this entry expires. */
82
+ readonly expiresAt: number;
83
+ };
84
+ /**
85
+ * Cache performance statistics.
86
+ *
87
+ * Exposed via `renderCache.getStats()` for DevTools Cache Dashboard.
88
+ * Stats are cumulative since last `invalidateAll()` call.
89
+ */
90
+ type CacheStats = {
91
+ /** Total number of cache hits since last reset. */
92
+ readonly hits: number;
93
+ /** Total number of cache misses since last reset. */
94
+ readonly misses: number;
95
+ /** Current number of entries in the cache. */
96
+ readonly entries: number;
97
+ /**
98
+ * Cache hit rate as a ratio (0.0–1.0).
99
+ * Calculated as `hits / (hits + misses)`. Returns `0` if no lookups yet.
100
+ */
101
+ readonly hitRate: number;
102
+ };
103
+ /**
104
+ * A warmup entry describing a zone + intent pair to pre-compile and cache.
105
+ *
106
+ * @see Design Choice CA6 — warmup from static config + historical traces.
107
+ * @see Design Choice CA7 — async warmup, never blocking.
108
+ */
109
+ type WarmupEntry = {
110
+ /** Zone name for the cache key context. */
111
+ readonly zone: string;
112
+ /** The component intent to pre-compile and cache. */
113
+ readonly intent: ComponentIntent;
114
+ };
115
+ /**
116
+ * Compile function signature for warmup.
117
+ *
118
+ * The consumer wires this to `compiler.compile()`. The cache does NOT
119
+ * import `@enterstellar-ai/compiler` directly — dependency injection keeps the
120
+ * cache testable and avoids circular dependencies.
121
+ *
122
+ * @param intent - The `ComponentIntent` to compile.
123
+ * @returns The `CompilationResult` from the compiler pipeline.
124
+ */
125
+ type CompileFn = (intent: ComponentIntent) => Promise<CompilationResult>;
126
+ /**
127
+ * The Enterstellar Render Cache — makes GenUI feel instant.
128
+ *
129
+ * A global (non-zone-partitioned, per CA3) LRU cache that stores
130
+ * `CompilationResult` entries keyed by `intentHash + componentName` (per CA1).
131
+ * Cached entries bypass re-compilation for identical intents, dramatically
132
+ * reducing latency for repeated queries.
133
+ *
134
+ * **Factory:** Created via `createRenderCache(config)`. Returns a plain object
135
+ * with closures — no class instance, no prototype chain (per R1 pattern).
136
+ *
137
+ * **Invalidation triggers (CA4):**
138
+ * - Registry component update/unregister
139
+ * - Design token change (via `invalidateAll()`)
140
+ * - TTL expiry (lazy on `get()`)
141
+ * - Manual `invalidate()` / `invalidateAll()`
142
+ *
143
+ * @see Implementation Bible §4.6
144
+ * @see Design Choices CA1–CA7
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * import { createRenderCache, buildCacheKey } from '@enterstellar-ai/cache';
149
+ *
150
+ * const cache = createRenderCache({ maxEntries: 500, ttl: 1800 });
151
+ * const key = buildCacheKey(intentHash, componentName);
152
+ *
153
+ * // Store
154
+ * cache.set(key, { compiledIntent, compilationResult, cachedAt, expiresAt });
155
+ *
156
+ * // Retrieve
157
+ * const cached = cache.get(key); // CachedRender | undefined
158
+ *
159
+ * // Stats
160
+ * cache.getStats(); // { hits, misses, entries, hitRate }
161
+ * ```
162
+ */
163
+ interface RenderCache {
164
+ /**
165
+ * Retrieves a cached render entry by key.
166
+ *
167
+ * Returns `undefined` if the key is not found or the entry has expired.
168
+ * Expired entries are lazily evicted on access.
169
+ * A successful lookup counts as a cache hit; a miss (including expiry)
170
+ * counts as a cache miss.
171
+ *
172
+ * @param key - Cache key (from `buildCacheKey()`).
173
+ * @returns The cached render entry, or `undefined` if not found/expired.
174
+ */
175
+ get(key: string): CachedRender | undefined;
176
+ /**
177
+ * Stores a render entry in the cache.
178
+ *
179
+ * If the cache is at capacity (`maxEntries`), the least-recently-used
180
+ * entry is evicted before insertion.
181
+ *
182
+ * @param key - Cache key (from `buildCacheKey()`).
183
+ * @param render - The `CachedRender` entry to store.
184
+ */
185
+ set(key: string, render: CachedRender): void;
186
+ /**
187
+ * Invalidates (removes) a single entry by exact key.
188
+ *
189
+ * @param key - Cache key to remove.
190
+ * @returns `true` if an entry was removed, `false` if the key was not found.
191
+ */
192
+ invalidate(key: string): boolean;
193
+ /**
194
+ * Invalidates all cache entries for a specific component name.
195
+ *
196
+ * Iterates all entries and evicts those whose `compilationResult` resolved
197
+ * to the given component name. Returns the count of evicted entries.
198
+ *
199
+ * @param componentName - PascalCase component name.
200
+ * @returns The number of entries evicted.
201
+ *
202
+ * @see Design Choice CA5 — evict ALL entries for a changed component.
203
+ */
204
+ invalidateByComponent(componentName: string): number;
205
+ /**
206
+ * Clears the entire cache and resets all stats counters.
207
+ *
208
+ * @see Design Choice CA4 — manual clear is one of four invalidation triggers.
209
+ */
210
+ invalidateAll(): void;
211
+ /**
212
+ * Returns a snapshot of cache performance statistics.
213
+ *
214
+ * Stats are cumulative since the last `invalidateAll()` call.
215
+ *
216
+ * @returns A `CacheStats` object with hits, misses, entries, and hitRate.
217
+ */
218
+ getStats(): CacheStats;
219
+ /**
220
+ * Pre-warms the cache with common intents.
221
+ *
222
+ * Compiles each intent via the provided `compile` function and stores
223
+ * the result. Failures are logged and skipped — warmup never throws.
224
+ *
225
+ * Should be called after app startup using `requestIdleCallback` or
226
+ * `setTimeout(0)` as a fallback (per CA7 — never blocking).
227
+ *
228
+ * @param entries - Array of `{ zone, intent }` pairs to pre-compile.
229
+ * @param compile - Compile function (typically `compiler.compile()`).
230
+ *
231
+ * @see Design Choice CA6 — warmup from static config + historical traces.
232
+ * @see Design Choice CA7 — async warmup, never blocking.
233
+ */
234
+ warmup(entries: readonly WarmupEntry[], compile: CompileFn): Promise<void>;
235
+ /**
236
+ * Current number of entries in the cache.
237
+ */
238
+ readonly size: number;
239
+ }
240
+
241
+ /**
242
+ * @module @enterstellar-ai/cache/create-render-cache
243
+ * @description Factory function for creating an Enterstellar Render Cache.
244
+ *
245
+ * Returns a plain object with closures (per R1 — no class instance, no
246
+ * prototype chain). The cache uses an internal LRU data structure for O(1)
247
+ * get/set/eviction, with lazy TTL expiry on `get()`.
248
+ *
249
+ * **Configuration defaults:**
250
+ * - `strategy: 'lru'`
251
+ * - `maxEntries: 1000`
252
+ * - `ttl: 3600` (1 hour in seconds)
253
+ *
254
+ * **L15 compliance:** Zero framework imports. Pure TypeScript.
255
+ *
256
+ * @see Implementation Bible §4.6
257
+ * @see Design Choices CA1–CA7
258
+ */
259
+
260
+ /**
261
+ * Creates an Enterstellar Render Cache for compiled intents.
262
+ *
263
+ * The cache stores `CompilationResult` entries (per CA2 — NOT rendered React
264
+ * trees) in a global LRU (per CA3 — no zone partitioning). Cache keys are
265
+ * `intentHash + componentName` (per CA1 — NOT prop hashes).
266
+ *
267
+ * Configuration is validated with Zod at creation time (fail-fast). Invalid
268
+ * config throws `EnterstellarError` with code `ENS-3001`.
269
+ *
270
+ * @param config - Optional partial configuration. Unspecified fields use defaults.
271
+ * @returns A `RenderCache` instance (plain object with closures per R1).
272
+ * @throws {EnterstellarError} If the configuration is invalid (`ENS-3001`).
273
+ *
274
+ * @see Implementation Bible §4.6
275
+ * @see Design Choice CA1 — cache key = intentHash + componentName.
276
+ * @see Design Choice CA2 — cache `CompilationResult` only.
277
+ * @see Design Choice CA3 — global cache, no zone partitioning.
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * import { createRenderCache, buildCacheKey } from '@enterstellar-ai/cache';
282
+ *
283
+ * const cache = createRenderCache({ maxEntries: 500, ttl: 1800 });
284
+ * const key = buildCacheKey(intentHash, 'PatientVitals');
285
+ *
286
+ * cache.set(key, {
287
+ * compiledIntent: intent,
288
+ * compilationResult: result,
289
+ * cachedAt: Date.now(),
290
+ * expiresAt: Date.now() + 1800 * 1000,
291
+ * });
292
+ *
293
+ * const cached = cache.get(key); // CachedRender | undefined
294
+ * ```
295
+ */
296
+ declare function createRenderCache(config?: Partial<RenderCacheConfig>): RenderCache;
297
+
298
+ /**
299
+ * @module @enterstellar-ai/cache/cache-key
300
+ * @description Cache key construction utility.
301
+ *
302
+ * Builds deterministic cache keys from an intent hash and resolved component
303
+ * name. The key format follows Design Choice CA1: the cache key is based on
304
+ * the *decision* (which component for which intent), NOT on prop variations.
305
+ *
306
+ * **Rationale (CA1):** Hashing props misses the cache if the LLM changes a
307
+ * trivial field (timestamp, request ID). The decision to use `PatientVitals`
308
+ * for "show patient vitals" is stable regardless of prop variations.
309
+ *
310
+ * **L15 compliance:** Zero framework imports. Pure TypeScript.
311
+ *
312
+ * @see Design Choice CA1 — intent hash + resolved component name.
313
+ */
314
+ /**
315
+ * Builds a deterministic cache key from an intent hash and component name.
316
+ *
317
+ * The intent hash is typically a SHA-256 of the raw intent string (produced
318
+ * by `@enterstellar-ai/telemetry`). The component name is the PascalCase name of the
319
+ * resolved component from the registry.
320
+ *
321
+ * @param intentHash - Hash of the raw intent string (e.g., SHA-256 hex).
322
+ * @param componentName - PascalCase name of the resolved component.
323
+ * @returns A deterministic cache key string.
324
+ *
325
+ * @see Design Choice CA1 — key = intentHash + componentName, NOT props.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * import { buildCacheKey } from '@enterstellar-ai/cache';
330
+ *
331
+ * const key = buildCacheKey(
332
+ * 'a1b2c3d4e5f6...', // SHA-256 of "show patient vitals"
333
+ * 'PatientVitals',
334
+ * );
335
+ * // => "a1b2c3d4e5f6...::PatientVitals"
336
+ * ```
337
+ */
338
+ declare function buildCacheKey(intentHash: string, componentName: string): string;
339
+
340
+ /**
341
+ * @module @enterstellar-ai/cache/with-registry-invalidation
342
+ * @description Wires registry events to cache invalidation.
343
+ *
344
+ * Higher-order function that subscribes to `EnterstellarRegistry` events (`update`,
345
+ * `unregister`) and automatically calls `cache.invalidateByComponent()` for
346
+ * the affected component. New component registrations (`register` events)
347
+ * do NOT trigger invalidation — new components don't affect existing cache
348
+ * entries.
349
+ *
350
+ * **Dependency model:** `EnterstellarRegistry` is imported as a **type-only** import.
351
+ * The registry instance is injected at runtime by the consumer. There is NO
352
+ * hard dependency on `@enterstellar-ai/registry` in `package.json`. This avoids
353
+ * circular dependencies and keeps the cache self-contained.
354
+ *
355
+ * **L15 compliance:** Zero framework imports. Pure TypeScript.
356
+ *
357
+ * @see Design Choice CA4 — registry update is one of four invalidation triggers.
358
+ * @see Design Choice CA5 — evict ALL entries for a changed component.
359
+ */
360
+
361
+ /**
362
+ * Minimal interface required from an `EnterstellarRegistry` for cache invalidation.
363
+ *
364
+ * This avoids importing the full `EnterstellarRegistry` type from `@enterstellar-ai/registry`,
365
+ * keeping `@enterstellar-ai/cache` decoupled. Any object that satisfies this interface
366
+ * (including the real `EnterstellarRegistry`) can be used.
367
+ *
368
+ * @see Design Choice R18 — registry emits `register`, `unregister`, `update` events.
369
+ */
370
+ interface CacheInvalidationSource {
371
+ /**
372
+ * Subscribes to a registry event.
373
+ *
374
+ * @param event - The event type to listen for.
375
+ * @param handler - Callback receiving the affected contract.
376
+ * @returns An unsubscribe function.
377
+ */
378
+ on(event: 'register' | 'unregister' | 'update', handler: (contract: ComponentContract) => void): () => void;
379
+ }
380
+ /**
381
+ * Result of wiring registry invalidation to a cache.
382
+ * Contains the cache (unchanged) and a dispose function for cleanup.
383
+ */
384
+ type RegistryInvalidationBinding = {
385
+ /** The same `RenderCache` instance passed in (for chaining convenience). */
386
+ readonly cache: RenderCache;
387
+ /**
388
+ * Unsubscribes all registry event listeners.
389
+ * Safe to call multiple times — subsequent calls are no-ops.
390
+ */
391
+ readonly dispose: () => void;
392
+ };
393
+ /**
394
+ * Wires registry events to cache invalidation.
395
+ *
396
+ * Subscribes to `update` and `unregister` events on the provided registry
397
+ * (or any object implementing `CacheInvalidationSource`). When a component
398
+ * is updated or removed, all cache entries for that component are evicted
399
+ * (per CA5 — evict ALL entries for the changed component).
400
+ *
401
+ * The `register` event is intentionally ignored — new component registrations
402
+ * do not invalidate existing cache entries.
403
+ *
404
+ * Returns a `dispose()` function that unsubscribes all listeners. This should
405
+ * be called during app teardown or when the cache is no longer needed.
406
+ *
407
+ * @param cache - The `RenderCache` to wire invalidation to.
408
+ * @param registry - An `EnterstellarRegistry` (or compatible `CacheInvalidationSource`).
409
+ * @returns A `RegistryInvalidationBinding` with the cache and a dispose function.
410
+ *
411
+ * @see Design Choice CA4 — registry update triggers cache invalidation.
412
+ * @see Design Choice CA5 — ALL entries for a changed component are evicted.
413
+ *
414
+ * @example
415
+ * ```ts
416
+ * import { createRenderCache, withRegistryInvalidation } from '@enterstellar-ai/cache';
417
+ * import { createRegistry } from '@enterstellar-ai/registry';
418
+ *
419
+ * const cache = createRenderCache();
420
+ * const registry = createRegistry({ components: [...] });
421
+ *
422
+ * const { dispose } = withRegistryInvalidation(cache, registry);
423
+ *
424
+ * // When a component is updated in the registry, the cache auto-evicts
425
+ * // all entries for that component.
426
+ *
427
+ * // Cleanup on app teardown:
428
+ * dispose();
429
+ * ```
430
+ */
431
+ declare function withRegistryInvalidation(cache: RenderCache, registry: CacheInvalidationSource): RegistryInvalidationBinding;
432
+
433
+ export { type CacheInvalidationSource, type CacheStats, type CachedRender, type CompileFn, type EvictionReason, type RegistryInvalidationBinding, type RenderCache, type RenderCacheConfig, type WarmupEntry, buildCacheKey, createRenderCache, withRegistryInvalidation };