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,53 @@
1
+ import { type CacheProvider, type CacheStats } from './CacheProvider';
2
+
3
+ /**
4
+ * No-op cache implementation for testing and cache-disabled scenarios
5
+ * All operations return null/void and stats show zero activity
6
+ */
7
+ export class NoOpCache implements CacheProvider {
8
+ async get<T>(key: string): Promise<T | null> {
9
+ return null;
10
+ }
11
+
12
+ async set<T>(key: string, value: T, ttl?: number): Promise<void> {
13
+ // No-op
14
+ }
15
+
16
+ async delete(key: string | string[]): Promise<void> {
17
+ // No-op
18
+ }
19
+
20
+ async clear(): Promise<void> {
21
+ // No-op
22
+ }
23
+
24
+ async getMany<T>(keys: string[]): Promise<(T | null)[]> {
25
+ return new Array(keys.length).fill(null);
26
+ }
27
+
28
+ async setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void> {
29
+ // No-op
30
+ }
31
+
32
+ async deleteMany(keys: string[]): Promise<void> {
33
+ // No-op
34
+ }
35
+
36
+ async invalidatePattern(pattern: string): Promise<void> {
37
+ // No-op
38
+ }
39
+
40
+ async ping(): Promise<boolean> {
41
+ return true; // No-op cache is always "available"
42
+ }
43
+
44
+ async getStats(): Promise<CacheStats> {
45
+ return {
46
+ hits: 0,
47
+ misses: 0,
48
+ hitRate: 0,
49
+ size: 0,
50
+ memoryUsage: 0
51
+ };
52
+ }
53
+ }
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Redis Cache Implementation for BunSane Framework
3
+ * Provides distributed caching with Redis backend
4
+ */
5
+
6
+ import Redis, { type RedisOptions } from 'ioredis';
7
+ import { type CacheProvider, type CacheStats } from './CacheProvider';
8
+ import { type CacheConfig } from '../../config/cache.config';
9
+ import { logger } from '../Logger';
10
+ import { CompressionUtils } from './CompressionUtils';
11
+
12
+ export interface HealthStatus {
13
+ connected: boolean;
14
+ latency: number;
15
+ memoryUsage?: number;
16
+ connections?: number;
17
+ version?: string;
18
+ }
19
+
20
+ export interface RedisCacheConfig {
21
+ host: string;
22
+ port: number;
23
+ password?: string;
24
+ db?: number;
25
+ keyPrefix?: string;
26
+ retryStrategy?: (times: number) => number | void;
27
+ maxRetriesPerRequest?: number;
28
+ lazyConnect?: boolean;
29
+ enableReadyCheck?: boolean;
30
+ }
31
+
32
+ /**
33
+ * Redis-based cache implementation with connection pooling and Pub/Sub support
34
+ */
35
+ export class RedisCache implements CacheProvider {
36
+ private client: Redis;
37
+ private subscriber?: Redis;
38
+ private publisher?: Redis;
39
+ private config: RedisCacheConfig;
40
+ private keyPrefix: string;
41
+ private stats = {
42
+ hits: 0,
43
+ misses: 0,
44
+ size: 0
45
+ };
46
+ private invalidationHandlers: Map<string, (channel: string, message: string) => void> = new Map();
47
+ private monitoringInterval: Timer | null = null;
48
+ private subscriberListenerAttached = false;
49
+
50
+ constructor(config: RedisCacheConfig) {
51
+ this.config = config;
52
+ this.keyPrefix = config.keyPrefix || 'bunsane:';
53
+
54
+ const redisOptions: RedisOptions = {
55
+ host: config.host,
56
+ port: config.port,
57
+ password: config.password,
58
+ db: config.db || 0,
59
+ retryStrategy: config.retryStrategy,
60
+ maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
61
+ lazyConnect: config.lazyConnect || false,
62
+ enableReadyCheck: config.enableReadyCheck || false,
63
+ // Connection pooling settings
64
+ enableOfflineQueue: true,
65
+ };
66
+
67
+ this.client = new Redis(redisOptions);
68
+ this.setupEventHandlers();
69
+ this.setupMonitoring();
70
+ }
71
+
72
+ /**
73
+ * Setup Redis event handlers for connection monitoring
74
+ */
75
+ private setupEventHandlers(): void {
76
+ this.client.on('connect', () => {
77
+ logger.info('Redis cache connected');
78
+ });
79
+
80
+ this.client.on('ready', () => {
81
+ logger.info('Redis cache ready');
82
+ });
83
+
84
+ this.client.on('error', (error: Error) => {
85
+ logger.error({ error, msg: 'Redis cache error' });
86
+ });
87
+
88
+ this.client.on('close', () => {
89
+ logger.warn('Redis cache connection closed');
90
+ });
91
+
92
+ this.client.on('reconnecting', (delay: number) => {
93
+ logger.info(`Redis cache reconnecting in ${delay}ms`);
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Setup monitoring for memory usage and connection stats
99
+ */
100
+ private setupMonitoring(): void {
101
+ // Log memory usage every 5 minutes
102
+ this.monitoringInterval = setInterval(async () => {
103
+ try {
104
+ const info = await this.client.info('memory');
105
+ const memoryMatch = info.match(/used_memory:(\d+)/);
106
+ if (memoryMatch && memoryMatch[1]) {
107
+ const memoryUsage = parseInt(memoryMatch[1], 10);
108
+ logger.debug({ msg: 'Redis memory usage', memoryUsage });
109
+ }
110
+ } catch (error) {
111
+ logger.error({ error, msg: 'Failed to get Redis memory info' });
112
+ }
113
+ }, 300000); // 5 minutes
114
+ }
115
+
116
+ /**
117
+ * Get a value from cache
118
+ */
119
+ async get<T>(key: string): Promise<T | null> {
120
+ try {
121
+ const prefixedKey = this.prefixKey(key);
122
+ const value = await this.client.get(prefixedKey);
123
+
124
+ if (value === null) {
125
+ this.stats.misses++;
126
+ return null;
127
+ }
128
+
129
+ this.stats.hits++;
130
+ const parsed = JSON.parse(value);
131
+ return await CompressionUtils.decompress(parsed) as T;
132
+ } catch (error) {
133
+ logger.error({ error, msg: 'Redis get error' });
134
+ this.stats.misses++;
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Set a value in cache with optional TTL
141
+ */
142
+ async set<T>(key: string, value: T, ttl?: number): Promise<void> {
143
+ try {
144
+ const prefixedKey = this.prefixKey(key);
145
+ const compressedValue = await CompressionUtils.compress(value);
146
+ const serializedValue = JSON.stringify(compressedValue);
147
+
148
+ if (ttl) {
149
+ await this.client.setex(prefixedKey, Math.floor(ttl / 1000), serializedValue);
150
+ } else {
151
+ await this.client.set(prefixedKey, serializedValue);
152
+ }
153
+ } catch (error) {
154
+ logger.error({ error, msg: 'Redis set error' });
155
+ // Don't throw - cache failures shouldn't break the app
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Delete a key or array of keys from cache
161
+ */
162
+ async delete(key: string | string[]): Promise<void> {
163
+ try {
164
+ const keys = Array.isArray(key) ? key : [key];
165
+ const prefixedKeys = keys.map(k => this.prefixKey(k));
166
+
167
+ if (prefixedKeys.length > 0) {
168
+ await this.client.del(...prefixedKeys);
169
+ }
170
+ } catch (error) {
171
+ logger.error({ error, msg: 'Redis delete error' });
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Clear all cache entries
177
+ */
178
+ async clear(): Promise<void> {
179
+ try {
180
+ const keys = await this.client.keys(`${this.keyPrefix}*`);
181
+ if (keys.length > 0) {
182
+ await this.client.del(...keys);
183
+ }
184
+ } catch (error) {
185
+ logger.error({ error, msg: 'Redis clear error' });
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Get multiple values from cache
191
+ */
192
+ async getMany<T>(keys: string[]): Promise<(T | null)[]> {
193
+ try {
194
+ const prefixedKeys = keys.map(k => this.prefixKey(k));
195
+ const values = await this.client.mget(...prefixedKeys);
196
+
197
+ return values.map((value, index) => {
198
+ if (value === null) {
199
+ this.stats.misses++;
200
+ return null;
201
+ }
202
+ this.stats.hits++;
203
+ try {
204
+ return JSON.parse(value) as T;
205
+ } catch (parseError) {
206
+ logger.error({ error: parseError, key: keys[index], msg: 'Failed to parse cached value' });
207
+ return null;
208
+ }
209
+ });
210
+ } catch (error) {
211
+ logger.error({ error, msg: 'Redis getMany error' });
212
+ return new Array(keys.length).fill(null);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Set multiple values in cache
218
+ */
219
+ async setMany<T>(entries: Array<{key: string, value: T, ttl?: number}>): Promise<void> {
220
+ try {
221
+ const pipeline = this.client.pipeline();
222
+
223
+ for (const entry of entries) {
224
+ const prefixedKey = this.prefixKey(entry.key);
225
+ const serializedValue = JSON.stringify(entry.value);
226
+
227
+ if (entry.ttl) {
228
+ pipeline.setex(prefixedKey, Math.floor(entry.ttl / 1000), serializedValue);
229
+ } else {
230
+ pipeline.set(prefixedKey, serializedValue);
231
+ }
232
+ }
233
+
234
+ await pipeline.exec();
235
+ } catch (error) {
236
+ logger.error({ error, msg: 'Redis setMany error' });
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Delete multiple keys from cache
242
+ */
243
+ async deleteMany(keys: string[]): Promise<void> {
244
+ try {
245
+ const prefixedKeys = keys.map(k => this.prefixKey(k));
246
+ if (prefixedKeys.length > 0) {
247
+ await this.client.del(...prefixedKeys);
248
+ }
249
+ } catch (error) {
250
+ logger.error({ error, msg: 'Redis deleteMany error' });
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Invalidate keys matching a pattern using SCAN to avoid blocking
256
+ */
257
+ async invalidatePattern(pattern: string): Promise<void> {
258
+ try {
259
+ const prefixedPattern = this.prefixKey(pattern);
260
+ let cursor = '0';
261
+ const keysToDelete: string[] = [];
262
+
263
+ do {
264
+ const [newCursor, keys] = await this.client.scan(cursor, 'MATCH', prefixedPattern, 'COUNT', 100);
265
+ cursor = newCursor;
266
+ keysToDelete.push(...keys);
267
+ } while (cursor !== '0');
268
+
269
+ if (keysToDelete.length > 0) {
270
+ await this.client.del(...keysToDelete);
271
+ logger.debug({ pattern, count: keysToDelete.length, msg: `Invalidated ${keysToDelete.length} keys matching pattern` });
272
+ }
273
+ } catch (error) {
274
+ logger.error({ error, msg: 'Redis invalidatePattern error' });
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Check if Redis is reachable
280
+ */
281
+ async ping(): Promise<boolean> {
282
+ try {
283
+ const result = await this.client.ping();
284
+ return result === 'PONG';
285
+ } catch (error) {
286
+ logger.error({ error, msg: 'Redis ping error' });
287
+ return false;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Get cache statistics
293
+ */
294
+ async getStats(): Promise<CacheStats> {
295
+ try {
296
+ // Get approximate key count using DBSIZE
297
+ const size = await this.client.dbsize();
298
+
299
+ // Get memory usage
300
+ const info = await this.client.info('memory');
301
+ const memoryMatch = info.match(/used_memory:(\d+)/);
302
+ const memoryUsage = memoryMatch && memoryMatch[1] ? parseInt(memoryMatch[1], 10) : undefined;
303
+
304
+ return {
305
+ hits: this.stats.hits,
306
+ misses: this.stats.misses,
307
+ hitRate: this.stats.hits / (this.stats.hits + this.stats.misses) || 0,
308
+ size,
309
+ memoryUsage
310
+ };
311
+ } catch (error) {
312
+ logger.error({ error, msg: 'Redis getStats error' });
313
+ return {
314
+ hits: this.stats.hits,
315
+ misses: this.stats.misses,
316
+ hitRate: 0,
317
+ size: 0
318
+ };
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Perform health check with detailed status
324
+ */
325
+ async healthCheck(): Promise<HealthStatus> {
326
+ const startTime = Date.now();
327
+
328
+ try {
329
+ const pingResult = await this.ping();
330
+ const latency = Date.now() - startTime;
331
+
332
+ if (!pingResult) {
333
+ return {
334
+ connected: false,
335
+ latency
336
+ };
337
+ }
338
+
339
+ // Get additional health info
340
+ const info = await this.client.info();
341
+ const memoryMatch = info.match(/used_memory:(\d+)/);
342
+ const memoryUsage = memoryMatch && memoryMatch[1] ? parseInt(memoryMatch[1], 10) : undefined;
343
+
344
+ const connectionsMatch = info.match(/connected_clients:(\d+)/);
345
+ const connections = connectionsMatch && connectionsMatch[1] ? parseInt(connectionsMatch[1], 10) : undefined;
346
+
347
+ const versionMatch = info.match(/redis_version:([^\r\n]+)/);
348
+ const version = versionMatch ? versionMatch[1] : undefined;
349
+
350
+ return {
351
+ connected: true,
352
+ latency,
353
+ memoryUsage,
354
+ connections,
355
+ version
356
+ };
357
+ } catch (error) {
358
+ logger.error({ error, msg: 'Redis health check error' });
359
+ return {
360
+ connected: false,
361
+ latency: Date.now() - startTime
362
+ };
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Publish cache invalidation event
368
+ */
369
+ async publishInvalidation(channel: string, message: string): Promise<void> {
370
+ try {
371
+ if (!this.publisher) {
372
+ this.publisher = this.client.duplicate();
373
+ }
374
+ await this.publisher.publish(channel, message);
375
+ } catch (error) {
376
+ logger.error({ error, msg: 'Redis publish invalidation error' });
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Subscribe to cache invalidation events
382
+ */
383
+ async subscribeInvalidation(channel: string, handler: (channel: string, message: string) => void): Promise<void> {
384
+ try {
385
+ if (!this.subscriber) {
386
+ this.subscriber = this.client.duplicate();
387
+ }
388
+
389
+ this.invalidationHandlers.set(channel, handler);
390
+ await this.subscriber.subscribe(channel);
391
+
392
+ // Only attach the message listener once to avoid stacking
393
+ if (!this.subscriberListenerAttached) {
394
+ this.subscriberListenerAttached = true;
395
+ this.subscriber.on('message', (receivedChannel, message) => {
396
+ this.handleInvalidationEvent(receivedChannel, message);
397
+ });
398
+ }
399
+ } catch (error) {
400
+ logger.error({ error, msg: 'Redis subscribe invalidation error' });
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Unsubscribe from cache invalidation events
406
+ */
407
+ async unsubscribeInvalidation(channel: string): Promise<void> {
408
+ try {
409
+ if (this.subscriber) {
410
+ await this.subscriber.unsubscribe(channel);
411
+ this.invalidationHandlers.delete(channel);
412
+ }
413
+ } catch (error) {
414
+ logger.error({ error, msg: 'Redis unsubscribe invalidation error' });
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Handle incoming invalidation events
420
+ */
421
+ private handleInvalidationEvent(channel: string, message: string): void {
422
+ try {
423
+ const handler = this.invalidationHandlers.get(channel);
424
+ if (handler) {
425
+ handler(channel, message);
426
+ }
427
+ } catch (error) {
428
+ logger.error({ error, msg: 'Error handling invalidation event' });
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Prefix a key with the configured prefix
434
+ */
435
+ private prefixKey(key: string): string {
436
+ return `${this.keyPrefix}${key}`;
437
+ }
438
+
439
+ /**
440
+ * Close all Redis connections
441
+ */
442
+ async disconnect(): Promise<void> {
443
+ try {
444
+ if (this.monitoringInterval) {
445
+ clearInterval(this.monitoringInterval);
446
+ this.monitoringInterval = null;
447
+ }
448
+
449
+ await this.client.disconnect();
450
+
451
+ if (this.subscriber) {
452
+ await this.subscriber.disconnect();
453
+ }
454
+
455
+ if (this.publisher) {
456
+ await this.publisher.disconnect();
457
+ }
458
+
459
+ logger.info('Redis cache disconnected');
460
+ } catch (error) {
461
+ logger.error({ error, msg: 'Redis disconnect error' });
462
+ }
463
+ }
464
+ }