bunsane 0.3.1 → 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 (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. package/tests/utils/test-context.ts +0 -149
@@ -1,196 +0,0 @@
1
- /**
2
- * Unit tests for BatchLoader TTL behavior and bounded cache
3
- */
4
- import { describe, test, expect, beforeEach } from 'bun:test';
5
- import { BatchLoader } from '../../core/BatchLoader';
6
-
7
- describe('BatchLoader', () => {
8
- beforeEach(() => {
9
- BatchLoader.clearCache();
10
- });
11
-
12
- describe('getCacheStats()', () => {
13
- test('returns zero stats when cache is empty', () => {
14
- const stats = BatchLoader.getCacheStats();
15
- expect(stats.size).toBe(0);
16
- expect(stats.entries).toBe(0);
17
- expect(stats.expired).toBe(0);
18
- });
19
-
20
- test('includes memory estimate', () => {
21
- const stats = BatchLoader.getCacheStats();
22
- expect(stats.memoryEstimate).toBeDefined();
23
- expect(typeof stats.memoryEstimate).toBe('string');
24
- });
25
- });
26
-
27
- describe('clearCache()', () => {
28
- test('clears all cached entries', () => {
29
- // Access internal cache to set up test data
30
- const cache = (BatchLoader as any).cache;
31
- const now = Date.now();
32
-
33
- // Use the cache's set method to add entries
34
- cache.set('type1\x00value', 'parent1', {
35
- ids: ['a', 'b'],
36
- expiresAt: now + 300000,
37
- lastAccessed: now
38
- });
39
-
40
- expect(BatchLoader.getCacheStats().entries).toBe(1);
41
-
42
- BatchLoader.clearCache();
43
- expect(BatchLoader.getCacheStats().entries).toBe(0);
44
- });
45
- });
46
-
47
- describe('TTL expiry', () => {
48
- test('getCacheStats reports expired entries', () => {
49
- const cache = (BatchLoader as any).cache;
50
- const now = Date.now();
51
-
52
- // One fresh entry
53
- cache.set('type1\x00value', 'parent1', {
54
- ids: ['a'],
55
- expiresAt: now + 300000,
56
- lastAccessed: now
57
- });
58
- // One expired entry
59
- cache.set('type1\x00value', 'parent2', {
60
- ids: ['b'],
61
- expiresAt: now - 1000,
62
- lastAccessed: now - 1000
63
- });
64
-
65
- const stats = BatchLoader.getCacheStats();
66
- expect(stats.entries).toBe(2);
67
- expect(stats.expired).toBe(1);
68
- });
69
-
70
- test('expired entries are counted correctly for multiple type keys', () => {
71
- const cache = (BatchLoader as any).cache;
72
- const now = Date.now();
73
-
74
- // Type 1 entries
75
- cache.set('type1\x00field', 'p1', {
76
- ids: ['a'],
77
- expiresAt: now - 5000,
78
- lastAccessed: now - 5000
79
- });
80
- cache.set('type1\x00field', 'p2', {
81
- ids: ['b'],
82
- expiresAt: now + 300000,
83
- lastAccessed: now
84
- });
85
-
86
- // Type 2 entry
87
- cache.set('type2\x00field', 'p3', {
88
- ids: ['c'],
89
- expiresAt: now - 1000,
90
- lastAccessed: now - 1000
91
- });
92
-
93
- const stats = BatchLoader.getCacheStats();
94
- expect(stats.size).toBe(2);
95
- expect(stats.entries).toBe(3);
96
- expect(stats.expired).toBe(2);
97
- });
98
-
99
- test('all entries fresh means zero expired', () => {
100
- const cache = (BatchLoader as any).cache;
101
- const now = Date.now();
102
-
103
- cache.set('type\x00field', 'p1', {
104
- ids: ['a'],
105
- expiresAt: now + 60000,
106
- lastAccessed: now
107
- });
108
- cache.set('type\x00field', 'p2', {
109
- ids: ['b'],
110
- expiresAt: now + 60000,
111
- lastAccessed: now
112
- });
113
- cache.set('type\x00field', 'p3', {
114
- ids: ['c'],
115
- expiresAt: now + 60000,
116
- lastAccessed: now
117
- });
118
-
119
- const stats = BatchLoader.getCacheStats();
120
- expect(stats.entries).toBe(3);
121
- expect(stats.expired).toBe(0);
122
- });
123
- });
124
-
125
- describe('pruneExpiredEntries()', () => {
126
- test('removes expired entries and returns count', () => {
127
- const cache = (BatchLoader as any).cache;
128
- const now = Date.now();
129
-
130
- // Add mix of fresh and expired entries
131
- cache.set('type\x00field', 'p1', {
132
- ids: ['a'],
133
- expiresAt: now - 5000,
134
- lastAccessed: now - 5000
135
- });
136
- cache.set('type\x00field', 'p2', {
137
- ids: ['b'],
138
- expiresAt: now + 300000,
139
- lastAccessed: now
140
- });
141
- cache.set('type\x00field', 'p3', {
142
- ids: ['c'],
143
- expiresAt: now - 1000,
144
- lastAccessed: now - 1000
145
- });
146
-
147
- expect(BatchLoader.getCacheStats().entries).toBe(3);
148
- expect(BatchLoader.getCacheStats().expired).toBe(2);
149
-
150
- const pruned = BatchLoader.pruneExpiredEntries();
151
- expect(pruned).toBe(2);
152
-
153
- const statsAfter = BatchLoader.getCacheStats();
154
- expect(statsAfter.entries).toBe(1);
155
- expect(statsAfter.expired).toBe(0);
156
- });
157
-
158
- test('returns 0 when no expired entries', () => {
159
- const cache = (BatchLoader as any).cache;
160
- const now = Date.now();
161
-
162
- cache.set('type\x00field', 'p1', {
163
- ids: ['a'],
164
- expiresAt: now + 60000,
165
- lastAccessed: now
166
- });
167
-
168
- const pruned = BatchLoader.pruneExpiredEntries();
169
- expect(pruned).toBe(0);
170
- });
171
- });
172
-
173
- describe('bounded cache behavior', () => {
174
- test('memory estimate increases with entries', () => {
175
- const cache = (BatchLoader as any).cache;
176
- const now = Date.now();
177
-
178
- const statsBefore = BatchLoader.getCacheStats();
179
-
180
- // Add several entries
181
- for (let i = 0; i < 100; i++) {
182
- cache.set(`type${i}\x00field`, `parent${i}`, {
183
- ids: [`id${i}`],
184
- expiresAt: now + 60000,
185
- lastAccessed: now
186
- });
187
- }
188
-
189
- const statsAfter = BatchLoader.getCacheStats();
190
- expect(statsAfter.entries).toBe(100);
191
-
192
- // Memory estimate should reflect the entries
193
- expect(statsAfter.memoryEstimate).toBeDefined();
194
- });
195
- });
196
- });
@@ -1,107 +0,0 @@
1
- /**
2
- * Unit tests for ArcheType system
3
- * Tests archetype definition and basic functionality
4
- */
5
- import { describe, test, expect, beforeAll } from 'bun:test';
6
- import { BaseArcheType, ArcheType, ArcheTypeField } from '../../../core/ArcheType';
7
- import { TestUser, TestProduct, TestOrder } from '../../fixtures/components';
8
- import { TestUserArchetype, TestUserWithOrdersArchetype } from '../../fixtures/archetypes/TestUserArchetype';
9
- import { ensureComponentsRegistered } from '../../utils';
10
-
11
- describe('ArcheType', () => {
12
- beforeAll(async () => {
13
- await ensureComponentsRegistered(TestUser, TestProduct, TestOrder);
14
- });
15
-
16
- describe('ArcheType definition', () => {
17
- test('archetype class extends BaseArcheType', () => {
18
- const archetype = new TestUserArchetype();
19
- expect(archetype).toBeInstanceOf(BaseArcheType);
20
- });
21
-
22
- test('archetype has componentMap', () => {
23
- const archetype = new TestUserArchetype();
24
- expect(archetype.componentMap).toBeDefined();
25
- expect(typeof archetype.componentMap).toBe('object');
26
- });
27
-
28
- test('componentMap contains declared fields', () => {
29
- const archetype = new TestUserArchetype();
30
- expect(archetype.componentMap.user).toBeDefined();
31
- });
32
-
33
- test('archetype with multiple components', () => {
34
- const archetype = new TestUserWithOrdersArchetype();
35
- expect(archetype.componentMap.user).toBeDefined();
36
- expect(archetype.componentMap.order).toBeDefined();
37
- });
38
- });
39
-
40
- describe('createEntity()', () => {
41
- test('creates entity with id', () => {
42
- const archetype = new TestUserArchetype();
43
- const entity = archetype.createEntity();
44
-
45
- expect(entity).toBeDefined();
46
- expect(entity.id).toBeDefined();
47
- expect(entity.id.length).toBeGreaterThan(0);
48
- });
49
-
50
- test('entity is dirty after creation', () => {
51
- const archetype = new TestUserArchetype();
52
- const entity = archetype.createEntity();
53
-
54
- expect((entity as any)._dirty).toBe(true);
55
- });
56
-
57
- test('entity is not persisted after creation', () => {
58
- const archetype = new TestUserArchetype();
59
- const entity = archetype.createEntity();
60
-
61
- expect(entity._persisted).toBe(false);
62
- });
63
- });
64
-
65
- describe('getZodObjectSchema()', () => {
66
- test('returns zod schema for archetype', () => {
67
- const archetype = new TestUserArchetype();
68
- const schema = archetype.getZodObjectSchema();
69
-
70
- expect(schema).toBeDefined();
71
- });
72
- });
73
-
74
- describe('getInputSchema()', () => {
75
- test('returns input schema for archetype', () => {
76
- const archetype = new TestUserArchetype();
77
- const schema = archetype.getInputSchema();
78
-
79
- expect(schema).toBeDefined();
80
- });
81
- });
82
-
83
- describe('getComponentsToLoad()', () => {
84
- test('returns non-empty components array', () => {
85
- const archetype = new TestUserArchetype();
86
- const components = (archetype as any).getComponentsToLoad();
87
-
88
- expect(Array.isArray(components)).toBe(true);
89
- expect(components.length).toBeGreaterThan(0);
90
- expect(components).toContain(TestUser);
91
- });
92
- });
93
-
94
- describe('withValidation()', () => {
95
- test('returns a Zod schema with validations applied', () => {
96
- const archetype = new TestUserArchetype();
97
- const schema = archetype.withValidation({
98
- user: { name: 'Valid', email: 'valid@test.com', age: 25 }
99
- });
100
-
101
- expect(schema).toBeDefined();
102
- // Should return a Zod schema with a shape property
103
- expect(schema.shape).toBeDefined();
104
- expect(typeof schema.safeParse).toBe('function');
105
- });
106
- });
107
- });
@@ -1,367 +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 } 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('cache disabled', () => {
214
- beforeEach(async () => {
215
- await cacheManager.initialize({ enabled: false });
216
- });
217
-
218
- test('get returns null when disabled', async () => {
219
- const result = await cacheManager.get('key');
220
- expect(result).toBeNull();
221
- });
222
-
223
- test('set does nothing when disabled', async () => {
224
- await cacheManager.set('key', 'value');
225
- // Re-enable to check
226
- await cacheManager.initialize({ enabled: true, provider: 'memory' });
227
- const result = await cacheManager.get('key');
228
- expect(result).toBeNull();
229
- });
230
-
231
- test('getEntity returns null when disabled', async () => {
232
- const result = await cacheManager.getEntity('id');
233
- expect(result).toBeNull();
234
- });
235
-
236
- test('getComponentsByEntity returns null when disabled', async () => {
237
- const result = await cacheManager.getComponentsByEntity('id');
238
- expect(result).toBeNull();
239
- });
240
- });
241
-
242
- describe('getStats()', () => {
243
- test('returns statistics object', async () => {
244
- const stats = await cacheManager.getStats();
245
- expect(stats).toBeDefined();
246
- expect(typeof stats.hits).toBe('number');
247
- expect(typeof stats.misses).toBe('number');
248
- });
249
- });
250
-
251
- describe('ping()', () => {
252
- test('returns true for healthy cache', async () => {
253
- const result = await cacheManager.ping();
254
- expect(result).toBe(true);
255
- });
256
- });
257
-
258
- describe('shutdown()', () => {
259
- test('calls stopCleanup on MemoryCache provider', async () => {
260
- await cacheManager.initialize({
261
- enabled: true,
262
- provider: 'memory',
263
- defaultTTL: 3600000
264
- });
265
-
266
- const provider = cacheManager.getProvider() as MemoryCache;
267
- const stopCleanupSpy = mock(() => {});
268
- (provider as any).stopCleanup = stopCleanupSpy;
269
-
270
- await cacheManager.shutdown();
271
- expect(stopCleanupSpy).toHaveBeenCalled();
272
- });
273
-
274
- test('shutdown does not throw on NoOp provider', async () => {
275
- await cacheManager.initialize({ enabled: false });
276
- await expect(cacheManager.shutdown()).resolves.toBeUndefined();
277
- });
278
- });
279
-
280
- describe('initialize() cleanup', () => {
281
- test('shuts down old provider when reinitializing', async () => {
282
- await cacheManager.initialize({
283
- enabled: true,
284
- provider: 'memory',
285
- defaultTTL: 3600000
286
- });
287
-
288
- const oldProvider = cacheManager.getProvider() as MemoryCache;
289
- const stopCleanupSpy = mock(() => {});
290
- (oldProvider as any).stopCleanup = stopCleanupSpy;
291
-
292
- // Reinitialize with new config
293
- await cacheManager.initialize({
294
- enabled: true,
295
- provider: 'memory',
296
- defaultTTL: 5000
297
- });
298
-
299
- expect(stopCleanupSpy).toHaveBeenCalled();
300
- });
301
- });
302
-
303
- describe('cross-instance invalidation (pub/sub)', () => {
304
- test('pub/sub not enabled for memory-only provider', async () => {
305
- await cacheManager.initialize({
306
- enabled: true,
307
- provider: 'memory',
308
- defaultTTL: 3600000
309
- });
310
-
311
- // pubSubEnabled is private, so we test indirectly:
312
- // publishInvalidation should be a no-op (no errors, no side effects)
313
- await cacheManager.set('test-key', 'value');
314
- await cacheManager.delete('test-key');
315
- // If pub/sub were broken for memory provider, this would throw
316
- const result = await cacheManager.get('test-key');
317
- expect(result).toBeNull();
318
- });
319
-
320
- test('invalidateEntity still works without pub/sub', async () => {
321
- const provider = cacheManager.getProvider();
322
- await provider.set('entity:abc', 'abc', 3600000);
323
-
324
- await cacheManager.invalidateEntity('abc');
325
- const result = await provider.get('entity:abc');
326
- expect(result).toBeNull();
327
- });
328
-
329
- test('invalidateComponent still works without pub/sub', async () => {
330
- const provider = cacheManager.getProvider();
331
- await provider.set('component:e1:t1', { data: 'test' }, 3600000);
332
-
333
- await cacheManager.invalidateComponent('e1', 't1');
334
- const result = await provider.get('component:e1:t1');
335
- expect(result).toBeNull();
336
- });
337
-
338
- test('clear still works without pub/sub', async () => {
339
- await cacheManager.set('key1', 'v1');
340
- await cacheManager.set('key2', 'v2');
341
-
342
- await cacheManager.clear();
343
- expect(await cacheManager.get('key1')).toBeNull();
344
- expect(await cacheManager.get('key2')).toBeNull();
345
- });
346
-
347
- test('handleRemoteInvalidation ignores messages from self', async () => {
348
- // Access private method via any
349
- const cm = cacheManager as any;
350
- const myId = cm.instanceId;
351
-
352
- // Simulate receiving our own message — should NOT invalidate
353
- await cacheManager.set('survive-key', 'should-survive');
354
-
355
- // Call private method directly
356
- await cm.handleRemoteInvalidation(JSON.stringify({
357
- instanceId: myId,
358
- type: 'key',
359
- keys: ['survive-key']
360
- }));
361
-
362
- // Key should still exist because self-messages are ignored
363
- const result = await cacheManager.get('survive-key');
364
- expect(result).toBe('should-survive');
365
- });
366
- });
367
- });