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,520 @@
1
+ import { type CacheProvider } from './CacheProvider';
2
+ import { type CacheConfig, defaultCacheConfig } from '../../config/cache.config';
3
+ import { CacheFactory } from './CacheFactory';
4
+ import { MultiLevelCache } from './MultiLevelCache';
5
+ import { RedisCache } from './RedisCache';
6
+ import { logger } from '../Logger';
7
+ import type { Entity } from '../Entity';
8
+ import type { BaseComponent } from '../components';
9
+ import type { ComponentData } from '../RequestLoaders';
10
+
11
+ interface InvalidationMessage {
12
+ instanceId: string;
13
+ type: 'key' | 'pattern';
14
+ keys?: string[];
15
+ pattern?: string;
16
+ }
17
+
18
+ /**
19
+ * High-level cache operations manager
20
+ * Singleton that provides entity and component caching methods
21
+ * Note: Query-level caching has been removed in favor of component-level caching only
22
+ */
23
+ export class CacheManager {
24
+ private static instance: CacheManager;
25
+ private provider: CacheProvider;
26
+ private config: CacheConfig;
27
+ private instanceId = crypto.randomUUID();
28
+ private pubSubEnabled = false;
29
+ private static readonly INVALIDATION_CHANNEL = 'bunsane:cache:invalidate';
30
+
31
+ private constructor() {
32
+ this.config = defaultCacheConfig;
33
+ this.provider = CacheFactory.create(this.config);
34
+ }
35
+
36
+ public static getInstance(): CacheManager {
37
+ if (!CacheManager.instance) {
38
+ CacheManager.instance = new CacheManager();
39
+ }
40
+ return CacheManager.instance;
41
+ }
42
+
43
+ /**
44
+ * Initialize or reinitialize the cache manager with new config
45
+ */
46
+ public async initialize(config: Partial<CacheConfig>): Promise<void> {
47
+ // Shutdown old provider before replacing
48
+ await this.shutdownProvider();
49
+ this.pubSubEnabled = false;
50
+
51
+ this.config = { ...defaultCacheConfig, ...config };
52
+ this.provider = CacheFactory.create(this.config);
53
+
54
+ await this.setupPubSub();
55
+
56
+ logger.info({ scope: 'cache', component: 'CacheManager', msg: 'CacheManager initialized', provider: this.config.provider, enabled: this.config.enabled });
57
+ }
58
+
59
+ /**
60
+ * Get the current cache configuration
61
+ */
62
+ public getConfig(): CacheConfig {
63
+ return { ...this.config };
64
+ }
65
+
66
+ /**
67
+ * Get the current cache provider
68
+ */
69
+ public getProvider(): CacheProvider {
70
+ return this.provider;
71
+ }
72
+
73
+ // Entity caching methods
74
+
75
+ /**
76
+ * Get an entity existence check from cache
77
+ * Returns entity ID if exists, null if not found
78
+ */
79
+ public async getEntity(id: string): Promise<string | null> {
80
+ if (!this.config.enabled || !this.config.entity?.enabled) {
81
+ return null;
82
+ }
83
+
84
+ try {
85
+ const key = `entity:${id}`;
86
+ const result = await this.provider.get<string>(key);
87
+ return result || null;
88
+ } catch (error) {
89
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting entity from cache', error });
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Set an entity existence in cache with write-through strategy
96
+ * Only caches entity ID for existence tracking, not full entity data
97
+ */
98
+ public async setEntityWriteThrough(entity: Entity, ttl?: number): Promise<void> {
99
+ if (!this.config.enabled || !this.config.entity?.enabled) {
100
+ return;
101
+ }
102
+
103
+ try {
104
+ const key = `entity:${entity.id}`;
105
+ const effectiveTTL = ttl ?? this.config.entity.ttl;
106
+ // Only cache entity ID for existence check
107
+ await this.provider.set(key, entity.id, effectiveTTL);
108
+ } catch (error) {
109
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting entity in cache', error });
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Invalidate an entity from cache
115
+ */
116
+ public async invalidateEntity(id: string): Promise<void> {
117
+ if (!this.config.enabled || !this.config.entity?.enabled) {
118
+ return;
119
+ }
120
+
121
+ try {
122
+ const key = `entity:${id}`;
123
+ await this.provider.delete(key);
124
+ await this.publishInvalidation('key', [key]);
125
+ } catch (error) {
126
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error invalidating entity from cache', error });
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Get multiple entity existence checks from cache (for DataLoader integration)
132
+ * Returns entity IDs if they exist, null if not found
133
+ */
134
+ public async getEntities(ids: string[]): Promise<(string | null)[]> {
135
+ if (!this.config.enabled || !this.config.entity?.enabled) {
136
+ return ids.map(() => null);
137
+ }
138
+
139
+ try {
140
+ const cacheKeys = ids.map(id => `entity:${id}`);
141
+ const results = await this.provider.getMany<string>(cacheKeys);
142
+ return results;
143
+ } catch (error) {
144
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting entities from cache', error });
145
+ return ids.map(() => null);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Set multiple entity existences in cache with write-through strategy (for DataLoader integration)
151
+ * Only caches entity IDs for existence tracking, not full entity data
152
+ */
153
+ public async setEntitiesWriteThrough(entities: Entity[], ttl?: number): Promise<void> {
154
+ if (!this.config.enabled || !this.config.entity?.enabled) {
155
+ return;
156
+ }
157
+
158
+ try {
159
+ const effectiveTTL = ttl ?? this.config.entity?.ttl;
160
+ const entries = entities.map(entity => ({
161
+ key: `entity:${entity.id}`,
162
+ // Only cache entity ID for existence check
163
+ value: entity.id,
164
+ ttl: effectiveTTL
165
+ }));
166
+ await this.provider.setMany(entries);
167
+ } catch (error) {
168
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting entities in cache', error });
169
+ }
170
+ }
171
+
172
+ // Component caching methods
173
+
174
+ /**
175
+ * Get components for an entity from cache
176
+ */
177
+ public async getComponentsByEntity(entityId: string, componentType?: string): Promise<BaseComponent[] | null> {
178
+ if (!this.config.enabled || !this.config.component?.enabled) {
179
+ return null;
180
+ }
181
+
182
+ try {
183
+ const key = componentType
184
+ ? `component:${entityId}:${componentType}`
185
+ : `components:${entityId}`;
186
+ return await this.provider.get<BaseComponent[]>(key);
187
+ } catch (error) {
188
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting components from cache', error });
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Set components for an entity in cache with write-through strategy.
195
+ * Converts BaseComponent instances to ComponentData format for cache compatibility with DataLoader.
196
+ */
197
+ public async setComponentWriteThrough(entityId: string, components: BaseComponent[], componentType?: string, ttl?: number): Promise<void> {
198
+ if (!this.config.enabled || !this.config.component?.enabled) {
199
+ return;
200
+ }
201
+
202
+ try {
203
+ const effectiveTTL = ttl ?? this.config.component.ttl;
204
+
205
+ // Convert BaseComponent to ComponentData format for cache compatibility with DataLoader
206
+ for (const component of components) {
207
+ const typeId = componentType || component.getTypeID();
208
+ const key = `component:${entityId}:${typeId}`;
209
+
210
+ // Create ComponentData structure matching what DataLoader expects
211
+ const componentData: ComponentData = {
212
+ id: component.id,
213
+ entityId: entityId,
214
+ typeId: typeId,
215
+ data: component.data(),
216
+ createdAt: new Date(), // Component doesn't track this, use current time
217
+ updatedAt: new Date(),
218
+ deletedAt: null
219
+ };
220
+
221
+ await this.provider.set(key, componentData, effectiveTTL);
222
+ }
223
+ } catch (error) {
224
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting components in cache', error });
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Invalidate a specific component for an entity from cache
230
+ * More granular than invalidateComponents which can invalidate all components
231
+ */
232
+ public async invalidateComponent(entityId: string, typeId: string): Promise<void> {
233
+ if (!this.config.enabled || !this.config.component?.enabled) {
234
+ return;
235
+ }
236
+
237
+ try {
238
+ logger.trace({
239
+ msg: 'Invalidating component from cache',
240
+ entityId,
241
+ typeId
242
+ })
243
+ const key = `component:${entityId}:${typeId}`;
244
+ await this.provider.delete(key);
245
+ await this.publishInvalidation('key', [key]);
246
+ } catch (error) {
247
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error invalidating component from cache', error });
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Invalidate multiple specific components from cache
253
+ * Useful for bulk invalidation operations
254
+ */
255
+ public async invalidateComponents(components: Array<{ entityId: string; typeId: string }>): Promise<void> {
256
+ if (!this.config.enabled || !this.config.component?.enabled) {
257
+ return;
258
+ }
259
+
260
+ try {
261
+ const keys = components.map(comp => `component:${comp.entityId}:${comp.typeId}`);
262
+ await this.provider.deleteMany(keys);
263
+ await this.publishInvalidation('key', keys);
264
+ } catch (error) {
265
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error invalidating components from cache', error });
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Invalidate all components for a specific entity from cache
271
+ * Uses pattern matching to efficiently clear all component caches for an entity
272
+ */
273
+ public async invalidateAllEntityComponents(entityId: string): Promise<void> {
274
+ if (!this.config.enabled || !this.config.component?.enabled) {
275
+ return;
276
+ }
277
+
278
+ try {
279
+ const pattern = `component:${entityId}:*`;
280
+ await this.provider.invalidatePattern(pattern);
281
+ await this.publishInvalidation('pattern', undefined, pattern);
282
+ } catch (error) {
283
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error invalidating all entity components from cache', error });
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Get components by entity and type from cache (for DataLoader integration)
289
+ */
290
+ public async getComponents(keys: Array<{ entityId: string; typeId: string }>): Promise<(ComponentData | null)[]> {
291
+ if (!this.config.enabled || !this.config.component?.enabled) {
292
+ return keys.map(() => null);
293
+ }
294
+
295
+ try {
296
+ const cacheKeys = keys.map(k => `component:${k.entityId}:${k.typeId}`);
297
+ const results = await this.provider.getMany<ComponentData>(cacheKeys);
298
+ return results;
299
+ } catch (error) {
300
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting components from cache', error });
301
+ return keys.map(() => null);
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Set components in cache with write-through strategy (for DataLoader integration)
307
+ */
308
+ public async setComponentsWriteThrough(components: ComponentData[], ttl?: number): Promise<void> {
309
+ if (!this.config.enabled || !this.config.component?.enabled) {
310
+ return;
311
+ }
312
+
313
+ try {
314
+ const effectiveTTL = ttl ?? this.config.component?.ttl;
315
+ const entries = components.map(comp => ({
316
+ key: `component:${comp.entityId}:${comp.typeId}`,
317
+ value: comp,
318
+ ttl: effectiveTTL
319
+ }));
320
+ await this.provider.setMany(entries);
321
+ } catch (error) {
322
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting components in cache', error });
323
+ }
324
+ }
325
+
326
+ // Generic cache methods
327
+
328
+ /**
329
+ * Generic get method
330
+ */
331
+ public async get<T>(key: string): Promise<T | null> {
332
+ if (!this.config.enabled) {
333
+ return null;
334
+ }
335
+
336
+ try {
337
+ return await this.provider.get<T>(key);
338
+ } catch (error) {
339
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting from cache', error });
340
+ return null;
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Generic set method
346
+ */
347
+ public async set<T>(key: string, value: T, ttl?: number): Promise<void> {
348
+ if (!this.config.enabled) {
349
+ return;
350
+ }
351
+
352
+ try {
353
+ const effectiveTTL = ttl ?? this.config.defaultTTL;
354
+ await this.provider.set(key, value, effectiveTTL);
355
+ } catch (error) {
356
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error setting in cache', error });
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Generic delete method
362
+ */
363
+ public async delete(key: string | string[]): Promise<void> {
364
+ if (!this.config.enabled) {
365
+ return;
366
+ }
367
+
368
+ try {
369
+ await this.provider.delete(key);
370
+ const keys = Array.isArray(key) ? key : [key];
371
+ await this.publishInvalidation('key', keys);
372
+ } catch (error) {
373
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error deleting from cache', error });
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Clear all cache
379
+ */
380
+ public async clear(): Promise<void> {
381
+ if (!this.config.enabled) {
382
+ return;
383
+ }
384
+
385
+ try {
386
+ await this.provider.clear();
387
+ await this.publishInvalidation('pattern', undefined, '*');
388
+ } catch (error) {
389
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error clearing cache', error });
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Get cache statistics
395
+ */
396
+ public async getStats() {
397
+ try {
398
+ return await this.provider.getStats();
399
+ } catch (error) {
400
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error getting cache stats', error });
401
+ return {
402
+ hits: 0,
403
+ misses: 0,
404
+ hitRate: 0,
405
+ size: 0,
406
+ memoryUsage: 0
407
+ };
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Health check for cache
413
+ */
414
+ public async ping(): Promise<boolean> {
415
+ try {
416
+ return await this.provider.ping();
417
+ } catch (error) {
418
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Cache ping failed', error });
419
+ return false;
420
+ }
421
+ }
422
+
423
+ // --- Cross-instance pub/sub ---
424
+
425
+ /**
426
+ * Setup pub/sub for cross-instance cache invalidation.
427
+ * Only activates when using MultiLevel provider with a Redis L2.
428
+ */
429
+ private async setupPubSub(): Promise<void> {
430
+ if (!(this.provider instanceof MultiLevelCache)) return;
431
+
432
+ const l2 = this.provider.getL2Cache();
433
+ if (!(l2 instanceof RedisCache)) return;
434
+
435
+ try {
436
+ await l2.subscribeInvalidation(
437
+ CacheManager.INVALIDATION_CHANNEL,
438
+ (_channel, message) => this.handleRemoteInvalidation(message)
439
+ );
440
+ this.pubSubEnabled = true;
441
+ logger.info({ scope: 'cache', component: 'CacheManager', msg: 'Cross-instance cache invalidation enabled', instanceId: this.instanceId });
442
+ } catch (error) {
443
+ logger.warn({ scope: 'cache', component: 'CacheManager', msg: 'Failed to setup pub/sub', error });
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Handle an invalidation message from another instance.
449
+ * Ignores messages from self. Invalidates L1 only (L2 is shared Redis).
450
+ */
451
+ private async handleRemoteInvalidation(raw: string): Promise<void> {
452
+ try {
453
+ const msg: InvalidationMessage = JSON.parse(raw);
454
+
455
+ // Ignore our own messages
456
+ if (msg.instanceId === this.instanceId) return;
457
+
458
+ if (!(this.provider instanceof MultiLevelCache)) return;
459
+ const l1 = this.provider.getL1Cache();
460
+
461
+ if (msg.type === 'key' && msg.keys) {
462
+ await l1.deleteMany(msg.keys);
463
+ } else if (msg.type === 'pattern' && msg.pattern) {
464
+ await l1.invalidatePattern(msg.pattern);
465
+ }
466
+
467
+ logger.debug({ scope: 'cache', component: 'CacheManager', msg: 'Applied remote invalidation', from: msg.instanceId, type: msg.type });
468
+ } catch (error) {
469
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error handling remote invalidation', error });
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Publish an invalidation event to other instances via Redis pub/sub.
475
+ */
476
+ private async publishInvalidation(type: 'key' | 'pattern', keys?: string[], pattern?: string): Promise<void> {
477
+ if (!this.pubSubEnabled) return;
478
+ if (!(this.provider instanceof MultiLevelCache)) return;
479
+
480
+ const l2 = this.provider.getL2Cache();
481
+ if (!(l2 instanceof RedisCache)) return;
482
+
483
+ try {
484
+ const msg: InvalidationMessage = { instanceId: this.instanceId, type, keys, pattern };
485
+ await l2.publishInvalidation(CacheManager.INVALIDATION_CHANNEL, JSON.stringify(msg));
486
+ } catch (error) {
487
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error publishing invalidation', error });
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Shutdown the current provider (disconnect Redis, stop Memory cleanup timer)
493
+ */
494
+ private async shutdownProvider(): Promise<void> {
495
+ try {
496
+ const provider = this.provider as any;
497
+ // RedisCache has disconnect()
498
+ if (typeof provider.disconnect === 'function') {
499
+ await provider.disconnect();
500
+ }
501
+ // MemoryCache has stopCleanup()
502
+ if (typeof provider.stopCleanup === 'function') {
503
+ provider.stopCleanup();
504
+ }
505
+ } catch (error) {
506
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error shutting down provider', error });
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Shutdown the cache manager
512
+ */
513
+ public async shutdown(): Promise<void> {
514
+ try {
515
+ await this.shutdownProvider();
516
+ } catch (error) {
517
+ logger.error({ scope: 'cache', component: 'CacheManager', msg: 'Error shutting down cache', error });
518
+ }
519
+ }
520
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Cache Provider Interface for BunSane Framework
3
+ * Defines the contract for all cache implementations
4
+ */
5
+
6
+ export interface CacheStats {
7
+ hits: number;
8
+ misses: number;
9
+ hitRate: number;
10
+ size: number;
11
+ memoryUsage?: number;
12
+ }
13
+
14
+ export interface CacheProvider {
15
+ // Basic operations
16
+ get<T>(key: string): Promise<T | null>;
17
+ set<T>(key: string, value: T, ttl?: number): Promise<void>;
18
+ delete(key: string | string[]): Promise<void>;
19
+ clear(): Promise<void>;
20
+
21
+ // Batch operations
22
+ getMany<T>(keys: string[]): Promise<(T | null)[]>;
23
+ setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void>;
24
+ deleteMany(keys: string[]): Promise<void>;
25
+
26
+ // Pattern-based operations
27
+ invalidatePattern(pattern: string): Promise<void>;
28
+
29
+ // Health check
30
+ ping(): Promise<boolean>;
31
+
32
+ // Statistics
33
+ getStats(): Promise<CacheStats>;
34
+ }