ai-functions 2.1.1 → 2.3.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/.turbo/turbo-build.log +1 -4
- package/CHANGELOG.md +68 -1
- package/README.md +397 -157
- package/dist/ai-promise.d.ts +50 -3
- package/dist/ai-promise.d.ts.map +1 -1
- package/dist/ai-promise.js +410 -51
- package/dist/ai-promise.js.map +1 -1
- package/dist/ai-schemas.d.ts +56 -0
- package/dist/ai-schemas.d.ts.map +1 -0
- package/dist/ai-schemas.js +53 -0
- package/dist/ai-schemas.js.map +1 -0
- package/dist/ai.d.ts +16 -242
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +54 -837
- package/dist/ai.js.map +1 -1
- package/dist/batch/anthropic.d.ts +6 -4
- package/dist/batch/anthropic.d.ts.map +1 -1
- package/dist/batch/anthropic.js +83 -145
- package/dist/batch/anthropic.js.map +1 -1
- package/dist/batch/bedrock.d.ts +8 -30
- package/dist/batch/bedrock.d.ts.map +1 -1
- package/dist/batch/bedrock.js +155 -338
- package/dist/batch/bedrock.js.map +1 -1
- package/dist/batch/cloudflare.d.ts +8 -20
- package/dist/batch/cloudflare.d.ts.map +1 -1
- package/dist/batch/cloudflare.js +68 -189
- package/dist/batch/cloudflare.js.map +1 -1
- package/dist/batch/google.d.ts +6 -20
- package/dist/batch/google.d.ts.map +1 -1
- package/dist/batch/google.js +70 -238
- package/dist/batch/google.js.map +1 -1
- package/dist/batch/index.d.ts +4 -1
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +4 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/memory.d.ts +1 -1
- package/dist/batch/memory.d.ts.map +1 -1
- package/dist/batch/memory.js +14 -10
- package/dist/batch/memory.js.map +1 -1
- package/dist/batch/openai.d.ts +11 -14
- package/dist/batch/openai.d.ts.map +1 -1
- package/dist/batch/openai.js +52 -156
- package/dist/batch/openai.js.map +1 -1
- package/dist/batch/provider.d.ts +111 -0
- package/dist/batch/provider.d.ts.map +1 -0
- package/dist/batch/provider.js +233 -0
- package/dist/batch/provider.js.map +1 -0
- package/dist/batch-map.d.ts.map +1 -1
- package/dist/batch-map.js +23 -17
- package/dist/batch-map.js.map +1 -1
- package/dist/batch-queue.d.ts +65 -0
- package/dist/batch-queue.d.ts.map +1 -1
- package/dist/batch-queue.js +169 -14
- package/dist/batch-queue.js.map +1 -1
- package/dist/budget.d.ts +272 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +513 -0
- package/dist/budget.js.map +1 -0
- package/dist/cache.d.ts +295 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +433 -0
- package/dist/cache.js.map +1 -0
- package/dist/context.d.ts +42 -8
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +64 -62
- package/dist/context.js.map +1 -1
- package/dist/digital-objects-registry.d.ts +229 -0
- package/dist/digital-objects-registry.d.ts.map +1 -0
- package/dist/digital-objects-registry.js +617 -0
- package/dist/digital-objects-registry.js.map +1 -0
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/eval/runner.d.ts +10 -1
- package/dist/eval/runner.d.ts.map +1 -1
- package/dist/eval/runner.js +41 -35
- package/dist/eval/runner.js.map +1 -1
- package/dist/eval-log/in-memory.d.ts +34 -0
- package/dist/eval-log/in-memory.d.ts.map +1 -0
- package/dist/eval-log/in-memory.js +84 -0
- package/dist/eval-log/in-memory.js.map +1 -0
- package/dist/eval-log/index.d.ts +29 -0
- package/dist/eval-log/index.d.ts.map +1 -0
- package/dist/eval-log/index.js +39 -0
- package/dist/eval-log/index.js.map +1 -0
- package/dist/eval-log/types.d.ts +101 -0
- package/dist/eval-log/types.d.ts.map +1 -0
- package/dist/eval-log/types.js +16 -0
- package/dist/eval-log/types.js.map +1 -0
- package/dist/function-registry.d.ts +116 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +546 -0
- package/dist/function-registry.js.map +1 -0
- package/dist/generate.d.ts +9 -3
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +18 -22
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +35 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -42
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +118 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +187 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware/budget.d.ts +84 -0
- package/dist/middleware/budget.d.ts.map +1 -0
- package/dist/middleware/budget.js +110 -0
- package/dist/middleware/budget.js.map +1 -0
- package/dist/middleware/cache.d.ts +103 -0
- package/dist/middleware/cache.d.ts.map +1 -0
- package/dist/middleware/cache.js +228 -0
- package/dist/middleware/cache.js.map +1 -0
- package/dist/middleware/embed-cache.d.ts +99 -0
- package/dist/middleware/embed-cache.d.ts.map +1 -0
- package/dist/middleware/embed-cache.js +128 -0
- package/dist/middleware/embed-cache.js.map +1 -0
- package/dist/middleware/index.d.ts +11 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +11 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/trace.d.ts +103 -0
- package/dist/middleware/trace.d.ts.map +1 -0
- package/dist/middleware/trace.js +176 -0
- package/dist/middleware/trace.js.map +1 -0
- package/dist/primitives.d.ts +120 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +398 -26
- package/dist/primitives.js.map +1 -1
- package/dist/retry.d.ts +368 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +646 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -10
- package/dist/schema.js.map +1 -1
- package/dist/telemetry.d.ts +128 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +285 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +6 -1
- package/dist/template.js.map +1 -1
- package/dist/tool-orchestration.d.ts +453 -0
- package/dist/tool-orchestration.d.ts.map +1 -0
- package/dist/tool-orchestration.js +763 -0
- package/dist/tool-orchestration.js.map +1 -0
- package/dist/type-guards.d.ts +28 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +29 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +135 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +36 -1
- package/dist/types.js.map +1 -1
- package/dist/wrap-for-v3.d.ts +80 -0
- package/dist/wrap-for-v3.d.ts.map +1 -0
- package/dist/wrap-for-v3.js +89 -0
- package/dist/wrap-for-v3.js.map +1 -0
- package/examples/00-quickstart.ts +232 -0
- package/examples/01-rag-chatbot.ts +212 -0
- package/examples/02-multi-agent-research.ts +290 -0
- package/examples/03-email-classification.ts +379 -0
- package/examples/04-content-moderation.ts +400 -0
- package/examples/05-document-extraction.ts +455 -0
- package/examples/06-streaming-chat-nextjs.ts +437 -0
- package/examples/07-cloudflare-worker.ts +483 -0
- package/examples/08-batch-processing.ts +491 -0
- package/examples/09-budget-constrained.ts +527 -0
- package/examples/10-tool-orchestration.ts +565 -0
- package/examples/11-retry-resilience.ts +403 -0
- package/examples/12-caching-strategies.ts +422 -0
- package/examples/README.md +145 -0
- package/package.json +10 -6
- package/src/ai-promise.ts +528 -99
- package/src/ai-schemas.ts +122 -0
- package/src/ai.ts +69 -1153
- package/src/batch/anthropic.ts +96 -161
- package/src/batch/bedrock.ts +203 -454
- package/src/batch/cloudflare.ts +99 -282
- package/src/batch/google.ts +91 -297
- package/src/batch/index.ts +4 -1
- package/src/batch/memory.ts +15 -10
- package/src/batch/openai.ts +65 -193
- package/src/batch/provider.ts +336 -0
- package/src/batch-map.ts +29 -24
- package/src/batch-queue.ts +200 -11
- package/src/budget.ts +740 -0
- package/src/cache.ts +681 -0
- package/src/context.ts +122 -76
- package/src/digital-objects-registry.ts +750 -0
- package/src/errors.ts +37 -0
- package/src/eval/runner.ts +63 -38
- package/src/eval-log/in-memory.ts +90 -0
- package/src/eval-log/index.ts +46 -0
- package/src/eval-log/types.ts +110 -0
- package/src/function-registry.ts +671 -0
- package/src/generate.ts +33 -33
- package/src/index.ts +325 -49
- package/src/logger.ts +232 -0
- package/src/middleware/budget.ts +171 -0
- package/src/middleware/cache.ts +299 -0
- package/src/middleware/embed-cache.ts +195 -0
- package/src/middleware/index.ts +23 -0
- package/src/middleware/trace.ts +248 -0
- package/src/primitives.ts +589 -62
- package/src/retry.ts +902 -0
- package/src/schema.ts +8 -17
- package/src/telemetry.ts +403 -0
- package/src/template.ts +8 -4
- package/src/tool-orchestration.ts +1173 -0
- package/src/type-guards.ts +31 -0
- package/src/types.ts +164 -25
- package/src/wrap-for-v3.ts +105 -0
- package/test/ai-promise.test.ts +1080 -0
- package/test/ai-proxy.test.ts +1 -1
- package/test/backward-compat.test.ts +147 -0
- package/test/batch-autosubmit-errors.test.ts +610 -0
- package/test/batch-blog-posts.test.ts +87 -129
- package/test/budget-tracking.test.ts +800 -0
- package/test/cache.test.ts +712 -0
- package/test/context-isolation.test.ts +687 -0
- package/test/core-functions.test.ts +183 -579
- package/test/decide.test.ts +154 -322
- package/test/define.test.ts +211 -8
- package/test/digital-objects-registry.test.ts +760 -0
- package/test/embedding-cache-middleware.test.ts +140 -0
- package/test/evals/deterministic.eval.test.ts +376 -0
- package/test/generate-core.test.ts +140 -229
- package/test/implicit-batch.test.ts +22 -65
- package/test/json-parse-error-handling.test.ts +463 -0
- package/test/retry-policy-integration.test.ts +117 -0
- package/test/retry.test.ts +1016 -0
- package/test/schema.test.ts +55 -19
- package/test/streaming.test.ts +316 -0
- package/test/template.test.ts +1164 -0
- package/test/tool-orchestration.test.ts +1040 -0
- package/test/wrap-for-v3.test.ts +612 -0
- package/vitest.config.js +6 -0
- package/vitest.config.ts +20 -0
- package/dist/rpc/auth.d.ts +0 -69
- package/dist/rpc/auth.d.ts.map +0 -1
- package/dist/rpc/auth.js +0 -136
- package/dist/rpc/auth.js.map +0 -1
- package/dist/rpc/client.d.ts +0 -62
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -103
- package/dist/rpc/client.js.map +0 -1
- package/dist/rpc/deferred.d.ts +0 -60
- package/dist/rpc/deferred.d.ts.map +0 -1
- package/dist/rpc/deferred.js +0 -96
- package/dist/rpc/deferred.js.map +0 -1
- package/dist/rpc/index.d.ts +0 -22
- package/dist/rpc/index.d.ts.map +0 -1
- package/dist/rpc/index.js +0 -38
- package/dist/rpc/index.js.map +0 -1
- package/dist/rpc/local.d.ts +0 -42
- package/dist/rpc/local.d.ts.map +0 -1
- package/dist/rpc/local.js +0 -50
- package/dist/rpc/local.js.map +0 -1
- package/dist/rpc/server.d.ts +0 -165
- package/dist/rpc/server.d.ts.map +0 -1
- package/dist/rpc/server.js +0 -405
- package/dist/rpc/server.js.map +0 -1
- package/dist/rpc/session.d.ts +0 -32
- package/dist/rpc/session.d.ts.map +0 -1
- package/dist/rpc/session.js +0 -43
- package/dist/rpc/session.js.map +0 -1
- package/dist/rpc/transport.d.ts +0 -306
- package/dist/rpc/transport.d.ts.map +0 -1
- package/dist/rpc/transport.js +0 -731
- package/dist/rpc/transport.js.map +0 -1
- package/src/batch/anthropic.js +0 -256
- package/src/batch/bedrock.js +0 -584
- package/src/batch/cloudflare.js +0 -287
- package/src/batch/google.js +0 -359
- package/src/batch/index.js +0 -30
- package/src/batch/memory.js +0 -187
- package/src/batch/openai.js +0 -402
- package/src/eval/index.js +0 -7
- package/src/eval/models.js +0 -119
- package/src/eval/runner.js +0 -147
- package/test/schema.test.js +0 -96
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for caching layer for embeddings and generations
|
|
3
|
+
*
|
|
4
|
+
* TDD: RED Phase - These tests should fail initially
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
8
|
+
import {
|
|
9
|
+
// Cache storage interface and implementations
|
|
10
|
+
CacheStorage,
|
|
11
|
+
MemoryCache,
|
|
12
|
+
|
|
13
|
+
// Specialized caches
|
|
14
|
+
EmbeddingCache,
|
|
15
|
+
GenerationCache,
|
|
16
|
+
|
|
17
|
+
// Cache wrapper
|
|
18
|
+
withCache,
|
|
19
|
+
|
|
20
|
+
// Utilities
|
|
21
|
+
hashKey,
|
|
22
|
+
createCacheKey,
|
|
23
|
+
|
|
24
|
+
// Types
|
|
25
|
+
CacheEntry,
|
|
26
|
+
CacheOptions,
|
|
27
|
+
CacheStats,
|
|
28
|
+
} from '../src/index.js'
|
|
29
|
+
|
|
30
|
+
describe('CacheStorage interface', () => {
|
|
31
|
+
describe('MemoryCache', () => {
|
|
32
|
+
let cache: MemoryCache<string>
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
cache = new MemoryCache<string>()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('stores and retrieves values', async () => {
|
|
39
|
+
await cache.set('key1', 'value1')
|
|
40
|
+
const result = await cache.get('key1')
|
|
41
|
+
expect(result).toBe('value1')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('returns undefined for missing keys', async () => {
|
|
45
|
+
const result = await cache.get('nonexistent')
|
|
46
|
+
expect(result).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('checks if key exists', async () => {
|
|
50
|
+
await cache.set('key1', 'value1')
|
|
51
|
+
expect(await cache.has('key1')).toBe(true)
|
|
52
|
+
expect(await cache.has('nonexistent')).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('deletes keys', async () => {
|
|
56
|
+
await cache.set('key1', 'value1')
|
|
57
|
+
await cache.delete('key1')
|
|
58
|
+
expect(await cache.has('key1')).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('clears all entries', async () => {
|
|
62
|
+
await cache.set('key1', 'value1')
|
|
63
|
+
await cache.set('key2', 'value2')
|
|
64
|
+
await cache.clear()
|
|
65
|
+
expect(await cache.has('key1')).toBe(false)
|
|
66
|
+
expect(await cache.has('key2')).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns cache size', async () => {
|
|
70
|
+
expect(await cache.size()).toBe(0)
|
|
71
|
+
await cache.set('key1', 'value1')
|
|
72
|
+
expect(await cache.size()).toBe(1)
|
|
73
|
+
await cache.set('key2', 'value2')
|
|
74
|
+
expect(await cache.size()).toBe(2)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('lists all keys', async () => {
|
|
78
|
+
await cache.set('key1', 'value1')
|
|
79
|
+
await cache.set('key2', 'value2')
|
|
80
|
+
const keys = await cache.keys()
|
|
81
|
+
expect(keys).toContain('key1')
|
|
82
|
+
expect(keys).toContain('key2')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('MemoryCache with TTL', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.useFakeTimers()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
vi.useRealTimers()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('expires entries after TTL', async () => {
|
|
96
|
+
const cache = new MemoryCache<string>({ defaultTTL: 1000 }) // 1 second TTL
|
|
97
|
+
|
|
98
|
+
await cache.set('key1', 'value1')
|
|
99
|
+
expect(await cache.get('key1')).toBe('value1')
|
|
100
|
+
|
|
101
|
+
// Advance time past TTL
|
|
102
|
+
vi.advanceTimersByTime(1500)
|
|
103
|
+
|
|
104
|
+
expect(await cache.get('key1')).toBeUndefined()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('allows per-key TTL override', async () => {
|
|
108
|
+
const cache = new MemoryCache<string>({ defaultTTL: 1000 })
|
|
109
|
+
|
|
110
|
+
await cache.set('key1', 'value1', { ttl: 500 })
|
|
111
|
+
await cache.set('key2', 'value2', { ttl: 2000 })
|
|
112
|
+
|
|
113
|
+
vi.advanceTimersByTime(750)
|
|
114
|
+
|
|
115
|
+
expect(await cache.get('key1')).toBeUndefined() // Expired
|
|
116
|
+
expect(await cache.get('key2')).toBe('value2') // Still valid
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('supports sliding window TTL refresh on access', async () => {
|
|
120
|
+
const cache = new MemoryCache<string>({ defaultTTL: 1000, slidingExpiration: true })
|
|
121
|
+
|
|
122
|
+
await cache.set('key1', 'value1')
|
|
123
|
+
|
|
124
|
+
// Access before expiry to refresh TTL
|
|
125
|
+
vi.advanceTimersByTime(800)
|
|
126
|
+
expect(await cache.get('key1')).toBe('value1') // Refreshes TTL
|
|
127
|
+
|
|
128
|
+
// Another 800ms should still be valid because we refreshed
|
|
129
|
+
vi.advanceTimersByTime(800)
|
|
130
|
+
expect(await cache.get('key1')).toBe('value1')
|
|
131
|
+
|
|
132
|
+
// Without access for full TTL, it should expire
|
|
133
|
+
vi.advanceTimersByTime(1100)
|
|
134
|
+
expect(await cache.get('key1')).toBeUndefined()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('cleans up expired entries automatically', async () => {
|
|
138
|
+
const cache = new MemoryCache<string>({
|
|
139
|
+
defaultTTL: 1000,
|
|
140
|
+
cleanupInterval: 500
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await cache.set('key1', 'value1')
|
|
144
|
+
expect(await cache.size()).toBe(1)
|
|
145
|
+
|
|
146
|
+
vi.advanceTimersByTime(1500)
|
|
147
|
+
|
|
148
|
+
// After cleanup, size should be 0
|
|
149
|
+
expect(await cache.size()).toBe(0)
|
|
150
|
+
|
|
151
|
+
cache.dispose() // Clean up timer
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('MemoryCache with LRU eviction', () => {
|
|
156
|
+
it('evicts least recently used entries when max size reached', async () => {
|
|
157
|
+
const cache = new MemoryCache<string>({ maxSize: 3 })
|
|
158
|
+
|
|
159
|
+
await cache.set('key1', 'value1')
|
|
160
|
+
await cache.set('key2', 'value2')
|
|
161
|
+
await cache.set('key3', 'value3')
|
|
162
|
+
|
|
163
|
+
// Access key1 to make it recently used
|
|
164
|
+
await cache.get('key1')
|
|
165
|
+
|
|
166
|
+
// Add key4, should evict key2 (least recently used)
|
|
167
|
+
await cache.set('key4', 'value4')
|
|
168
|
+
|
|
169
|
+
expect(await cache.size()).toBe(3)
|
|
170
|
+
expect(await cache.has('key1')).toBe(true)
|
|
171
|
+
expect(await cache.has('key2')).toBe(false) // Evicted
|
|
172
|
+
expect(await cache.has('key3')).toBe(true)
|
|
173
|
+
expect(await cache.has('key4')).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('Cache key generation', () => {
|
|
179
|
+
describe('hashKey', () => {
|
|
180
|
+
it('generates consistent hashes for same input', () => {
|
|
181
|
+
const hash1 = hashKey('hello world')
|
|
182
|
+
const hash2 = hashKey('hello world')
|
|
183
|
+
expect(hash1).toBe(hash2)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('generates different hashes for different inputs', () => {
|
|
187
|
+
const hash1 = hashKey('hello')
|
|
188
|
+
const hash2 = hashKey('world')
|
|
189
|
+
expect(hash1).not.toBe(hash2)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('handles objects by serializing them', () => {
|
|
193
|
+
const hash1 = hashKey({ a: 1, b: 2 })
|
|
194
|
+
const hash2 = hashKey({ a: 1, b: 2 })
|
|
195
|
+
const hash3 = hashKey({ b: 2, a: 1 }) // Different key order
|
|
196
|
+
|
|
197
|
+
expect(hash1).toBe(hash2)
|
|
198
|
+
// Sorted keys should produce same hash
|
|
199
|
+
expect(hash1).toBe(hash3)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('handles arrays', () => {
|
|
203
|
+
const hash1 = hashKey([1, 2, 3])
|
|
204
|
+
const hash2 = hashKey([1, 2, 3])
|
|
205
|
+
const hash3 = hashKey([3, 2, 1])
|
|
206
|
+
|
|
207
|
+
expect(hash1).toBe(hash2)
|
|
208
|
+
expect(hash1).not.toBe(hash3)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('createCacheKey', () => {
|
|
213
|
+
it('creates content-addressable keys for embeddings', () => {
|
|
214
|
+
const key1 = createCacheKey('embedding', { content: 'hello world', model: 'text-embedding-3-small' })
|
|
215
|
+
const key2 = createCacheKey('embedding', { content: 'hello world', model: 'text-embedding-3-small' })
|
|
216
|
+
const key3 = createCacheKey('embedding', { content: 'different', model: 'text-embedding-3-small' })
|
|
217
|
+
|
|
218
|
+
expect(key1).toBe(key2)
|
|
219
|
+
expect(key1).not.toBe(key3)
|
|
220
|
+
expect(key1).toContain('embedding:')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('creates parameter-aware keys for generations', () => {
|
|
224
|
+
const key1 = createCacheKey('generation', {
|
|
225
|
+
prompt: 'Write a poem',
|
|
226
|
+
model: 'sonnet',
|
|
227
|
+
temperature: 0.7
|
|
228
|
+
})
|
|
229
|
+
const key2 = createCacheKey('generation', {
|
|
230
|
+
prompt: 'Write a poem',
|
|
231
|
+
model: 'sonnet',
|
|
232
|
+
temperature: 0.7
|
|
233
|
+
})
|
|
234
|
+
const key3 = createCacheKey('generation', {
|
|
235
|
+
prompt: 'Write a poem',
|
|
236
|
+
model: 'sonnet',
|
|
237
|
+
temperature: 0.9 // Different temperature
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(key1).toBe(key2)
|
|
241
|
+
expect(key1).not.toBe(key3)
|
|
242
|
+
expect(key1).toContain('generation:')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('includes schema version in structured output keys', () => {
|
|
246
|
+
const key1 = createCacheKey('generation', {
|
|
247
|
+
prompt: 'Extract data',
|
|
248
|
+
model: 'sonnet',
|
|
249
|
+
schemaVersion: 'v1'
|
|
250
|
+
})
|
|
251
|
+
const key2 = createCacheKey('generation', {
|
|
252
|
+
prompt: 'Extract data',
|
|
253
|
+
model: 'sonnet',
|
|
254
|
+
schemaVersion: 'v2'
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
expect(key1).not.toBe(key2)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('EmbeddingCache', () => {
|
|
263
|
+
let embeddingCache: EmbeddingCache
|
|
264
|
+
|
|
265
|
+
beforeEach(() => {
|
|
266
|
+
embeddingCache = new EmbeddingCache()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('stores and retrieves embeddings by content hash', async () => {
|
|
270
|
+
const embedding = [0.1, 0.2, 0.3, 0.4, 0.5]
|
|
271
|
+
|
|
272
|
+
await embeddingCache.set('hello world', embedding, { model: 'text-embedding-3-small' })
|
|
273
|
+
const result = await embeddingCache.get('hello world', { model: 'text-embedding-3-small' })
|
|
274
|
+
|
|
275
|
+
expect(result).toEqual(embedding)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('returns undefined for cache miss', async () => {
|
|
279
|
+
const result = await embeddingCache.get('unknown text', { model: 'text-embedding-3-small' })
|
|
280
|
+
expect(result).toBeUndefined()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('differentiates by model', async () => {
|
|
284
|
+
const embedding1 = [0.1, 0.2, 0.3]
|
|
285
|
+
const embedding2 = [0.4, 0.5, 0.6]
|
|
286
|
+
|
|
287
|
+
await embeddingCache.set('hello', embedding1, { model: 'model-a' })
|
|
288
|
+
await embeddingCache.set('hello', embedding2, { model: 'model-b' })
|
|
289
|
+
|
|
290
|
+
expect(await embeddingCache.get('hello', { model: 'model-a' })).toEqual(embedding1)
|
|
291
|
+
expect(await embeddingCache.get('hello', { model: 'model-b' })).toEqual(embedding2)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('caches batch embeddings', async () => {
|
|
295
|
+
const texts = ['doc1', 'doc2', 'doc3']
|
|
296
|
+
const embeddings = [
|
|
297
|
+
[0.1, 0.2],
|
|
298
|
+
[0.3, 0.4],
|
|
299
|
+
[0.5, 0.6]
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
await embeddingCache.setMany(texts, embeddings, { model: 'text-embedding-3-small' })
|
|
303
|
+
|
|
304
|
+
expect(await embeddingCache.get('doc1', { model: 'text-embedding-3-small' })).toEqual([0.1, 0.2])
|
|
305
|
+
expect(await embeddingCache.get('doc2', { model: 'text-embedding-3-small' })).toEqual([0.3, 0.4])
|
|
306
|
+
expect(await embeddingCache.get('doc3', { model: 'text-embedding-3-small' })).toEqual([0.5, 0.6])
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('returns partial hits for batch lookups', async () => {
|
|
310
|
+
// Pre-populate some embeddings
|
|
311
|
+
await embeddingCache.set('doc1', [0.1, 0.2], { model: 'model-a' })
|
|
312
|
+
await embeddingCache.set('doc3', [0.5, 0.6], { model: 'model-a' })
|
|
313
|
+
|
|
314
|
+
const result = await embeddingCache.getMany(['doc1', 'doc2', 'doc3'], { model: 'model-a' })
|
|
315
|
+
|
|
316
|
+
expect(result.hits).toEqual({
|
|
317
|
+
'doc1': [0.1, 0.2],
|
|
318
|
+
'doc3': [0.5, 0.6]
|
|
319
|
+
})
|
|
320
|
+
expect(result.misses).toEqual(['doc2'])
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('provides cache statistics', async () => {
|
|
324
|
+
await embeddingCache.set('doc1', [0.1], { model: 'model-a' })
|
|
325
|
+
|
|
326
|
+
await embeddingCache.get('doc1', { model: 'model-a' }) // Hit
|
|
327
|
+
await embeddingCache.get('doc2', { model: 'model-a' }) // Miss
|
|
328
|
+
|
|
329
|
+
const stats = embeddingCache.getStats()
|
|
330
|
+
|
|
331
|
+
expect(stats.hits).toBe(1)
|
|
332
|
+
expect(stats.misses).toBe(1)
|
|
333
|
+
expect(stats.hitRate).toBeCloseTo(0.5)
|
|
334
|
+
expect(stats.size).toBe(1)
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
describe('GenerationCache', () => {
|
|
339
|
+
let generationCache: GenerationCache
|
|
340
|
+
|
|
341
|
+
beforeEach(() => {
|
|
342
|
+
generationCache = new GenerationCache()
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('caches generation results by prompt and parameters', async () => {
|
|
346
|
+
const result = { text: 'Hello, world!' }
|
|
347
|
+
|
|
348
|
+
await generationCache.set({
|
|
349
|
+
prompt: 'Say hello',
|
|
350
|
+
model: 'sonnet',
|
|
351
|
+
temperature: 0.7
|
|
352
|
+
}, result)
|
|
353
|
+
|
|
354
|
+
const cached = await generationCache.get({
|
|
355
|
+
prompt: 'Say hello',
|
|
356
|
+
model: 'sonnet',
|
|
357
|
+
temperature: 0.7
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
expect(cached).toEqual(result)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('returns undefined for cache miss', async () => {
|
|
364
|
+
const cached = await generationCache.get({
|
|
365
|
+
prompt: 'Unknown prompt',
|
|
366
|
+
model: 'sonnet'
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
expect(cached).toBeUndefined()
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('differentiates by temperature', async () => {
|
|
373
|
+
const result1 = { text: 'Deterministic response' }
|
|
374
|
+
const result2 = { text: 'Creative response' }
|
|
375
|
+
|
|
376
|
+
await generationCache.set({
|
|
377
|
+
prompt: 'Write something',
|
|
378
|
+
model: 'sonnet',
|
|
379
|
+
temperature: 0
|
|
380
|
+
}, result1)
|
|
381
|
+
|
|
382
|
+
await generationCache.set({
|
|
383
|
+
prompt: 'Write something',
|
|
384
|
+
model: 'sonnet',
|
|
385
|
+
temperature: 1
|
|
386
|
+
}, result2)
|
|
387
|
+
|
|
388
|
+
expect(await generationCache.get({
|
|
389
|
+
prompt: 'Write something',
|
|
390
|
+
model: 'sonnet',
|
|
391
|
+
temperature: 0
|
|
392
|
+
})).toEqual(result1)
|
|
393
|
+
|
|
394
|
+
expect(await generationCache.get({
|
|
395
|
+
prompt: 'Write something',
|
|
396
|
+
model: 'sonnet',
|
|
397
|
+
temperature: 1
|
|
398
|
+
})).toEqual(result2)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('differentiates by model', async () => {
|
|
402
|
+
await generationCache.set({
|
|
403
|
+
prompt: 'Hello',
|
|
404
|
+
model: 'sonnet'
|
|
405
|
+
}, { text: 'Sonnet response' })
|
|
406
|
+
|
|
407
|
+
await generationCache.set({
|
|
408
|
+
prompt: 'Hello',
|
|
409
|
+
model: 'opus'
|
|
410
|
+
}, { text: 'Opus response' })
|
|
411
|
+
|
|
412
|
+
expect(await generationCache.get({
|
|
413
|
+
prompt: 'Hello',
|
|
414
|
+
model: 'sonnet'
|
|
415
|
+
})).toEqual({ text: 'Sonnet response' })
|
|
416
|
+
|
|
417
|
+
expect(await generationCache.get({
|
|
418
|
+
prompt: 'Hello',
|
|
419
|
+
model: 'opus'
|
|
420
|
+
})).toEqual({ text: 'Opus response' })
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('includes system prompt in cache key', async () => {
|
|
424
|
+
await generationCache.set({
|
|
425
|
+
prompt: 'Hello',
|
|
426
|
+
model: 'sonnet',
|
|
427
|
+
system: 'You are a pirate'
|
|
428
|
+
}, { text: 'Ahoy!' })
|
|
429
|
+
|
|
430
|
+
await generationCache.set({
|
|
431
|
+
prompt: 'Hello',
|
|
432
|
+
model: 'sonnet',
|
|
433
|
+
system: 'You are a robot'
|
|
434
|
+
}, { text: 'Beep boop' })
|
|
435
|
+
|
|
436
|
+
expect(await generationCache.get({
|
|
437
|
+
prompt: 'Hello',
|
|
438
|
+
model: 'sonnet',
|
|
439
|
+
system: 'You are a pirate'
|
|
440
|
+
})).toEqual({ text: 'Ahoy!' })
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('supports schema versioning for structured outputs', async () => {
|
|
444
|
+
await generationCache.set({
|
|
445
|
+
prompt: 'Extract user',
|
|
446
|
+
model: 'sonnet',
|
|
447
|
+
schemaVersion: 'v1'
|
|
448
|
+
}, { object: { name: 'John' } })
|
|
449
|
+
|
|
450
|
+
await generationCache.set({
|
|
451
|
+
prompt: 'Extract user',
|
|
452
|
+
model: 'sonnet',
|
|
453
|
+
schemaVersion: 'v2'
|
|
454
|
+
}, { object: { name: 'John', age: 30 } })
|
|
455
|
+
|
|
456
|
+
expect(await generationCache.get({
|
|
457
|
+
prompt: 'Extract user',
|
|
458
|
+
model: 'sonnet',
|
|
459
|
+
schemaVersion: 'v1'
|
|
460
|
+
})).toEqual({ object: { name: 'John' } })
|
|
461
|
+
|
|
462
|
+
expect(await generationCache.get({
|
|
463
|
+
prompt: 'Extract user',
|
|
464
|
+
model: 'sonnet',
|
|
465
|
+
schemaVersion: 'v2'
|
|
466
|
+
})).toEqual({ object: { name: 'John', age: 30 } })
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('supports cache bypass option', async () => {
|
|
470
|
+
await generationCache.set({
|
|
471
|
+
prompt: 'Hello',
|
|
472
|
+
model: 'sonnet'
|
|
473
|
+
}, { text: 'Cached response' })
|
|
474
|
+
|
|
475
|
+
// With bypass, should return undefined
|
|
476
|
+
const bypassResult = await generationCache.get({
|
|
477
|
+
prompt: 'Hello',
|
|
478
|
+
model: 'sonnet'
|
|
479
|
+
}, { bypass: true })
|
|
480
|
+
|
|
481
|
+
expect(bypassResult).toBeUndefined()
|
|
482
|
+
|
|
483
|
+
// Without bypass, should return cached
|
|
484
|
+
const cachedResult = await generationCache.get({
|
|
485
|
+
prompt: 'Hello',
|
|
486
|
+
model: 'sonnet'
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
expect(cachedResult).toEqual({ text: 'Cached response' })
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('provides cache statistics', async () => {
|
|
493
|
+
await generationCache.set({ prompt: 'test', model: 'sonnet' }, { text: 'result' })
|
|
494
|
+
|
|
495
|
+
await generationCache.get({ prompt: 'test', model: 'sonnet' }) // Hit
|
|
496
|
+
await generationCache.get({ prompt: 'other', model: 'sonnet' }) // Miss
|
|
497
|
+
|
|
498
|
+
const stats = generationCache.getStats()
|
|
499
|
+
|
|
500
|
+
expect(stats.hits).toBe(1)
|
|
501
|
+
expect(stats.misses).toBe(1)
|
|
502
|
+
expect(stats.hitRate).toBeCloseTo(0.5)
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
describe('withCache wrapper', () => {
|
|
507
|
+
let cache: MemoryCache<string>
|
|
508
|
+
|
|
509
|
+
beforeEach(() => {
|
|
510
|
+
cache = new MemoryCache<string>()
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it('wraps an async function with caching', async () => {
|
|
514
|
+
let callCount = 0
|
|
515
|
+
const expensiveOperation = async (input: string) => {
|
|
516
|
+
callCount++
|
|
517
|
+
return `Result for ${input}`
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const cachedOperation = withCache(cache, expensiveOperation, {
|
|
521
|
+
keyFn: (input) => input
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
// First call - should execute function
|
|
525
|
+
const result1 = await cachedOperation('test')
|
|
526
|
+
expect(result1).toBe('Result for test')
|
|
527
|
+
expect(callCount).toBe(1)
|
|
528
|
+
|
|
529
|
+
// Second call with same input - should use cache
|
|
530
|
+
const result2 = await cachedOperation('test')
|
|
531
|
+
expect(result2).toBe('Result for test')
|
|
532
|
+
expect(callCount).toBe(1) // Still 1, didn't call again
|
|
533
|
+
|
|
534
|
+
// Different input - should execute function
|
|
535
|
+
const result3 = await cachedOperation('other')
|
|
536
|
+
expect(result3).toBe('Result for other')
|
|
537
|
+
expect(callCount).toBe(2)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
it('supports custom key generation', async () => {
|
|
541
|
+
const fn = async (a: number, b: number) => a + b
|
|
542
|
+
|
|
543
|
+
const cachedFn = withCache(cache, fn, {
|
|
544
|
+
keyFn: (a, b) => `sum:${a}:${b}`
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
await cachedFn(1, 2)
|
|
548
|
+
await cachedFn(1, 2)
|
|
549
|
+
|
|
550
|
+
expect(await cache.has('sum:1:2')).toBe(true)
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('respects TTL option', async () => {
|
|
554
|
+
vi.useFakeTimers()
|
|
555
|
+
|
|
556
|
+
let callCount = 0
|
|
557
|
+
const fn = async () => {
|
|
558
|
+
callCount++
|
|
559
|
+
return 'result'
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const ttlCache = new MemoryCache<string>({ defaultTTL: 1000 })
|
|
563
|
+
const cachedFn = withCache(ttlCache, fn, { keyFn: () => 'key' })
|
|
564
|
+
|
|
565
|
+
await cachedFn()
|
|
566
|
+
expect(callCount).toBe(1)
|
|
567
|
+
|
|
568
|
+
await cachedFn()
|
|
569
|
+
expect(callCount).toBe(1) // Cached
|
|
570
|
+
|
|
571
|
+
vi.advanceTimersByTime(1500)
|
|
572
|
+
|
|
573
|
+
await cachedFn()
|
|
574
|
+
expect(callCount).toBe(2) // Cache expired, called again
|
|
575
|
+
|
|
576
|
+
vi.useRealTimers()
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('handles errors without caching them', async () => {
|
|
580
|
+
let callCount = 0
|
|
581
|
+
const failingFn = async () => {
|
|
582
|
+
callCount++
|
|
583
|
+
throw new Error('Failed')
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const cachedFn = withCache(cache, failingFn, { keyFn: () => 'key' })
|
|
587
|
+
|
|
588
|
+
await expect(cachedFn()).rejects.toThrow('Failed')
|
|
589
|
+
expect(callCount).toBe(1)
|
|
590
|
+
|
|
591
|
+
// Should retry since error wasn't cached
|
|
592
|
+
await expect(cachedFn()).rejects.toThrow('Failed')
|
|
593
|
+
expect(callCount).toBe(2)
|
|
594
|
+
|
|
595
|
+
// Nothing should be in cache
|
|
596
|
+
expect(await cache.has('key')).toBe(false)
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it('supports bypass option', async () => {
|
|
600
|
+
let callCount = 0
|
|
601
|
+
const fn = async () => {
|
|
602
|
+
callCount++
|
|
603
|
+
return `result-${callCount}`
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const cachedFn = withCache(cache, fn, { keyFn: () => 'key' })
|
|
607
|
+
|
|
608
|
+
const result1 = await cachedFn()
|
|
609
|
+
expect(result1).toBe('result-1')
|
|
610
|
+
|
|
611
|
+
// Force fresh result
|
|
612
|
+
const result2 = await cachedFn.bypass()
|
|
613
|
+
expect(result2).toBe('result-2')
|
|
614
|
+
expect(callCount).toBe(2)
|
|
615
|
+
|
|
616
|
+
// Cache should be updated with new result
|
|
617
|
+
const result3 = await cachedFn()
|
|
618
|
+
expect(result3).toBe('result-2')
|
|
619
|
+
expect(callCount).toBe(2)
|
|
620
|
+
})
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
describe('Cache entry metadata', () => {
|
|
624
|
+
it('tracks creation time', async () => {
|
|
625
|
+
const cache = new MemoryCache<string>()
|
|
626
|
+
const before = Date.now()
|
|
627
|
+
|
|
628
|
+
await cache.set('key', 'value')
|
|
629
|
+
|
|
630
|
+
const entry = await cache.getEntry('key')
|
|
631
|
+
expect(entry).toBeDefined()
|
|
632
|
+
expect(entry!.createdAt).toBeGreaterThanOrEqual(before)
|
|
633
|
+
expect(entry!.createdAt).toBeLessThanOrEqual(Date.now())
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('tracks access time', async () => {
|
|
637
|
+
vi.useFakeTimers()
|
|
638
|
+
|
|
639
|
+
const cache = new MemoryCache<string>()
|
|
640
|
+
await cache.set('key', 'value')
|
|
641
|
+
|
|
642
|
+
const entry1 = await cache.getEntry('key')
|
|
643
|
+
const createdAt = entry1!.createdAt
|
|
644
|
+
|
|
645
|
+
vi.advanceTimersByTime(1000)
|
|
646
|
+
|
|
647
|
+
await cache.get('key')
|
|
648
|
+
const entry2 = await cache.getEntry('key')
|
|
649
|
+
|
|
650
|
+
expect(entry2!.lastAccessedAt).toBeGreaterThan(createdAt)
|
|
651
|
+
|
|
652
|
+
vi.useRealTimers()
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it('tracks access count', async () => {
|
|
656
|
+
const cache = new MemoryCache<string>()
|
|
657
|
+
await cache.set('key', 'value')
|
|
658
|
+
|
|
659
|
+
await cache.get('key')
|
|
660
|
+
await cache.get('key')
|
|
661
|
+
await cache.get('key')
|
|
662
|
+
|
|
663
|
+
const entry = await cache.getEntry('key')
|
|
664
|
+
expect(entry!.accessCount).toBe(3)
|
|
665
|
+
})
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
describe('Distributed cache interface', () => {
|
|
669
|
+
// These tests verify the interface contracts for Redis-like backends
|
|
670
|
+
|
|
671
|
+
it('CacheStorage interface is properly typed', () => {
|
|
672
|
+
// This is a compile-time check
|
|
673
|
+
const mockStorage: CacheStorage<string> = {
|
|
674
|
+
get: async () => undefined,
|
|
675
|
+
set: async () => {},
|
|
676
|
+
has: async () => false,
|
|
677
|
+
delete: async () => {},
|
|
678
|
+
clear: async () => {},
|
|
679
|
+
size: async () => 0,
|
|
680
|
+
keys: async () => []
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
expect(mockStorage).toBeDefined()
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
it('MemoryCache implements CacheStorage', () => {
|
|
687
|
+
const cache = new MemoryCache<string>()
|
|
688
|
+
|
|
689
|
+
// Verify all methods exist
|
|
690
|
+
expect(typeof cache.get).toBe('function')
|
|
691
|
+
expect(typeof cache.set).toBe('function')
|
|
692
|
+
expect(typeof cache.has).toBe('function')
|
|
693
|
+
expect(typeof cache.delete).toBe('function')
|
|
694
|
+
expect(typeof cache.clear).toBe('function')
|
|
695
|
+
expect(typeof cache.size).toBe('function')
|
|
696
|
+
expect(typeof cache.keys).toBe('function')
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
describe('Cache serialization', () => {
|
|
701
|
+
it('serializes cache entries for distributed storage', async () => {
|
|
702
|
+
const cache = new MemoryCache<{ data: number[] }>()
|
|
703
|
+
|
|
704
|
+
await cache.set('vectors', { data: [0.1, 0.2, 0.3] })
|
|
705
|
+
|
|
706
|
+
const entry = await cache.getEntry('vectors')
|
|
707
|
+
const serialized = JSON.stringify(entry)
|
|
708
|
+
const deserialized = JSON.parse(serialized)
|
|
709
|
+
|
|
710
|
+
expect(deserialized.value.data).toEqual([0.1, 0.2, 0.3])
|
|
711
|
+
})
|
|
712
|
+
})
|