bunsane 0.3.2 → 0.4.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.
Files changed (214) hide show
  1. package/CHANGELOG.md +445 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +85 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +4 -4
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +16 -8
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +364 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/pendingOps.ts +72 -0
  30. package/core/entity/saveEntity.ts +377 -0
  31. package/core/hooks/dispatcher.ts +439 -0
  32. package/core/hooks/guards.ts +155 -0
  33. package/core/hooks/registry.ts +247 -0
  34. package/core/metadata/definitions/Component.ts +1 -1
  35. package/core/metadata/index.ts +15 -4
  36. package/core/middleware/RateLimit.ts +102 -105
  37. package/core/middleware/RequestId.ts +2 -9
  38. package/core/middleware/SecurityHeaders.ts +2 -11
  39. package/core/middleware/headers.ts +28 -0
  40. package/core/remote/OutboxWorker.ts +213 -183
  41. package/core/remote/RemoteManager.ts +401 -400
  42. package/core/remote/types.ts +153 -151
  43. package/core/requestScope.ts +34 -0
  44. package/core/scheduler/cronEvaluator.ts +174 -0
  45. package/core/scheduler/lifecycleHooks.ts +21 -0
  46. package/core/scheduler/lockCoordinator.ts +27 -0
  47. package/core/scheduler/metrics.ts +14 -0
  48. package/core/scheduler/taskRunner.ts +420 -0
  49. package/database/DatabaseHelper.ts +128 -101
  50. package/database/IndexingStrategy.ts +72 -2
  51. package/database/PreparedStatementCache.ts +8 -2
  52. package/database/cancellable.ts +35 -22
  53. package/database/index.ts +15 -3
  54. package/database/instrumentedDb.ts +141 -141
  55. package/endpoints/archetypes.ts +2 -8
  56. package/endpoints/tables.ts +6 -1
  57. package/gql/index.ts +1 -1
  58. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  59. package/package.json +22 -1
  60. package/query/CTENode.ts +5 -3
  61. package/query/ComponentInclusionNode.ts +240 -13
  62. package/query/OrNode.ts +6 -5
  63. package/query/Query.ts +157 -46
  64. package/query/QueryContext.ts +6 -0
  65. package/query/QueryDAG.ts +7 -2
  66. package/query/membershipSource.ts +66 -0
  67. package/storage/LocalStorageProvider.ts +8 -3
  68. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  69. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  70. package/studio/{index.html → dist/index.html} +3 -2
  71. package/swagger/generator.ts +11 -1
  72. package/upload/UploadManager.ts +8 -6
  73. package/utils/uuid.ts +40 -10
  74. package/.claude/scheduled_tasks.lock +0 -1
  75. package/.claude/settings.local.json +0 -47
  76. package/.prettierrc +0 -4
  77. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  78. package/.serena/memories/architecture.md +0 -154
  79. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  80. package/.serena/memories/code_style_and_conventions.md +0 -76
  81. package/.serena/memories/project_overview.md +0 -43
  82. package/.serena/memories/schema-dsl-plan.md +0 -107
  83. package/.serena/memories/suggested_commands.md +0 -80
  84. package/.serena/memories/typescript-compilation-status.md +0 -54
  85. package/.serena/project.yml +0 -114
  86. package/BunSane.jpg +0 -0
  87. package/CLAUDE.md +0 -198
  88. package/TODO.md +0 -2
  89. package/bun.lock +0 -302
  90. package/bunfig.toml +0 -10
  91. package/docs/RFC_APP_REFACTOR.md +0 -248
  92. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  93. package/docs/SCALABILITY_PLAN.md +0 -175
  94. package/studio/bun.lock +0 -482
  95. package/studio/package.json +0 -39
  96. package/studio/postcss.config.js +0 -6
  97. package/studio/src/components/DataTable.tsx +0 -211
  98. package/studio/src/components/Layout.tsx +0 -13
  99. package/studio/src/components/PageContainer.tsx +0 -9
  100. package/studio/src/components/PageHeader.tsx +0 -13
  101. package/studio/src/components/SearchBar.tsx +0 -57
  102. package/studio/src/components/Sidebar.tsx +0 -294
  103. package/studio/src/components/ui/button.tsx +0 -56
  104. package/studio/src/components/ui/checkbox.tsx +0 -26
  105. package/studio/src/components/ui/input.tsx +0 -25
  106. package/studio/src/hooks/useDataTable.ts +0 -131
  107. package/studio/src/index.css +0 -36
  108. package/studio/src/lib/api.ts +0 -186
  109. package/studio/src/lib/utils.ts +0 -13
  110. package/studio/src/main.tsx +0 -17
  111. package/studio/src/pages/ArcheType.tsx +0 -239
  112. package/studio/src/pages/Components.tsx +0 -124
  113. package/studio/src/pages/EntityInspector.tsx +0 -302
  114. package/studio/src/pages/QueryRunner.tsx +0 -246
  115. package/studio/src/pages/Table.tsx +0 -94
  116. package/studio/src/pages/Welcome.tsx +0 -241
  117. package/studio/src/routes.tsx +0 -45
  118. package/studio/src/store/archeTypeSettings.ts +0 -30
  119. package/studio/src/store/studio.ts +0 -65
  120. package/studio/src/utils/columnHelpers.tsx +0 -114
  121. package/studio/studio-instructions.md +0 -81
  122. package/studio/tailwind.config.js +0 -77
  123. package/studio/utils.ts +0 -54
  124. package/studio/vite.config.js +0 -19
  125. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  126. package/tests/benchmark/bunfig.toml +0 -9
  127. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  128. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  129. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  130. package/tests/benchmark/fixtures/index.ts +0 -6
  131. package/tests/benchmark/index.ts +0 -22
  132. package/tests/benchmark/noop-preload.ts +0 -3
  133. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  134. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  135. package/tests/benchmark/runners/index.ts +0 -4
  136. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  137. package/tests/benchmark/scripts/generate-db.ts +0 -344
  138. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  139. package/tests/e2e/http.test.ts +0 -130
  140. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  141. package/tests/fixtures/components/TestOrder.ts +0 -23
  142. package/tests/fixtures/components/TestProduct.ts +0 -23
  143. package/tests/fixtures/components/TestUser.ts +0 -20
  144. package/tests/fixtures/components/index.ts +0 -6
  145. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  146. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  147. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  148. package/tests/helpers/MockRedisClient.ts +0 -113
  149. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  150. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  151. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  152. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  153. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  154. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  155. package/tests/integration/query/Query.abort.test.ts +0 -66
  156. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  157. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  158. package/tests/integration/query/Query.exec.test.ts +0 -576
  159. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  160. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  161. package/tests/integration/remote/dlq.test.ts +0 -175
  162. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  163. package/tests/integration/remote/outbox.test.ts +0 -130
  164. package/tests/integration/remote/rpc.test.ts +0 -177
  165. package/tests/pglite-setup.ts +0 -62
  166. package/tests/setup.ts +0 -164
  167. package/tests/stress/BenchmarkRunner.ts +0 -203
  168. package/tests/stress/DataSeeder.ts +0 -190
  169. package/tests/stress/StressTestReporter.ts +0 -229
  170. package/tests/stress/cursor-perf-test.ts +0 -171
  171. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  172. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  173. package/tests/stress/index.ts +0 -7
  174. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  175. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  176. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  177. package/tests/unit/BatchLoader.test.ts +0 -196
  178. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  179. package/tests/unit/cache/CacheManager.test.ts +0 -498
  180. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  181. package/tests/unit/cache/RedisCache.test.ts +0 -411
  182. package/tests/unit/database/cancellable.test.ts +0 -81
  183. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  184. package/tests/unit/entity/Entity.components.test.ts +0 -317
  185. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  186. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  187. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  188. package/tests/unit/entity/Entity.test.ts +0 -345
  189. package/tests/unit/gql/depthLimit.test.ts +0 -203
  190. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  191. package/tests/unit/health/Health.test.ts +0 -129
  192. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  193. package/tests/unit/middleware/Middleware.test.ts +0 -98
  194. package/tests/unit/middleware/RequestId.test.ts +0 -54
  195. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  196. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  197. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  198. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  199. package/tests/unit/query/Query.test.ts +0 -310
  200. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  201. package/tests/unit/remote/RemoteError.test.ts +0 -55
  202. package/tests/unit/remote/decorators.test.ts +0 -195
  203. package/tests/unit/remote/metrics.test.ts +0 -115
  204. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  205. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  206. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  207. package/tests/unit/schema/schema-integration.test.ts +0 -426
  208. package/tests/unit/schema/schema.test.ts +0 -580
  209. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  210. package/tests/unit/upload/RestUpload.test.ts +0 -267
  211. package/tests/unit/validateEnv.test.ts +0 -82
  212. package/tests/utils/entity-tracker.ts +0 -57
  213. package/tests/utils/index.ts +0 -13
  214. package/tests/utils/test-context.ts +0 -149
