bunsane 0.3.2 → 0.5.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 (220) hide show
  1. package/CHANGELOG.md +471 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +93 -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 +8 -7
  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 +25 -10
  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 +383 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/getCacheManager.ts +10 -0
  30. package/core/entity/pendingOps.ts +72 -0
  31. package/core/entity/saveEntity.ts +375 -0
  32. package/core/health.ts +93 -4
  33. package/core/hooks/dispatcher.ts +439 -0
  34. package/core/hooks/guards.ts +155 -0
  35. package/core/hooks/registry.ts +247 -0
  36. package/core/metadata/definitions/Component.ts +1 -1
  37. package/core/metadata/index.ts +15 -4
  38. package/core/middleware/RateLimit.ts +102 -105
  39. package/core/middleware/RequestId.ts +2 -9
  40. package/core/middleware/SecurityHeaders.ts +2 -11
  41. package/core/middleware/headers.ts +28 -0
  42. package/core/remote/OutboxWorker.ts +213 -183
  43. package/core/remote/RemoteManager.ts +401 -400
  44. package/core/remote/StreamConsumer.ts +535 -535
  45. package/core/remote/types.ts +153 -151
  46. package/core/requestScope.ts +34 -0
  47. package/core/scheduler/cronEvaluator.ts +174 -0
  48. package/core/scheduler/lifecycleHooks.ts +21 -0
  49. package/core/scheduler/lockCoordinator.ts +27 -0
  50. package/core/scheduler/metrics.ts +14 -0
  51. package/core/scheduler/taskRunner.ts +420 -0
  52. package/core/validateEnv.ts +10 -0
  53. package/database/DatabaseHelper.ts +128 -101
  54. package/database/IndexingStrategy.ts +72 -2
  55. package/database/PreparedStatementCache.ts +8 -2
  56. package/database/cancellable.ts +35 -22
  57. package/database/index.ts +29 -3
  58. package/database/instrumentedDb.ts +141 -141
  59. package/database/sqlHelpers.ts +3 -1
  60. package/endpoints/archetypes.ts +2 -8
  61. package/endpoints/tables.ts +6 -1
  62. package/gql/index.ts +1 -1
  63. package/gql/schema/index.ts +15 -4
  64. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  65. package/package.json +22 -1
  66. package/query/CTENode.ts +5 -3
  67. package/query/ComponentInclusionNode.ts +245 -14
  68. package/query/OrNode.ts +8 -19
  69. package/query/Query.ts +208 -79
  70. package/query/QueryContext.ts +6 -0
  71. package/query/QueryDAG.ts +7 -2
  72. package/query/membershipSource.ts +66 -0
  73. package/storage/LocalStorageProvider.ts +8 -3
  74. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  75. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  76. package/studio/{index.html → dist/index.html} +3 -2
  77. package/swagger/generator.ts +11 -1
  78. package/upload/UploadManager.ts +8 -6
  79. package/utils/uuid.ts +40 -10
  80. package/.claude/scheduled_tasks.lock +0 -1
  81. package/.claude/settings.local.json +0 -47
  82. package/.prettierrc +0 -4
  83. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  84. package/.serena/memories/architecture.md +0 -154
  85. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  86. package/.serena/memories/code_style_and_conventions.md +0 -76
  87. package/.serena/memories/project_overview.md +0 -43
  88. package/.serena/memories/schema-dsl-plan.md +0 -107
  89. package/.serena/memories/suggested_commands.md +0 -80
  90. package/.serena/memories/typescript-compilation-status.md +0 -54
  91. package/.serena/project.yml +0 -114
  92. package/BunSane.jpg +0 -0
  93. package/CLAUDE.md +0 -198
  94. package/TODO.md +0 -2
  95. package/bun.lock +0 -302
  96. package/bunfig.toml +0 -10
  97. package/docs/RFC_APP_REFACTOR.md +0 -248
  98. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  99. package/docs/SCALABILITY_PLAN.md +0 -175
  100. package/studio/bun.lock +0 -482
  101. package/studio/package.json +0 -39
  102. package/studio/postcss.config.js +0 -6
  103. package/studio/src/components/DataTable.tsx +0 -211
  104. package/studio/src/components/Layout.tsx +0 -13
  105. package/studio/src/components/PageContainer.tsx +0 -9
  106. package/studio/src/components/PageHeader.tsx +0 -13
  107. package/studio/src/components/SearchBar.tsx +0 -57
  108. package/studio/src/components/Sidebar.tsx +0 -294
  109. package/studio/src/components/ui/button.tsx +0 -56
  110. package/studio/src/components/ui/checkbox.tsx +0 -26
  111. package/studio/src/components/ui/input.tsx +0 -25
  112. package/studio/src/hooks/useDataTable.ts +0 -131
  113. package/studio/src/index.css +0 -36
  114. package/studio/src/lib/api.ts +0 -186
  115. package/studio/src/lib/utils.ts +0 -13
  116. package/studio/src/main.tsx +0 -17
  117. package/studio/src/pages/ArcheType.tsx +0 -239
  118. package/studio/src/pages/Components.tsx +0 -124
  119. package/studio/src/pages/EntityInspector.tsx +0 -302
  120. package/studio/src/pages/QueryRunner.tsx +0 -246
  121. package/studio/src/pages/Table.tsx +0 -94
  122. package/studio/src/pages/Welcome.tsx +0 -241
  123. package/studio/src/routes.tsx +0 -45
  124. package/studio/src/store/archeTypeSettings.ts +0 -30
  125. package/studio/src/store/studio.ts +0 -65
  126. package/studio/src/utils/columnHelpers.tsx +0 -114
  127. package/studio/studio-instructions.md +0 -81
  128. package/studio/tailwind.config.js +0 -77
  129. package/studio/utils.ts +0 -54
  130. package/studio/vite.config.js +0 -19
  131. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  132. package/tests/benchmark/bunfig.toml +0 -9
  133. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  134. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  135. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  136. package/tests/benchmark/fixtures/index.ts +0 -6
  137. package/tests/benchmark/index.ts +0 -22
  138. package/tests/benchmark/noop-preload.ts +0 -3
  139. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  140. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  141. package/tests/benchmark/runners/index.ts +0 -4
  142. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  143. package/tests/benchmark/scripts/generate-db.ts +0 -344
  144. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  145. package/tests/e2e/http.test.ts +0 -130
  146. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  147. package/tests/fixtures/components/TestOrder.ts +0 -23
  148. package/tests/fixtures/components/TestProduct.ts +0 -23
  149. package/tests/fixtures/components/TestUser.ts +0 -20
  150. package/tests/fixtures/components/index.ts +0 -6
  151. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  152. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  153. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  154. package/tests/helpers/MockRedisClient.ts +0 -113
  155. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  156. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  157. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  158. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  159. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  160. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  161. package/tests/integration/query/Query.abort.test.ts +0 -66
  162. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  163. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  164. package/tests/integration/query/Query.exec.test.ts +0 -576
  165. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  166. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  167. package/tests/integration/remote/dlq.test.ts +0 -175
  168. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  169. package/tests/integration/remote/outbox.test.ts +0 -130
  170. package/tests/integration/remote/rpc.test.ts +0 -177
  171. package/tests/pglite-setup.ts +0 -62
  172. package/tests/setup.ts +0 -164
  173. package/tests/stress/BenchmarkRunner.ts +0 -203
  174. package/tests/stress/DataSeeder.ts +0 -190
  175. package/tests/stress/StressTestReporter.ts +0 -229
  176. package/tests/stress/cursor-perf-test.ts +0 -171
  177. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  178. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  179. package/tests/stress/index.ts +0 -7
  180. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  181. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  182. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  183. package/tests/unit/BatchLoader.test.ts +0 -196
  184. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  185. package/tests/unit/cache/CacheManager.test.ts +0 -498
  186. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  187. package/tests/unit/cache/RedisCache.test.ts +0 -411
  188. package/tests/unit/database/cancellable.test.ts +0 -81
  189. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  190. package/tests/unit/entity/Entity.components.test.ts +0 -317
  191. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  192. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  193. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  194. package/tests/unit/entity/Entity.test.ts +0 -345
  195. package/tests/unit/gql/depthLimit.test.ts +0 -203
  196. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  197. package/tests/unit/health/Health.test.ts +0 -129
  198. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  199. package/tests/unit/middleware/Middleware.test.ts +0 -98
  200. package/tests/unit/middleware/RequestId.test.ts +0 -54
  201. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  202. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  203. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  204. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  205. package/tests/unit/query/Query.test.ts +0 -310
  206. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  207. package/tests/unit/remote/RemoteError.test.ts +0 -55
  208. package/tests/unit/remote/decorators.test.ts +0 -195
  209. package/tests/unit/remote/metrics.test.ts +0 -115
  210. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  211. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  212. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  213. package/tests/unit/schema/schema-integration.test.ts +0 -426
  214. package/tests/unit/schema/schema.test.ts +0 -580
  215. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  216. package/tests/unit/upload/RestUpload.test.ts +0 -267
  217. package/tests/unit/validateEnv.test.ts +0 -82
  218. package/tests/utils/entity-tracker.ts +0 -57
  219. package/tests/utils/index.ts +0 -13
  220. 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
- });