bunsane 0.1.4 → 0.2.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 (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. package/tests/scheduler.test.ts +0 -724
@@ -0,0 +1,347 @@
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('getEntities returns null for missing entities', async () => {
145
+ const results = await cacheManager.getEntities(['id1', 'id2', 'id3']);
146
+ expect(results.length).toBe(3);
147
+ expect(results.every(r => r === null)).toBe(true);
148
+ });
149
+ });
150
+
151
+ describe('component cache operations', () => {
152
+ test('getComponentsByEntity returns null for missing components', async () => {
153
+ const result = await cacheManager.getComponentsByEntity('entity-id');
154
+ expect(result).toBeNull();
155
+ });
156
+
157
+ test('invalidateComponent removes specific component', async () => {
158
+ // Manually set component cache
159
+ const provider = cacheManager.getProvider();
160
+ await provider.set('component:entity-id:type-id', { data: 'test' }, 3600000);
161
+
162
+ await cacheManager.invalidateComponent('entity-id', 'type-id');
163
+ const result = await cacheManager.getComponentsByEntity('entity-id', 'type-id');
164
+ expect(result).toBeNull();
165
+ });
166
+
167
+ test('invalidateComponents removes multiple components', async () => {
168
+ const provider = cacheManager.getProvider();
169
+ await provider.set('component:e1:t1', { data: 'test1' }, 3600000);
170
+ await provider.set('component:e2:t2', { data: 'test2' }, 3600000);
171
+
172
+ await cacheManager.invalidateComponents([
173
+ { entityId: 'e1', typeId: 't1' },
174
+ { entityId: 'e2', typeId: 't2' }
175
+ ]);
176
+
177
+ const result1 = await provider.get('component:e1:t1');
178
+ const result2 = await provider.get('component:e2:t2');
179
+ expect(result1).toBeNull();
180
+ expect(result2).toBeNull();
181
+ });
182
+
183
+ test('getComponents returns null for missing components', async () => {
184
+ const results = await cacheManager.getComponents([
185
+ { entityId: 'e1', typeId: 't1' },
186
+ { entityId: 'e2', typeId: 't2' }
187
+ ]);
188
+ expect(results.length).toBe(2);
189
+ expect(results.every(r => r === null)).toBe(true);
190
+ });
191
+ });
192
+
193
+ describe('cache disabled', () => {
194
+ beforeEach(async () => {
195
+ await cacheManager.initialize({ enabled: false });
196
+ });
197
+
198
+ test('get returns null when disabled', async () => {
199
+ const result = await cacheManager.get('key');
200
+ expect(result).toBeNull();
201
+ });
202
+
203
+ test('set does nothing when disabled', async () => {
204
+ await cacheManager.set('key', 'value');
205
+ // Re-enable to check
206
+ await cacheManager.initialize({ enabled: true, provider: 'memory' });
207
+ const result = await cacheManager.get('key');
208
+ expect(result).toBeNull();
209
+ });
210
+
211
+ test('getEntity returns null when disabled', async () => {
212
+ const result = await cacheManager.getEntity('id');
213
+ expect(result).toBeNull();
214
+ });
215
+
216
+ test('getComponentsByEntity returns null when disabled', async () => {
217
+ const result = await cacheManager.getComponentsByEntity('id');
218
+ expect(result).toBeNull();
219
+ });
220
+ });
221
+
222
+ describe('getStats()', () => {
223
+ test('returns statistics object', async () => {
224
+ const stats = await cacheManager.getStats();
225
+ expect(stats).toBeDefined();
226
+ expect(typeof stats.hits).toBe('number');
227
+ expect(typeof stats.misses).toBe('number');
228
+ });
229
+ });
230
+
231
+ describe('ping()', () => {
232
+ test('returns true for healthy cache', async () => {
233
+ const result = await cacheManager.ping();
234
+ expect(result).toBe(true);
235
+ });
236
+ });
237
+
238
+ describe('shutdown()', () => {
239
+ test('calls stopCleanup on MemoryCache provider', async () => {
240
+ await cacheManager.initialize({
241
+ enabled: true,
242
+ provider: 'memory',
243
+ defaultTTL: 3600000
244
+ });
245
+
246
+ const provider = cacheManager.getProvider() as MemoryCache;
247
+ const stopCleanupSpy = mock(() => {});
248
+ (provider as any).stopCleanup = stopCleanupSpy;
249
+
250
+ await cacheManager.shutdown();
251
+ expect(stopCleanupSpy).toHaveBeenCalled();
252
+ });
253
+
254
+ test('shutdown does not throw on NoOp provider', async () => {
255
+ await cacheManager.initialize({ enabled: false });
256
+ await expect(cacheManager.shutdown()).resolves.toBeUndefined();
257
+ });
258
+ });
259
+
260
+ describe('initialize() cleanup', () => {
261
+ test('shuts down old provider when reinitializing', async () => {
262
+ await cacheManager.initialize({
263
+ enabled: true,
264
+ provider: 'memory',
265
+ defaultTTL: 3600000
266
+ });
267
+
268
+ const oldProvider = cacheManager.getProvider() as MemoryCache;
269
+ const stopCleanupSpy = mock(() => {});
270
+ (oldProvider as any).stopCleanup = stopCleanupSpy;
271
+
272
+ // Reinitialize with new config
273
+ await cacheManager.initialize({
274
+ enabled: true,
275
+ provider: 'memory',
276
+ defaultTTL: 5000
277
+ });
278
+
279
+ expect(stopCleanupSpy).toHaveBeenCalled();
280
+ });
281
+ });
282
+
283
+ describe('cross-instance invalidation (pub/sub)', () => {
284
+ test('pub/sub not enabled for memory-only provider', async () => {
285
+ await cacheManager.initialize({
286
+ enabled: true,
287
+ provider: 'memory',
288
+ defaultTTL: 3600000
289
+ });
290
+
291
+ // pubSubEnabled is private, so we test indirectly:
292
+ // publishInvalidation should be a no-op (no errors, no side effects)
293
+ await cacheManager.set('test-key', 'value');
294
+ await cacheManager.delete('test-key');
295
+ // If pub/sub were broken for memory provider, this would throw
296
+ const result = await cacheManager.get('test-key');
297
+ expect(result).toBeNull();
298
+ });
299
+
300
+ test('invalidateEntity still works without pub/sub', async () => {
301
+ const provider = cacheManager.getProvider();
302
+ await provider.set('entity:abc', 'abc', 3600000);
303
+
304
+ await cacheManager.invalidateEntity('abc');
305
+ const result = await provider.get('entity:abc');
306
+ expect(result).toBeNull();
307
+ });
308
+
309
+ test('invalidateComponent still works without pub/sub', async () => {
310
+ const provider = cacheManager.getProvider();
311
+ await provider.set('component:e1:t1', { data: 'test' }, 3600000);
312
+
313
+ await cacheManager.invalidateComponent('e1', 't1');
314
+ const result = await provider.get('component:e1:t1');
315
+ expect(result).toBeNull();
316
+ });
317
+
318
+ test('clear still works without pub/sub', async () => {
319
+ await cacheManager.set('key1', 'v1');
320
+ await cacheManager.set('key2', 'v2');
321
+
322
+ await cacheManager.clear();
323
+ expect(await cacheManager.get('key1')).toBeNull();
324
+ expect(await cacheManager.get('key2')).toBeNull();
325
+ });
326
+
327
+ test('handleRemoteInvalidation ignores messages from self', async () => {
328
+ // Access private method via any
329
+ const cm = cacheManager as any;
330
+ const myId = cm.instanceId;
331
+
332
+ // Simulate receiving our own message — should NOT invalidate
333
+ await cacheManager.set('survive-key', 'should-survive');
334
+
335
+ // Call private method directly
336
+ await cm.handleRemoteInvalidation(JSON.stringify({
337
+ instanceId: myId,
338
+ type: 'key',
339
+ keys: ['survive-key']
340
+ }));
341
+
342
+ // Key should still exist because self-messages are ignored
343
+ const result = await cacheManager.get('survive-key');
344
+ expect(result).toBe('should-survive');
345
+ });
346
+ });
347
+ });
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Unit tests for MemoryCache
3
+ * Tests in-memory cache provider functionality
4
+ */
5
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
6
+ import { MemoryCache } from '../../../core/cache/MemoryCache';
7
+
8
+ describe('MemoryCache', () => {
9
+ let cache: MemoryCache;
10
+
11
+ beforeEach(() => {
12
+ cache = new MemoryCache({
13
+ maxSize: 1000,
14
+ maxMemory: 10 * 1024 * 1024, // 10MB
15
+ defaultTTL: 3600000,
16
+ cleanupInterval: 60000
17
+ });
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await cache.clear();
22
+ cache.stopCleanup();
23
+ });
24
+
25
+ describe('constructor', () => {
26
+ test('creates cache with default config', () => {
27
+ const defaultCache = new MemoryCache();
28
+ expect(defaultCache).toBeDefined();
29
+ defaultCache.stopCleanup();
30
+ });
31
+
32
+ test('creates cache with custom config', () => {
33
+ const customCache = new MemoryCache({
34
+ maxSize: 500,
35
+ defaultTTL: 1000
36
+ });
37
+ expect(customCache).toBeDefined();
38
+ customCache.stopCleanup();
39
+ });
40
+ });
41
+
42
+ describe('get() and set()', () => {
43
+ test('returns null for non-existent key', async () => {
44
+ const result = await cache.get('non-existent');
45
+ expect(result).toBeNull();
46
+ });
47
+
48
+ test('sets and retrieves value', async () => {
49
+ await cache.set('key', 'value', 3600000);
50
+ const result = await cache.get<string>('key');
51
+ expect(result).toBe('value');
52
+ });
53
+
54
+ test('sets and retrieves object', async () => {
55
+ const obj = { name: 'test', value: 123 };
56
+ await cache.set('obj-key', obj, 3600000);
57
+ const result = await cache.get<typeof obj>('obj-key');
58
+ expect(result).toEqual(obj);
59
+ });
60
+
61
+ test('sets and retrieves array', async () => {
62
+ const arr = [1, 2, 3, 'four'];
63
+ await cache.set('arr-key', arr, 3600000);
64
+ const result = await cache.get<typeof arr>('arr-key');
65
+ expect(result).toEqual(arr);
66
+ });
67
+
68
+ test('expires after TTL', async () => {
69
+ await cache.set('expire-key', 'value', 50); // 50ms TTL
70
+ const immediate = await cache.get('expire-key');
71
+ expect(immediate).toBe('value');
72
+
73
+ await new Promise(resolve => setTimeout(resolve, 100));
74
+ const expired = await cache.get('expire-key');
75
+ expect(expired).toBeNull();
76
+ });
77
+
78
+ test('overwrites existing key', async () => {
79
+ await cache.set('key', 'original', 3600000);
80
+ await cache.set('key', 'updated', 3600000);
81
+ const result = await cache.get('key');
82
+ expect(result).toBe('updated');
83
+ });
84
+ });
85
+
86
+ describe('delete()', () => {
87
+ test('removes single key', async () => {
88
+ await cache.set('key', 'value', 3600000);
89
+ await cache.delete('key');
90
+ const result = await cache.get('key');
91
+ expect(result).toBeNull();
92
+ });
93
+
94
+ test('removes multiple keys', async () => {
95
+ await cache.set('key1', 'value1', 3600000);
96
+ await cache.set('key2', 'value2', 3600000);
97
+ await cache.delete(['key1', 'key2']);
98
+
99
+ const result1 = await cache.get('key1');
100
+ const result2 = await cache.get('key2');
101
+ expect(result1).toBeNull();
102
+ expect(result2).toBeNull();
103
+ });
104
+
105
+ test('handles non-existent key gracefully', async () => {
106
+ // Should not throw and key should remain absent
107
+ await cache.delete('non-existent');
108
+ const result = await cache.get('non-existent');
109
+ expect(result).toBeNull();
110
+ });
111
+ });
112
+
113
+ describe('deleteMany()', () => {
114
+ test('removes multiple keys', async () => {
115
+ await cache.set('a', 1, 3600000);
116
+ await cache.set('b', 2, 3600000);
117
+ await cache.set('c', 3, 3600000);
118
+
119
+ await cache.deleteMany(['a', 'b']);
120
+
121
+ expect(await cache.get('a')).toBeNull();
122
+ expect(await cache.get('b')).toBeNull();
123
+ expect(await cache.get<number>('c')).toBe(3);
124
+ });
125
+ });
126
+
127
+ describe('getMany()', () => {
128
+ test('returns array of values', async () => {
129
+ await cache.set('k1', 'v1', 3600000);
130
+ await cache.set('k2', 'v2', 3600000);
131
+ await cache.set('k3', 'v3', 3600000);
132
+
133
+ const results = await cache.getMany(['k1', 'k2', 'k3']);
134
+
135
+ expect(results).toEqual(['v1', 'v2', 'v3']);
136
+ });
137
+
138
+ test('returns null for missing keys', async () => {
139
+ await cache.set('k1', 'v1', 3600000);
140
+
141
+ const results = await cache.getMany(['k1', 'missing', 'k3']);
142
+
143
+ expect(results[0]).toBe('v1');
144
+ expect(results[1]).toBeNull();
145
+ expect(results[2]).toBeNull();
146
+ });
147
+ });
148
+
149
+ describe('setMany()', () => {
150
+ test('sets multiple entries', async () => {
151
+ await cache.setMany([
152
+ { key: 'a', value: 1, ttl: 3600000 },
153
+ { key: 'b', value: 2, ttl: 3600000 },
154
+ { key: 'c', value: 3, ttl: 3600000 }
155
+ ]);
156
+
157
+ expect(await cache.get<number>('a')).toBe(1);
158
+ expect(await cache.get<number>('b')).toBe(2);
159
+ expect(await cache.get<number>('c')).toBe(3);
160
+ });
161
+ });
162
+
163
+ describe('clear()', () => {
164
+ test('removes all entries', async () => {
165
+ await cache.set('key1', 'value1', 3600000);
166
+ await cache.set('key2', 'value2', 3600000);
167
+ await cache.clear();
168
+
169
+ expect(await cache.get('key1')).toBeNull();
170
+ expect(await cache.get('key2')).toBeNull();
171
+ });
172
+ });
173
+
174
+ describe('invalidatePattern()', () => {
175
+ test('removes keys matching pattern', async () => {
176
+ await cache.set('prefix:1', 'v1', 3600000);
177
+ await cache.set('prefix:2', 'v2', 3600000);
178
+ await cache.set('other:1', 'o1', 3600000);
179
+
180
+ await cache.invalidatePattern('prefix:*');
181
+
182
+ expect(await cache.get('prefix:1')).toBeNull();
183
+ expect(await cache.get('prefix:2')).toBeNull();
184
+ expect(await cache.get<string>('other:1')).toBe('o1');
185
+ });
186
+
187
+ test('handles complex patterns', async () => {
188
+ await cache.set('component:entity1:type1', 'c1', 3600000);
189
+ await cache.set('component:entity1:type2', 'c2', 3600000);
190
+ await cache.set('component:entity2:type1', 'c3', 3600000);
191
+
192
+ await cache.invalidatePattern('component:entity1:*');
193
+
194
+ expect(await cache.get('component:entity1:type1')).toBeNull();
195
+ expect(await cache.get('component:entity1:type2')).toBeNull();
196
+ expect(await cache.get<string>('component:entity2:type1')).toBe('c3');
197
+ });
198
+ });
199
+
200
+ describe('getStats()', () => {
201
+ test('returns statistics', async () => {
202
+ await cache.set('key', 'value', 3600000);
203
+ await cache.get('key'); // Hit
204
+ await cache.get('missing'); // Miss
205
+
206
+ const stats = await cache.getStats();
207
+
208
+ expect(stats.hits).toBe(1);
209
+ expect(stats.misses).toBe(1);
210
+ expect(stats.hitRate).toBe(0.5);
211
+ expect(stats.size).toBe(1);
212
+ });
213
+
214
+ test('tracks size correctly', async () => {
215
+ await cache.set('key1', 'value1', 3600000);
216
+ await cache.set('key2', 'value2', 3600000);
217
+
218
+ const stats = await cache.getStats();
219
+ expect(stats.size).toBe(2);
220
+ });
221
+ });
222
+
223
+ describe('ping()', () => {
224
+ test('returns true', async () => {
225
+ const result = await cache.ping();
226
+ expect(result).toBe(true);
227
+ });
228
+ });
229
+
230
+ describe('LRU eviction', () => {
231
+ test('evicts least recently used when max size reached', async () => {
232
+ const smallCache = new MemoryCache({
233
+ maxSize: 3,
234
+ defaultTTL: 3600000
235
+ });
236
+
237
+ await smallCache.set('a', 1, 3600000);
238
+ await smallCache.set('b', 2, 3600000);
239
+ await smallCache.set('c', 3, 3600000);
240
+
241
+ // Access 'a' to make it recently used
242
+ await smallCache.get('a');
243
+
244
+ // Add fourth item, should evict 'b' (least recently used)
245
+ await smallCache.set('d', 4, 3600000);
246
+
247
+ const stats = await smallCache.getStats();
248
+ expect(stats.size).toBeLessThanOrEqual(3);
249
+
250
+ // 'b' was least recently used and should have been evicted
251
+ expect(await smallCache.get('b')).toBeNull();
252
+ // 'a', 'c', 'd' should still be present
253
+ expect(await smallCache.get<number>('a')).toBe(1);
254
+ expect(await smallCache.get<number>('c')).toBe(3);
255
+ expect(await smallCache.get<number>('d')).toBe(4);
256
+
257
+ smallCache.stopCleanup();
258
+ });
259
+ });
260
+ });