@@ -1,498 +0,0 @@
1
- /**
2
- * Unit tests for CacheManager
3
- * Tests cache configuration and management
4
- */
5
- import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
6
- import { CacheManager, COMPONENT_TOMBSTONE } from '../../../core/cache/CacheManager';
7
- import { MemoryCache } from '../../../core/cache/MemoryCache';
8
- import { MultiLevelCache } from '../../../core/cache/MultiLevelCache';
9
-
10
- describe('CacheManager', () => {
11
- let cacheManager: CacheManager;
12
-
13
- beforeEach(async () => {
14
- cacheManager = CacheManager.getInstance();
15
- await cacheManager.initialize({
16
- enabled: true,
17
- provider: 'memory',
18
- strategy: 'write-through',
19
- defaultTTL: 3600000,
20
- entity: { enabled: true, ttl: 3600000 },
21
- component: { enabled: true, ttl: 1800000 },
22
- query: { enabled: false, ttl: 300000, maxSize: 10000 }
23
- });
24
- });
25
-
26
- afterEach(async () => {
27
- await cacheManager.clear();
28
- });
29
-
30
- describe('getInstance()', () => {
31
- test('returns singleton instance', () => {
32
- const instance1 = CacheManager.getInstance();
33
- const instance2 = CacheManager.getInstance();
34
- expect(instance1).toBe(instance2);
35
- });
36
-
37
- test('returns defined instance', () => {
38
- const instance = CacheManager.getInstance();
39
- expect(instance).toBeDefined();
40
- });
41
- });
42
-
43
- describe('initialize()', () => {
44
- test('applies configuration', async () => {
45
- await cacheManager.initialize({
46
- enabled: true,
47
- provider: 'memory',
48
- defaultTTL: 5000
49
- });
50
-
51
- const config = cacheManager.getConfig();
52
- expect(config.enabled).toBe(true);
53
- expect(config.provider).toBe('memory');
54
- expect(config.defaultTTL).toBe(5000);
55
- });
56
-
57
- test('can disable cache', async () => {
58
- await cacheManager.initialize({ enabled: false });
59
- const config = cacheManager.getConfig();
60
- expect(config.enabled).toBe(false);
61
- });
62
- });
63
-
64
- describe('getConfig()', () => {
65
- test('returns configuration object', () => {
66
- const config = cacheManager.getConfig();
67
- expect(config).toBeDefined();
68
- expect(typeof config.enabled).toBe('boolean');
69
- });
70
-
71
- test('returns copy of configuration', () => {
72
- const config1 = cacheManager.getConfig();
73
- const config2 = cacheManager.getConfig();
74
- expect(config1).not.toBe(config2);
75
- expect(config1).toEqual(config2);
76
- });
77
- });
78
-
79
- describe('getProvider()', () => {
80
- test('returns cache provider', () => {
81
- const provider = cacheManager.getProvider();
82
- expect(provider).toBeDefined();
83
- });
84
- });
85
-
86
- describe('generic cache operations', () => {
87
- test('get returns null for missing key', async () => {
88
- const result = await cacheManager.get('non-existent-key');
89
- expect(result).toBeNull();
90
- });
91
-
92
- test('set and get work correctly', async () => {
93
- await cacheManager.set('test-key', { data: 'value' });
94
- const result = await cacheManager.get<{ data: string }>('test-key');
95
- expect(result).toEqual({ data: 'value' });
96
- });
97
-
98
- test('set respects TTL', async () => {
99
- await cacheManager.set('test-key', 'value', 100);
100
- const immediate = await cacheManager.get('test-key');
101
- expect(immediate).toBe('value');
102
-
103
- // Wait for TTL to expire
104
- await new Promise(resolve => setTimeout(resolve, 150));
105
- const expired = await cacheManager.get('test-key');
106
- expect(expired).toBeNull();
107
- });
108
-
109
- test('delete removes key', async () => {
110
- await cacheManager.set('test-key', 'value');
111
- await cacheManager.delete('test-key');
112
- const result = await cacheManager.get('test-key');
113
- expect(result).toBeNull();
114
- });
115
-
116
- test('clear removes all keys', async () => {
117
- await cacheManager.set('key1', 'value1');
118
- await cacheManager.set('key2', 'value2');
119
- await cacheManager.clear();
120
-
121
- const result1 = await cacheManager.get('key1');
122
- const result2 = await cacheManager.get('key2');
123
- expect(result1).toBeNull();
124
- expect(result2).toBeNull();
125
- });
126
- });
127
-
128
- describe('entity cache operations', () => {
129
- test('getEntity returns null for missing entity', async () => {
130
- const result = await cacheManager.getEntity('missing-id');
131
- expect(result).toBeNull();
132
- });
133
-
134
- test('invalidateEntity removes entity from cache', async () => {
135
- // Manually set entity cache
136
- const provider = cacheManager.getProvider();
137
- await provider.set('entity:test-id', 'test-id', 3600000);
138
-
139
- await cacheManager.invalidateEntity('test-id');
140
- const result = await cacheManager.getEntity('test-id');
141
- expect(result).toBeNull();
142
- });
143
-
144
- test('invalidateEntities clears entity + all component caches for a batch', async () => {
145
- const provider = cacheManager.getProvider();
146
- // Two entities, each with cached entity entry + one component
147
- await provider.set('entity:e1', 'e1', 3600000);
148
- await provider.set('entity:e2', 'e2', 3600000);
149
- await provider.set('component:e1:t1', { data: 'a' }, 3600000);
150
- await provider.set('component:e2:t1', { data: 'b' }, 3600000);
151
-
152
- await cacheManager.invalidateEntities(['e1', 'e2']);
153
-
154
- expect(await cacheManager.getEntity('e1')).toBeNull();
155
- expect(await cacheManager.getEntity('e2')).toBeNull();
156
- expect(await cacheManager.getComponentsByEntity('e1', 't1')).toBeNull();
157
- expect(await cacheManager.getComponentsByEntity('e2', 't1')).toBeNull();
158
- });
159
-
160
- test('invalidateEntities is a noop for empty list', async () => {
161
- await expect(cacheManager.invalidateEntities([])).resolves.toBeUndefined();
162
- });
163
-
164
- test('getEntities returns null for missing entities', async () => {
165
- const results = await cacheManager.getEntities(['id1', 'id2', 'id3']);
166
- expect(results.length).toBe(3);
167
- expect(results.every(r => r === null)).toBe(true);
168
- });
169
- });
170
-
171
- describe('component cache operations', () => {
172
- test('getComponentsByEntity returns null for missing components', async () => {
173
- const result = await cacheManager.getComponentsByEntity('entity-id');
174
- expect(result).toBeNull();
175
- });
176
-
177
- test('invalidateComponent removes specific component', async () => {
178
- // Manually set component cache
179
- const provider = cacheManager.getProvider();
180
- await provider.set('component:entity-id:type-id', { data: 'test' }, 3600000);
181
-
182
- await cacheManager.invalidateComponent('entity-id', 'type-id');
183
- const result = await cacheManager.getComponentsByEntity('entity-id', 'type-id');
184
- expect(result).toBeNull();
185
- });
186
-
187
- test('invalidateComponents removes multiple components', async () => {
188
- const provider = cacheManager.getProvider();
189
- await provider.set('component:e1:t1', { data: 'test1' }, 3600000);
190
- await provider.set('component:e2:t2', { data: 'test2' }, 3600000);
191
-
192
- await cacheManager.invalidateComponents([
193
- { entityId: 'e1', typeId: 't1' },
194
- { entityId: 'e2', typeId: 't2' }
195
- ]);
196
-
197
- const result1 = await provider.get('component:e1:t1');
198
- const result2 = await provider.get('component:e2:t2');
199
- expect(result1).toBeNull();
200
- expect(result2).toBeNull();
201
- });
202
-
203
- test('getComponents returns null for missing components', async () => {
204
- const results = await cacheManager.getComponents([
205
- { entityId: 'e1', typeId: 't1' },
206
- { entityId: 'e2', typeId: 't2' }
207
- ]);
208
- expect(results.length).toBe(2);
209
- expect(results.every(r => r === null)).toBe(true);
210
- });
211
- });
212
-
213
- describe('component negative cache (tombstones)', () => {
214
- beforeEach(async () => {
215
- await cacheManager.initialize({
216
- enabled: true,
217
- provider: 'memory',
218
- strategy: 'write-through',
219
- defaultTTL: 3600000,
220
- entity: { enabled: true, ttl: 3600000 },
221
- component: {
222
- enabled: true,
223
- ttl: 1800000,
224
- negativeCacheEnabled: true,
225
- negativeCacheTtl: 60_000,
226
- },
227
- query: { enabled: false, ttl: 300000, maxSize: 10000 },
228
- });
229
- });
230
-
231
- test('setComponentsWriteThrough writes tombstones for absent requested keys', async () => {
232
- const requested = [
233
- { entityId: 'e1', typeId: 't1' },
234
- { entityId: 'e2', typeId: 't2' },
235
- ];
236
- await cacheManager.setComponentsWriteThrough([], requested);
237
- const results = await cacheManager.getComponents(requested);
238
- expect(results[0]).toBe(COMPONENT_TOMBSTONE);
239
- expect(results[1]).toBe(COMPONENT_TOMBSTONE);
240
- });
241
-
242
- test('found rows are written, absent are tombstoned, single setMany', async () => {
243
- const requested = [
244
- { entityId: 'e1', typeId: 't1' },
245
- { entityId: 'e2', typeId: 't2' },
246
- ];
247
- const found = [{
248
- id: 'c1', entityId: 'e1', typeId: 't1',
249
- data: { x: 1 }, createdAt: new Date(), updatedAt: new Date(), deletedAt: null,
250
- }];
251
- await cacheManager.setComponentsWriteThrough(found, requested);
252
- const results = await cacheManager.getComponents(requested);
253
- expect((results[0] as any)?.id).toBe('c1');
254
- expect(results[1]).toBe(COMPONENT_TOMBSTONE);
255
- });
256
-
257
- test('invalidateComponent drops tombstone', async () => {
258
- await cacheManager.setComponentsWriteThrough([], [{ entityId: 'e1', typeId: 't1' }]);
259
- await cacheManager.invalidateComponent('e1', 't1');
260
- const results = await cacheManager.getComponents([{ entityId: 'e1', typeId: 't1' }]);
261
- expect(results[0]).toBeNull();
262
- });
263
-
264
- test('negativeCacheEnabled=false skips tombstone writes (backward compat)', async () => {
265
- await cacheManager.initialize({
266
- enabled: true,
267
- provider: 'memory',
268
- strategy: 'write-through',
269
- defaultTTL: 3600000,
270
- entity: { enabled: true, ttl: 3600000 },
271
- component: { enabled: true, ttl: 1800000 },
272
- query: { enabled: false, ttl: 300000, maxSize: 10000 },
273
- });
274
- await cacheManager.setComponentsWriteThrough([], [{ entityId: 'e1', typeId: 't1' }]);
275
- const results = await cacheManager.getComponents([{ entityId: 'e1', typeId: 't1' }]);
276
- expect(results[0]).toBeNull();
277
- });
278
-
279
- test('legacy 2-arg signature (components, ttl) still works', async () => {
280
- const data = [{
281
- id: 'c1', entityId: 'e1', typeId: 't1',
282
- data: { x: 1 }, createdAt: new Date(), updatedAt: new Date(), deletedAt: null,
283
- }];
284
- await cacheManager.setComponentsWriteThrough(data, 3600_000);
285
- const results = await cacheManager.getComponents([{ entityId: 'e1', typeId: 't1' }]);
286
- expect((results[0] as any)?.id).toBe('c1');
287
- });
288
- });
289
-
290
- describe('relation negative cache', () => {
291
- beforeEach(async () => {
292
- await cacheManager.initialize({
293
- enabled: true,
294
- provider: 'memory',
295
- strategy: 'write-through',
296
- defaultTTL: 3600000,
297
- entity: { enabled: true, ttl: 3600000 },
298
- component: { enabled: true, ttl: 1800000 },
299
- relation: { negativeCacheEnabled: true, negativeCacheTtl: 60_000 },
300
- query: { enabled: false, ttl: 300000, maxSize: 10000 },
301
- });
302
- });
303
-
304
- test('setRelationsEmpty + getRelationsEmpty round trip', async () => {
305
- const keys = [
306
- { entityId: 'u1', relationField: 'orders', relatedType: 'Order', foreignKey: 'user_id' },
307
- { entityId: 'u2', relationField: 'orders', relatedType: 'Order', foreignKey: 'user_id' },
308
- ];
309
- await cacheManager.setRelationsEmpty(keys);
310
- const flags = await cacheManager.getRelationsEmpty(keys);
311
- expect(flags).toEqual([true, true]);
312
- });
313
-
314
- test('getRelationsEmpty returns false for keys never tombstoned', async () => {
315
- const flags = await cacheManager.getRelationsEmpty([
316
- { entityId: 'u-fresh', relationField: 'orders', relatedType: 'Order', foreignKey: 'user_id' },
317
- ]);
318
- expect(flags).toEqual([false]);
319
- });
320
-
321
- test('invalidateRelation drops tombstone', async () => {
322
- const key = { entityId: 'u1', relationField: 'orders', relatedType: 'Order', foreignKey: 'user_id' };
323
- await cacheManager.setRelationsEmpty([key]);
324
- await cacheManager.invalidateRelation(key.entityId, key.relationField, key.relatedType, key.foreignKey);
325
- const flags = await cacheManager.getRelationsEmpty([key]);
326
- expect(flags).toEqual([false]);
327
- });
328
-
329
- test('relation cache disabled returns all-false (no-op)', async () => {
330
- await cacheManager.initialize({
331
- enabled: true,
332
- provider: 'memory',
333
- strategy: 'write-through',
334
- defaultTTL: 3600000,
335
- relation: { negativeCacheEnabled: false },
336
- });
337
- const key = { entityId: 'u1', relationField: 'orders', relatedType: 'Order', foreignKey: 'user_id' };
338
- await cacheManager.setRelationsEmpty([key]);
339
- const flags = await cacheManager.getRelationsEmpty([key]);
340
- expect(flags).toEqual([false]);
341
- });
342
- });
343
-
344
- describe('cache disabled', () => {
345
- beforeEach(async () => {
346
- await cacheManager.initialize({ enabled: false });
347
- });
348
-
349
- test('get returns null when disabled', async () => {
350
- const result = await cacheManager.get('key');
351
- expect(result).toBeNull();
352
- });
353
-
354
- test('set does nothing when disabled', async () => {
355
- await cacheManager.set('key', 'value');
356
- // Re-enable to check
357
- await cacheManager.initialize({ enabled: true, provider: 'memory' });
358
- const result = await cacheManager.get('key');
359
- expect(result).toBeNull();
360
- });
361
-
362
- test('getEntity returns null when disabled', async () => {
363
- const result = await cacheManager.getEntity('id');
364
- expect(result).toBeNull();
365
- });
366
-
367
- test('getComponentsByEntity returns null when disabled', async () => {
368
- const result = await cacheManager.getComponentsByEntity('id');
369
- expect(result).toBeNull();
370
- });
371
- });
372
-
373
- describe('getStats()', () => {
374
- test('returns statistics object', async () => {
375
- const stats = await cacheManager.getStats();
376
- expect(stats).toBeDefined();
377
- expect(typeof stats.hits).toBe('number');
378
- expect(typeof stats.misses).toBe('number');
379
- });
380
- });
381
-
382
- describe('ping()', () => {
383
- test('returns true for healthy cache', async () => {
384
- const result = await cacheManager.ping();
385
- expect(result).toBe(true);
386
- });
387
- });
388
-
389
- describe('shutdown()', () => {
390
- test('calls stopCleanup on MemoryCache provider', async () => {
391
- await cacheManager.initialize({
392
- enabled: true,
393
- provider: 'memory',
394
- defaultTTL: 3600000
395
- });
396
-
397
- const provider = cacheManager.getProvider() as MemoryCache;
398
- const stopCleanupSpy = mock(() => {});
399
- (provider as any).stopCleanup = stopCleanupSpy;
400
-
401
- await cacheManager.shutdown();
402
- expect(stopCleanupSpy).toHaveBeenCalled();
403
- });
404
-
405
- test('shutdown does not throw on NoOp provider', async () => {
406
- await cacheManager.initialize({ enabled: false });
407
- await expect(cacheManager.shutdown()).resolves.toBeUndefined();
408
- });
409
- });
410
-
411
- describe('initialize() cleanup', () => {
412
- test('shuts down old provider when reinitializing', async () => {
413
- await cacheManager.initialize({
414
- enabled: true,
415
- provider: 'memory',
416
- defaultTTL: 3600000
417
- });
418
-
419
- const oldProvider = cacheManager.getProvider() as MemoryCache;
420
- const stopCleanupSpy = mock(() => {});
421
- (oldProvider as any).stopCleanup = stopCleanupSpy;
422
-
423
- // Reinitialize with new config
424
- await cacheManager.initialize({
425
- enabled: true,
426
- provider: 'memory',
427
- defaultTTL: 5000
428
- });
429
-
430
- expect(stopCleanupSpy).toHaveBeenCalled();
431
- });
432
- });
433
-
434
- describe('cross-instance invalidation (pub/sub)', () => {
435
- test('pub/sub not enabled for memory-only provider', async () => {
436
- await cacheManager.initialize({
437
- enabled: true,
438
- provider: 'memory',
439
- defaultTTL: 3600000
440
- });
441
-
442
- // pubSubEnabled is private, so we test indirectly:
443
- // publishInvalidation should be a no-op (no errors, no side effects)
444
- await cacheManager.set('test-key', 'value');
445
- await cacheManager.delete('test-key');
446
- // If pub/sub were broken for memory provider, this would throw
447
- const result = await cacheManager.get('test-key');
448
- expect(result).toBeNull();
449
- });
450
-
451
- test('invalidateEntity still works without pub/sub', async () => {
452
- const provider = cacheManager.getProvider();
453
- await provider.set('entity:abc', 'abc', 3600000);
454
-
455
- await cacheManager.invalidateEntity('abc');
456
- const result = await provider.get('entity:abc');
457
- expect(result).toBeNull();
458
- });
459
-
460
- test('invalidateComponent still works without pub/sub', async () => {
461
- const provider = cacheManager.getProvider();
462
- await provider.set('component:e1:t1', { data: 'test' }, 3600000);
463
-
464
- await cacheManager.invalidateComponent('e1', 't1');
465
- const result = await provider.get('component:e1:t1');
466
- expect(result).toBeNull();
467
- });
468
-
469
- test('clear still works without pub/sub', async () => {
470
- await cacheManager.set('key1', 'v1');
471
- await cacheManager.set('key2', 'v2');
472
-
473
- await cacheManager.clear();
474
- expect(await cacheManager.get('key1')).toBeNull();
475
- expect(await cacheManager.get('key2')).toBeNull();
476
- });
477
-
478
- test('handleRemoteInvalidation ignores messages from self', async () => {
479
- // Access private method via any
480
- const cm = cacheManager as any;
481
- const myId = cm.instanceId;
482
-
483
- // Simulate receiving our own message — should NOT invalidate
484
- await cacheManager.set('survive-key', 'should-survive');
485
-
486
- // Call private method directly
487
- await cm.handleRemoteInvalidation(JSON.stringify({
488
- instanceId: myId,
489
- type: 'key',
490
- keys: ['survive-key']
491
- }));
492
-
493
- // Key should still exist because self-messages are ignored
494
- const result = await cacheManager.get('survive-key');
495
- expect(result).toBe('should-survive');
496
- });
497
- });
498
- });