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,157 @@
1
+ import { CacheManager } from './CacheManager.js';
2
+ import { SchedulerManager } from '../SchedulerManager.js';
3
+
4
+ /**
5
+ * CacheWarmer preloads frequently accessed data into the cache to improve
6
+ * application startup performance and reduce initial request latency.
7
+ *
8
+ * Features:
9
+ * - Preloading of frequently accessed entities
10
+ * - Scheduled cache warming with cron support
11
+ * - Configurable warming strategies
12
+ * - Performance monitoring during warming
13
+ */
14
+ export class CacheWarmer {
15
+ private cacheManager: CacheManager;
16
+ private scheduler: SchedulerManager;
17
+ private warmingJobs: Map<string, { cancel: () => void }> = new Map();
18
+
19
+ constructor(cacheManager: CacheManager, scheduler: SchedulerManager) {
20
+ this.cacheManager = cacheManager;
21
+ this.scheduler = scheduler;
22
+ }
23
+
24
+ /**
25
+ * Warms the cache by preloading frequently accessed entities
26
+ */
27
+ async warmEntityCache(entityIds: string[], entityType: string): Promise<{
28
+ success: boolean;
29
+ warmed: number;
30
+ failed: number;
31
+ duration: number;
32
+ }> {
33
+ const startTime = Date.now();
34
+ let warmed = 0;
35
+ let failed = 0;
36
+
37
+ console.log(`Starting entity cache warming for ${entityIds.length} ${entityType} entities`);
38
+
39
+ // Process entities in batches to avoid overwhelming the database
40
+ const batchSize = 10;
41
+ for (let i = 0; i < entityIds.length; i += batchSize) {
42
+ const batch = entityIds.slice(i, i + batchSize);
43
+
44
+ try {
45
+ // Load entities (this will populate the cache via write-through strategy)
46
+ const entities = await this.loadEntitiesBatch(batch, entityType);
47
+ warmed += entities.length;
48
+ } catch (error) {
49
+ console.warn(`Failed to warm batch of entities:`, error);
50
+ failed += batch.length;
51
+ }
52
+
53
+ // Small delay between batches to prevent database overload
54
+ await new Promise(resolve => setTimeout(resolve, 50));
55
+ }
56
+
57
+ const duration = Date.now() - startTime;
58
+ console.log(`Entity cache warming completed: ${warmed} warmed, ${failed} failed in ${duration}ms`);
59
+
60
+ return { success: failed === 0, warmed, failed, duration };
61
+ }
62
+
63
+ /**
64
+ * Schedules periodic cache warming
65
+ */
66
+ scheduleWarming(config: {
67
+ name: string;
68
+ cronExpression: string;
69
+ type: 'entity';
70
+ config: { entityIds: string[]; entityType: string };
71
+ enabled?: boolean;
72
+ }): void {
73
+ if (!config.enabled) {
74
+ console.log(`Cache warming job "${config.name}" is disabled`);
75
+ return;
76
+ }
77
+
78
+ // Cancel existing job if it exists
79
+ this.cancelWarming(config.name);
80
+
81
+ const job = this.scheduler.scheduleJob(config.name, config.cronExpression, async () => {
82
+ try {
83
+ console.log(`Running scheduled cache warming: ${config.name}`);
84
+
85
+ if (config.type === 'entity') {
86
+ await this.warmEntityCache(config.config.entityIds, config.config.entityType);
87
+ }
88
+ } catch (error) {
89
+ console.error(`Scheduled cache warming failed for "${config.name}":`, error);
90
+ }
91
+ });
92
+
93
+ this.warmingJobs.set(config.name, job);
94
+ console.log(`Scheduled cache warming job "${config.name}" with cron: ${config.cronExpression}`);
95
+ }
96
+
97
+ /**
98
+ * Cancels a scheduled warming job
99
+ */
100
+ cancelWarming(name: string): boolean {
101
+ const job = this.warmingJobs.get(name);
102
+ if (job) {
103
+ job.cancel();
104
+ this.warmingJobs.delete(name);
105
+ console.log(`Cancelled cache warming job: ${name}`);
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Gets list of active warming jobs
113
+ */
114
+ getActiveJobs(): string[] {
115
+ return Array.from(this.warmingJobs.keys());
116
+ }
117
+
118
+ /**
119
+ * Performs a comprehensive cache warming operation
120
+ */
121
+ async warmAll(config: {
122
+ entities?: Array<{ entityIds: string[]; entityType: string }>;
123
+ }): Promise<{
124
+ entities: { success: boolean; warmed: number; failed: number; duration: number };
125
+ totalDuration: number;
126
+ }> {
127
+ const startTime = Date.now();
128
+
129
+ // Warm entities
130
+ const firstEntity = config.entities?.[0];
131
+ const entityResults = firstEntity
132
+ ? await this.warmEntityCache(firstEntity.entityIds, firstEntity.entityType)
133
+ : { success: true, warmed: 0, failed: 0, duration: 0 };
134
+
135
+ const totalDuration = Date.now() - startTime;
136
+
137
+ return {
138
+ entities: entityResults,
139
+ totalDuration
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Loads a batch of entities (placeholder - would need actual entity loading logic)
145
+ */
146
+ private async loadEntitiesBatch(entityIds: string[], entityType: string): Promise<any[]> {
147
+ // This is a placeholder - in a real implementation, this would load entities
148
+ // from the database using the appropriate entity manager or query system
149
+ console.log(`Loading batch of ${entityIds.length} ${entityType} entities: ${entityIds.slice(0, 3).join(', ')}...`);
150
+
151
+ // Simulate loading delay
152
+ await new Promise(resolve => setTimeout(resolve, 10));
153
+
154
+ // Return mock entities - in real implementation, this would be actual entity data
155
+ return entityIds.map(id => ({ id, type: entityType, loaded: true }));
156
+ }
157
+ }
@@ -0,0 +1,110 @@
1
+ import { gzip, gunzip } from 'zlib';
2
+ import { promisify } from 'util';
3
+
4
+ const gzipAsync = promisify(gzip);
5
+ const gunzipAsync = promisify(gunzip);
6
+
7
+ /**
8
+ * Compression utilities for cache payloads. Automatically compresses data
9
+ * over 1KB threshold to reduce memory usage and network transfer for Redis.
10
+ *
11
+ * Features:
12
+ * - Gzip compression for payloads > 1KB
13
+ * - Automatic compression/decompression
14
+ * - Metadata tracking for compressed data
15
+ * - Error handling with fallback to uncompressed data
16
+ */
17
+ export class CompressionUtils {
18
+ private static readonly COMPRESSION_THRESHOLD = 1024; // 1KB
19
+ private static readonly COMPRESSION_PREFIX = '__COMPRESSED__';
20
+
21
+ /**
22
+ * Compresses data if it exceeds the threshold size
23
+ */
24
+ static async compress(data: any): Promise<any> {
25
+ try {
26
+ const serialized = JSON.stringify(data);
27
+ const size = Buffer.byteLength(serialized, 'utf8');
28
+
29
+ if (size <= this.COMPRESSION_THRESHOLD) {
30
+ return data; // No compression needed
31
+ }
32
+
33
+ const compressed = await gzipAsync(serialized);
34
+ const compressedData = compressed.toString('base64');
35
+
36
+ // Return compressed data with metadata
37
+ return {
38
+ [this.COMPRESSION_PREFIX]: true,
39
+ data: compressedData,
40
+ originalSize: size,
41
+ compressedSize: compressed.length
42
+ };
43
+ } catch (error) {
44
+ // Fallback to uncompressed data on compression error
45
+ console.warn('Compression failed, using uncompressed data:', error);
46
+ return data;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Decompresses data if it was previously compressed
52
+ */
53
+ static async decompress(data: any): Promise<any> {
54
+ try {
55
+ // Check if data is compressed
56
+ if (typeof data === 'object' && data !== null && data[this.COMPRESSION_PREFIX]) {
57
+ const compressedBuffer = Buffer.from(data.data, 'base64');
58
+ const decompressed = await gunzipAsync(compressedBuffer);
59
+ return JSON.parse(decompressed.toString('utf8'));
60
+ }
61
+
62
+ // Data is not compressed
63
+ return data;
64
+ } catch (error) {
65
+ // Fallback to original data on decompression error
66
+ console.warn('Decompression failed, using original data:', error);
67
+ return data;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Checks if data is compressed
73
+ */
74
+ static isCompressed(data: any): boolean {
75
+ return typeof data === 'object' && data !== null && data[this.COMPRESSION_PREFIX] === true;
76
+ }
77
+
78
+ /**
79
+ * Gets compression statistics for monitoring
80
+ */
81
+ static getCompressionStats(data: any): { compressed: boolean; originalSize?: number; compressedSize?: number; ratio?: number } {
82
+ if (this.isCompressed(data)) {
83
+ const originalSize = data.originalSize;
84
+ const compressedSize = data.compressedSize;
85
+ const ratio = originalSize > 0 ? compressedSize / originalSize : 0;
86
+
87
+ return {
88
+ compressed: true,
89
+ originalSize,
90
+ compressedSize,
91
+ ratio
92
+ };
93
+ }
94
+
95
+ return { compressed: false };
96
+ }
97
+
98
+ /**
99
+ * Estimates if data would benefit from compression
100
+ */
101
+ static shouldCompress(data: any): boolean {
102
+ try {
103
+ const serialized = JSON.stringify(data);
104
+ const size = Buffer.byteLength(serialized, 'utf8');
105
+ return size > this.COMPRESSION_THRESHOLD;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,251 @@
1
+ import { type CacheProvider, type CacheStats } from './CacheProvider';
2
+ import { logger } from '../Logger';
3
+
4
+ interface CacheEntry<T> {
5
+ value: T;
6
+ expiresAt?: number;
7
+ lastAccessed: number;
8
+ accessCount: number;
9
+ }
10
+
11
+ export interface MemoryCacheConfig {
12
+ maxSize?: number; // Maximum number of entries
13
+ maxMemory?: number; // Maximum memory usage in bytes
14
+ defaultTTL?: number; // Default TTL in milliseconds
15
+ cleanupInterval?: number; // Cleanup interval in milliseconds
16
+ }
17
+
18
+ /**
19
+ * In-memory cache implementation with TTL and LRU eviction
20
+ */
21
+ export class MemoryCache implements CacheProvider {
22
+ private cache = new Map<string, CacheEntry<any>>();
23
+ private config: Required<MemoryCacheConfig>;
24
+ private cleanupTimer?: Timer;
25
+ private stats = {
26
+ hits: 0,
27
+ misses: 0,
28
+ size: 0,
29
+ memoryUsage: 0
30
+ };
31
+ private accessCounter = 0; // For LRU ordering
32
+
33
+ constructor(config: MemoryCacheConfig = {}) {
34
+ this.config = {
35
+ maxSize: config.maxSize ?? 10000,
36
+ maxMemory: config.maxMemory ?? 100 * 1024 * 1024, // 100MB default
37
+ defaultTTL: config.defaultTTL ?? 3600000, // 1 hour default
38
+ cleanupInterval: config.cleanupInterval ?? 60000 // 1 minute default
39
+ };
40
+
41
+ this.startCleanupTimer();
42
+ }
43
+
44
+ async get<T>(key: string): Promise<T | null> {
45
+ const entry = this.cache.get(key);
46
+
47
+ if (!entry) {
48
+ this.stats.misses++;
49
+ return null;
50
+ }
51
+
52
+ // Check if expired
53
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
54
+ this.cache.delete(key);
55
+ this.stats.misses++;
56
+ this.updateMemoryUsage();
57
+ return null;
58
+ }
59
+
60
+ // Update access tracking for LRU
61
+ entry.lastAccessed = ++this.accessCounter;
62
+ entry.accessCount++;
63
+
64
+ this.stats.hits++;
65
+ return entry.value;
66
+ }
67
+
68
+ async set<T>(key: string, value: T, ttl?: number): Promise<void> {
69
+ const expiresAt = ttl ? Date.now() + ttl : (this.config.defaultTTL ? Date.now() + this.config.defaultTTL : undefined);
70
+
71
+ const entry: CacheEntry<T> = {
72
+ value,
73
+ expiresAt,
74
+ lastAccessed: ++this.accessCounter,
75
+ accessCount: 1
76
+ };
77
+
78
+ const wasNew = !this.cache.has(key);
79
+ this.cache.set(key, entry);
80
+
81
+ if (wasNew) {
82
+ this.stats.size++;
83
+ }
84
+
85
+ this.updateMemoryUsage();
86
+
87
+ // Evict if necessary
88
+ await this.evictIfNeeded();
89
+ }
90
+
91
+ async delete(key: string | string[]): Promise<void> {
92
+ const keys = Array.isArray(key) ? key : [key];
93
+ let deletedCount = 0;
94
+
95
+ for (const k of keys) {
96
+ if (this.cache.delete(k)) {
97
+ deletedCount++;
98
+ }
99
+ }
100
+
101
+ this.stats.size -= deletedCount;
102
+ this.updateMemoryUsage();
103
+ }
104
+
105
+ async clear(): Promise<void> {
106
+ this.cache.clear();
107
+ this.stats.size = 0;
108
+ this.stats.memoryUsage = 0;
109
+ this.stats.hits = 0;
110
+ this.stats.misses = 0;
111
+ }
112
+
113
+ async getMany<T>(keys: string[]): Promise<(T | null)[]> {
114
+ const results: (T | null)[] = [];
115
+
116
+ for (const key of keys) {
117
+ const value = await this.get<T>(key);
118
+ results.push(value);
119
+ }
120
+
121
+ return results;
122
+ }
123
+
124
+ async setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void> {
125
+ for (const entry of entries) {
126
+ await this.set(entry.key, entry.value, entry.ttl);
127
+ }
128
+ }
129
+
130
+ async deleteMany(keys: string[]): Promise<void> {
131
+ return this.delete(keys);
132
+ }
133
+
134
+ async invalidatePattern(pattern: string): Promise<void> {
135
+ // Simple pattern matching - convert glob to regex
136
+ const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.'));
137
+
138
+ const keysToDelete: string[] = [];
139
+ for (const key of Array.from(this.cache.keys())) {
140
+ if (regex.test(key)) {
141
+ keysToDelete.push(key);
142
+ }
143
+ }
144
+
145
+ await this.delete(keysToDelete);
146
+ }
147
+
148
+ async ping(): Promise<boolean> {
149
+ return true; // Memory cache is always available
150
+ }
151
+
152
+ async getStats(): Promise<CacheStats> {
153
+ const totalRequests = this.stats.hits + this.stats.misses;
154
+ const hitRate = totalRequests > 0 ? this.stats.hits / totalRequests : 0;
155
+
156
+ return {
157
+ hits: this.stats.hits,
158
+ misses: this.stats.misses,
159
+ hitRate,
160
+ size: this.stats.size,
161
+ memoryUsage: this.stats.memoryUsage
162
+ };
163
+ }
164
+
165
+ private updateMemoryUsage(): void {
166
+ // Rough estimation of memory usage
167
+ // Each entry has overhead for the key, value, and metadata
168
+ let memoryUsage = 0;
169
+ for (const [key, entry] of Array.from(this.cache.entries())) {
170
+ memoryUsage += key.length * 2; // Rough string overhead
171
+ memoryUsage += this.estimateValueSize(entry.value);
172
+ memoryUsage += 100; // Overhead for entry metadata
173
+ }
174
+ this.stats.memoryUsage = memoryUsage;
175
+ }
176
+
177
+ private estimateValueSize(value: any): number {
178
+ if (value === null || value === undefined) return 8;
179
+ if (typeof value === 'string') return value.length * 2;
180
+ if (typeof value === 'number') return 8;
181
+ if (typeof value === 'boolean') return 1;
182
+ if (Array.isArray(value)) {
183
+ return value.reduce((size, item) => size + this.estimateValueSize(item), 16); // Array overhead
184
+ }
185
+ if (typeof value === 'object') {
186
+ let size = 16; // Object overhead
187
+ for (const key in value) {
188
+ size += key.length * 2 + this.estimateValueSize(value[key]);
189
+ }
190
+ return size;
191
+ }
192
+ return 16; // Default size for other types
193
+ }
194
+
195
+ private async evictIfNeeded(): Promise<void> {
196
+ // Check size limit
197
+ if (this.stats.size > this.config.maxSize) {
198
+ await this.evictLRU(Math.ceil(this.config.maxSize * 0.1)); // Evict 10% of max size
199
+ }
200
+
201
+ // Check memory limit
202
+ if (this.stats.memoryUsage > this.config.maxMemory) {
203
+ await this.evictLRU(Math.ceil(this.config.maxSize * 0.1)); // Evict 10% of max size
204
+ }
205
+ }
206
+
207
+ private async evictLRU(count: number): Promise<void> {
208
+ // Sort entries by last accessed time (oldest first)
209
+ const entries = Array.from(this.cache.entries())
210
+ .map(([key, entry]) => ({ key, entry }))
211
+ .sort((a, b) => a.entry.lastAccessed - b.entry.lastAccessed);
212
+
213
+ const keysToDelete = entries.slice(0, count).map(item => item.key);
214
+ await this.delete(keysToDelete);
215
+
216
+ logger.debug(`Evicted ${keysToDelete.length} entries from cache due to LRU policy`);
217
+ }
218
+
219
+ private startCleanupTimer(): void {
220
+ this.cleanupTimer = setInterval(() => {
221
+ this.cleanupExpired();
222
+ }, this.config.cleanupInterval);
223
+ }
224
+
225
+ private cleanupExpired(): void {
226
+ const now = Date.now();
227
+ const keysToDelete: string[] = [];
228
+
229
+ for (const [key, entry] of Array.from(this.cache.entries())) {
230
+ if (entry.expiresAt && now > entry.expiresAt) {
231
+ keysToDelete.push(key);
232
+ }
233
+ }
234
+
235
+ if (keysToDelete.length > 0) {
236
+ this.delete(keysToDelete).catch(error => {
237
+ logger.error('Error during cache cleanup:', error);
238
+ });
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Stop the cleanup timer (useful for testing or shutdown)
244
+ */
245
+ stopCleanup(): void {
246
+ if (this.cleanupTimer) {
247
+ clearInterval(this.cleanupTimer);
248
+ this.cleanupTimer = undefined;
249
+ }
250
+ }
251
+ }
@@ -0,0 +1,180 @@
1
+ import type { CacheProvider, CacheStats } from './CacheProvider.js';
2
+ import type { CacheConfig } from '../../config/cache.config.js';
3
+
4
+ /**
5
+ * MultiLevelCache implements a two-tier caching strategy with L1 in-memory cache
6
+ * and L2 persistent cache (Redis). This provides optimal performance by serving
7
+ * frequently accessed data from memory while maintaining persistence across requests.
8
+ *
9
+ * Key Features:
10
+ * - L1 MemoryCache for fast access to hot data
11
+ * - L2 RedisCache for persistence and cross-instance sharing
12
+ * - Automatic L1 promotion on L2 cache hits
13
+ * - Configurable TTL strategies for each level
14
+ * - Write-through strategy for data consistency
15
+ */
16
+ export class MultiLevelCache implements CacheProvider {
17
+ private l1Cache: CacheProvider;
18
+ private l2Cache: CacheProvider | null;
19
+ private config: CacheConfig;
20
+
21
+ constructor(l1Cache: CacheProvider, l2Cache: CacheProvider | null, config: CacheConfig) {
22
+ this.l1Cache = l1Cache;
23
+ this.l2Cache = l2Cache;
24
+ this.config = config;
25
+ }
26
+
27
+ getL1Cache(): CacheProvider {
28
+ return this.l1Cache;
29
+ }
30
+
31
+ getL2Cache(): CacheProvider | null {
32
+ return this.l2Cache;
33
+ }
34
+
35
+ async get(key: string): Promise<any | null> {
36
+ // Try L1 cache first
37
+ const l1Result = await this.l1Cache.get(key);
38
+ if (l1Result !== null) {
39
+ return l1Result;
40
+ }
41
+
42
+ // If L1 miss and L2 exists, try L2
43
+ if (this.l2Cache) {
44
+ const l2Result = await this.l2Cache.get(key);
45
+ if (l2Result !== null) {
46
+ // Promote to L1 cache for faster future access
47
+ await this.l1Cache.set(key, l2Result, this.config.defaultTTL);
48
+ return l2Result;
49
+ }
50
+ }
51
+
52
+ return null;
53
+ }
54
+
55
+ async set(key: string, value: any, ttl?: number): Promise<void> {
56
+ const effectiveTTL = ttl || this.config.defaultTTL;
57
+
58
+ // Set in L1 cache
59
+ await this.l1Cache.set(key, value, effectiveTTL);
60
+
61
+ // Set in L2 cache if available
62
+ if (this.l2Cache) {
63
+ await this.l2Cache.set(key, value, effectiveTTL);
64
+ }
65
+ }
66
+
67
+ async delete(key: string | string[]): Promise<void> {
68
+ // Delete from L1 cache
69
+ await this.l1Cache.delete(key);
70
+
71
+ // Delete from L2 cache if available
72
+ if (this.l2Cache) {
73
+ await this.l2Cache.delete(key);
74
+ }
75
+ }
76
+
77
+ async clear(): Promise<void> {
78
+ await this.l1Cache.clear();
79
+ if (this.l2Cache) {
80
+ await this.l2Cache.clear();
81
+ }
82
+ }
83
+
84
+ async getMany<T>(keys: string[]): Promise<(T | null)[]> {
85
+ const results: (T | null)[] = new Array(keys.length).fill(null);
86
+ const missingIndices: number[] = [];
87
+ const missingKeys: string[] = [];
88
+
89
+ // Try L1 cache first
90
+ const l1Results = await this.l1Cache.getMany<T>(keys);
91
+ for (let i = 0; i < keys.length; i++) {
92
+ const l1Value = l1Results[i];
93
+ const key = keys[i];
94
+ if (l1Value !== null && l1Value !== undefined) {
95
+ results[i] = l1Value;
96
+ } else if (key !== undefined) {
97
+ missingIndices.push(i);
98
+ missingKeys.push(key);
99
+ }
100
+ }
101
+
102
+ // If L2 exists and we have missing keys, try L2
103
+ if (this.l2Cache && missingKeys.length > 0) {
104
+ const l2Results = await this.l2Cache.getMany<T>(missingKeys);
105
+ for (let i = 0; i < missingKeys.length; i++) {
106
+ const value = l2Results[i];
107
+ const originalIndex = missingIndices[i];
108
+ const missingKey = missingKeys[i];
109
+ if (value !== null && value !== undefined && originalIndex !== undefined && missingKey !== undefined) {
110
+ results[originalIndex] = value;
111
+ // Promote to L1 cache
112
+ await this.l1Cache.set(missingKey, value, this.config.defaultTTL);
113
+ }
114
+ }
115
+ }
116
+
117
+ return results;
118
+ }
119
+
120
+ async setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void> {
121
+ // Apply default TTL to entries without one
122
+ const entriesWithTTL = entries.map(e => ({
123
+ ...e,
124
+ ttl: e.ttl || this.config.defaultTTL
125
+ }));
126
+
127
+ // Set in L1 cache
128
+ await this.l1Cache.setMany(entriesWithTTL);
129
+
130
+ // Set in L2 cache if available
131
+ if (this.l2Cache) {
132
+ await this.l2Cache.setMany(entriesWithTTL);
133
+ }
134
+ }
135
+
136
+ async deleteMany(keys: string[]): Promise<void> {
137
+ await this.l1Cache.deleteMany(keys);
138
+ if (this.l2Cache) {
139
+ await this.l2Cache.deleteMany(keys);
140
+ }
141
+ }
142
+
143
+ async invalidatePattern(pattern: string): Promise<void> {
144
+ await this.l1Cache.invalidatePattern(pattern);
145
+ if (this.l2Cache) {
146
+ await this.l2Cache.invalidatePattern(pattern);
147
+ }
148
+ }
149
+
150
+ async ping(): Promise<boolean> {
151
+ const l1Ping = await this.l1Cache.ping();
152
+
153
+ if (!this.l2Cache) {
154
+ return l1Ping;
155
+ }
156
+
157
+ const l2Ping = await this.l2Cache.ping();
158
+
159
+ // Multi-level cache is healthy if both levels are healthy
160
+ return l1Ping && l2Ping;
161
+ }
162
+
163
+ async getStats(): Promise<CacheStats> {
164
+ const l1Stats = await this.l1Cache.getStats();
165
+ const l2Stats = this.l2Cache ? await this.l2Cache.getStats() : null;
166
+
167
+ const totalHits = l1Stats.hits + (l2Stats?.hits || 0);
168
+ const totalMisses = l1Stats.misses + (l2Stats?.misses || 0);
169
+ const totalRequests = totalHits + totalMisses;
170
+ const hitRate = totalRequests > 0 ? totalHits / totalRequests : 0;
171
+
172
+ return {
173
+ hits: totalHits,
174
+ misses: totalMisses,
175
+ hitRate,
176
+ size: l1Stats.size + (l2Stats?.size || 0),
177
+ memoryUsage: (l1Stats.memoryUsage || 0) + (l2Stats?.memoryUsage || 0)
178
+ };
179
+ }
180
+ }