@buenojs/bueno 0.8.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 (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,795 @@
1
+ /**
2
+ * Caching Layer
3
+ *
4
+ * Unified interface over Bun.redis with in-memory fallback.
5
+ * Uses Bun 1.3+ native Redis client for production.
6
+ */
7
+
8
+ // ============= Types =============
9
+
10
+ export interface CacheConfig {
11
+ driver?: "redis" | "memory";
12
+ url?: string;
13
+ ttl?: number; // Default TTL in seconds
14
+ keyPrefix?: string;
15
+ enableMetrics?: boolean; // Enable metrics collection (default: true)
16
+ }
17
+
18
+ /**
19
+ * Cache metrics for observability
20
+ */
21
+ export interface CacheMetrics {
22
+ hits: number;
23
+ misses: number;
24
+ sets: number;
25
+ deletes: number;
26
+ errors: number;
27
+ avgLatency: number; // in milliseconds
28
+ totalOperations: number;
29
+ }
30
+
31
+ export interface SessionData {
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ export interface SessionStoreOptions {
36
+ ttl?: number; // Session TTL in seconds
37
+ prefix?: string;
38
+ driver?: "redis" | "memory";
39
+ url?: string;
40
+ }
41
+
42
+ export interface PubSubMessage {
43
+ channel: string;
44
+ message: string;
45
+ }
46
+
47
+ // ============= In-Memory Cache (Fallback) =============
48
+
49
+ class MemoryCache {
50
+ private store = new Map<string, { value: unknown; expiresAt: number }>();
51
+ private cleanupInterval: ReturnType<typeof setInterval>;
52
+ private pubsubListeners: Map<string, Set<(message: string) => void>> =
53
+ new Map();
54
+
55
+ constructor() {
56
+ this.cleanupInterval = setInterval(() => this.cleanup(), 30000);
57
+ }
58
+
59
+ async get(key: string): Promise<string | null> {
60
+ const entry = this.store.get(key);
61
+ if (!entry) return null;
62
+
63
+ if (Date.now() > entry.expiresAt) {
64
+ this.store.delete(key);
65
+ return null;
66
+ }
67
+
68
+ return entry.value as string;
69
+ }
70
+
71
+ async set(key: string, value: string, ttl?: number): Promise<void> {
72
+ const expiresAt = Date.now() + (ttl ?? 3600) * 1000;
73
+ this.store.set(key, { value, expiresAt });
74
+ }
75
+
76
+ async delete(key: string): Promise<boolean> {
77
+ return this.store.delete(key);
78
+ }
79
+
80
+ async has(key: string): Promise<boolean> {
81
+ const entry = this.store.get(key);
82
+ if (!entry) return false;
83
+
84
+ if (Date.now() > entry.expiresAt) {
85
+ this.store.delete(key);
86
+ return false;
87
+ }
88
+
89
+ return true;
90
+ }
91
+
92
+ async clear(): Promise<void> {
93
+ this.store.clear();
94
+ }
95
+
96
+ async incr(key: string): Promise<number> {
97
+ const entry = this.store.get(key);
98
+ const value = entry ? Number.parseInt(entry.value as string) || 0 : 0;
99
+ const newValue = value + 1;
100
+ await this.set(
101
+ key,
102
+ String(newValue),
103
+ entry ? Math.floor((entry.expiresAt - Date.now()) / 1000) : undefined,
104
+ );
105
+ return newValue;
106
+ }
107
+
108
+ async expire(key: string, ttl: number): Promise<boolean> {
109
+ const entry = this.store.get(key);
110
+ if (!entry) return false;
111
+ entry.expiresAt = Date.now() + ttl * 1000;
112
+ return true;
113
+ }
114
+
115
+ async ttl(key: string): Promise<number> {
116
+ const entry = this.store.get(key);
117
+ if (!entry) return -2;
118
+ const remaining = Math.floor((entry.expiresAt - Date.now()) / 1000);
119
+ return remaining > 0 ? remaining : -1;
120
+ }
121
+
122
+ // Pub/Sub simulation
123
+ async publish(channel: string, message: string): Promise<number> {
124
+ const listeners = this.pubsubListeners.get(channel);
125
+ if (listeners) {
126
+ for (const listener of listeners) {
127
+ listener(message);
128
+ }
129
+ return listeners.size;
130
+ }
131
+ return 0;
132
+ }
133
+
134
+ async subscribe(
135
+ channel: string,
136
+ callback: (message: string) => void,
137
+ ): Promise<void> {
138
+ if (!this.pubsubListeners.has(channel)) {
139
+ this.pubsubListeners.set(channel, new Set());
140
+ }
141
+ this.pubsubListeners.get(channel)?.add(callback);
142
+ }
143
+
144
+ async unsubscribe(
145
+ channel: string,
146
+ callback?: (message: string) => void,
147
+ ): Promise<void> {
148
+ if (callback) {
149
+ this.pubsubListeners.get(channel)?.delete(callback);
150
+ } else {
151
+ this.pubsubListeners.delete(channel);
152
+ }
153
+ }
154
+
155
+ private cleanup(): void {
156
+ const now = Date.now();
157
+ for (const [key, entry] of this.store.entries()) {
158
+ if (now > entry.expiresAt) {
159
+ this.store.delete(key);
160
+ }
161
+ }
162
+ }
163
+
164
+ destroy(): void {
165
+ clearInterval(this.cleanupInterval);
166
+ this.store.clear();
167
+ this.pubsubListeners.clear();
168
+ }
169
+ }
170
+
171
+ // ============= Redis Cache (Bun.redis Native) =============
172
+
173
+ class RedisCache {
174
+ private client: unknown = null;
175
+ private url: string;
176
+ private _isConnected = false;
177
+
178
+ constructor(url: string) {
179
+ this.url = url;
180
+ }
181
+
182
+ async connect(): Promise<void> {
183
+ try {
184
+ // Use Bun's native Redis client
185
+ const { RedisClient } = await import("bun");
186
+ this.client = new RedisClient(this.url);
187
+ this._isConnected = true;
188
+ } catch (error) {
189
+ throw new Error(
190
+ `Failed to connect to Redis: ${error instanceof Error ? error.message : String(error)}`,
191
+ );
192
+ }
193
+ }
194
+
195
+ async disconnect(): Promise<void> {
196
+ // Bun.redis handles connection management automatically
197
+ this._isConnected = false;
198
+ this.client = null;
199
+ }
200
+
201
+ get isConnected(): boolean {
202
+ return this._isConnected;
203
+ }
204
+
205
+ async get(key: string): Promise<string | null> {
206
+ const client = this.client as {
207
+ get: (key: string) => Promise<string | null>;
208
+ };
209
+ return client.get(key);
210
+ }
211
+
212
+ async set(key: string, value: string, ttl?: number): Promise<void> {
213
+ const client = this.client as {
214
+ set: (
215
+ key: string,
216
+ value: string,
217
+ options?: { ex?: number },
218
+ ) => Promise<unknown>;
219
+ };
220
+
221
+ if (ttl) {
222
+ await client.set(key, value, { ex: ttl });
223
+ } else {
224
+ await client.set(key, value);
225
+ }
226
+ }
227
+
228
+ async delete(key: string): Promise<boolean> {
229
+ const client = this.client as {
230
+ del: (key: string) => Promise<number>;
231
+ };
232
+ const result = await client.del(key);
233
+ return result > 0;
234
+ }
235
+
236
+ async has(key: string): Promise<boolean> {
237
+ const client = this.client as {
238
+ exists: (key: string) => Promise<number>;
239
+ };
240
+ const result = await client.exists(key);
241
+ return result > 0;
242
+ }
243
+
244
+ async clear(): Promise<void> {
245
+ const client = this.client as {
246
+ flushdb: () => Promise<unknown>;
247
+ };
248
+ await client.flushdb();
249
+ }
250
+
251
+ async incr(key: string): Promise<number> {
252
+ const client = this.client as {
253
+ incr: (key: string) => Promise<number>;
254
+ };
255
+ return client.incr(key);
256
+ }
257
+
258
+ async expire(key: string, ttl: number): Promise<boolean> {
259
+ const client = this.client as {
260
+ expire: (key: string, seconds: number) => Promise<number>;
261
+ };
262
+ const result = await client.expire(key, ttl);
263
+ return result === 1;
264
+ }
265
+
266
+ async ttl(key: string): Promise<number> {
267
+ const client = this.client as {
268
+ ttl: (key: string) => Promise<number>;
269
+ };
270
+ return client.ttl(key);
271
+ }
272
+
273
+ async publish(channel: string, message: string): Promise<number> {
274
+ const client = this.client as {
275
+ publish: (channel: string, message: string) => Promise<number>;
276
+ };
277
+ return client.publish(channel, message);
278
+ }
279
+
280
+ async subscribe(
281
+ channel: string,
282
+ callback: (message: string) => void,
283
+ ): Promise<void> {
284
+ // Bun.redis subscribe uses a different pattern
285
+ // For simplicity, we'll use the command pattern
286
+ const client = this.client as {
287
+ subscribe: (
288
+ channel: string,
289
+ callback: (message: string) => void,
290
+ ) => Promise<void>;
291
+ };
292
+ await client.subscribe(channel, callback);
293
+ }
294
+ }
295
+
296
+ // ============= Cache Driver Interface =============
297
+
298
+ interface CacheDriver {
299
+ get(key: string): Promise<string | null>;
300
+ set(key: string, value: string, ttl?: number): Promise<void>;
301
+ delete(key: string): Promise<boolean>;
302
+ has(key: string): Promise<boolean>;
303
+ clear(): Promise<void>;
304
+ incr(key: string): Promise<number>;
305
+ expire?(key: string, ttl: number): Promise<boolean>;
306
+ ttl?(key: string): Promise<number>;
307
+ publish?(channel: string, message: string): Promise<number>;
308
+ subscribe?(
309
+ channel: string,
310
+ callback: (message: string) => void,
311
+ ): Promise<void>;
312
+ destroy?(): void;
313
+ }
314
+
315
+ // ============= Cache Class =============
316
+
317
+ export class Cache {
318
+ private driver: CacheDriver;
319
+ private keyPrefix: string;
320
+ private defaultTTL: number;
321
+ private _isConnected = false;
322
+ private driverType: "redis" | "memory";
323
+ private enableMetrics: boolean;
324
+
325
+ // Metrics tracking
326
+ private metrics: CacheMetrics = {
327
+ hits: 0,
328
+ misses: 0,
329
+ sets: 0,
330
+ deletes: 0,
331
+ errors: 0,
332
+ avgLatency: 0,
333
+ totalOperations: 0,
334
+ };
335
+ private totalLatency = 0; // For calculating average
336
+
337
+ constructor(config: CacheConfig = {}) {
338
+ this.driverType = config.driver ?? "memory";
339
+ this.keyPrefix = config.keyPrefix ?? "bueno:";
340
+ this.defaultTTL = config.ttl ?? 3600;
341
+ this.enableMetrics = config.enableMetrics ?? true;
342
+
343
+ if (this.driverType === "redis" && config.url) {
344
+ this.driver = new RedisCache(config.url);
345
+ } else {
346
+ this.driver = new MemoryCache();
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Get current metrics snapshot
352
+ */
353
+ getMetrics(): CacheMetrics {
354
+ return { ...this.metrics };
355
+ }
356
+
357
+ /**
358
+ * Reset metrics counters
359
+ */
360
+ resetMetrics(): void {
361
+ this.metrics = {
362
+ hits: 0,
363
+ misses: 0,
364
+ sets: 0,
365
+ deletes: 0,
366
+ errors: 0,
367
+ avgLatency: 0,
368
+ totalOperations: 0,
369
+ };
370
+ this.totalLatency = 0;
371
+ }
372
+
373
+ /**
374
+ * Update metrics counters
375
+ */
376
+ private updateMetrics(
377
+ operation: "get" | "set" | "delete",
378
+ hit?: boolean,
379
+ latency?: number,
380
+ error?: boolean,
381
+ ): void {
382
+ if (!this.enableMetrics) return;
383
+
384
+ this.metrics.totalOperations++;
385
+
386
+ if (latency !== undefined) {
387
+ this.totalLatency += latency;
388
+ this.metrics.avgLatency = this.totalLatency / this.metrics.totalOperations;
389
+ }
390
+
391
+ if (error) {
392
+ this.metrics.errors++;
393
+ }
394
+
395
+ switch (operation) {
396
+ case "get":
397
+ if (hit === true) {
398
+ this.metrics.hits++;
399
+ } else if (hit === false) {
400
+ this.metrics.misses++;
401
+ }
402
+ break;
403
+ case "set":
404
+ this.metrics.sets++;
405
+ break;
406
+ case "delete":
407
+ this.metrics.deletes++;
408
+ break;
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Get current timestamp in milliseconds
414
+ */
415
+ private getTimestamp(): number {
416
+ // Use Bun.nanoseconds() if available, otherwise performance.now()
417
+ try {
418
+ return Bun.nanoseconds() / 1_000_000;
419
+ } catch {
420
+ return performance.now();
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Connect to cache
426
+ */
427
+ async connect(): Promise<void> {
428
+ if ("connect" in this.driver && typeof this.driver.connect === "function") {
429
+ await (this.driver as RedisCache).connect();
430
+ }
431
+ this._isConnected = true;
432
+ }
433
+
434
+ /**
435
+ * Disconnect from cache
436
+ */
437
+ async disconnect(): Promise<void> {
438
+ if (
439
+ "disconnect" in this.driver &&
440
+ typeof this.driver.disconnect === "function"
441
+ ) {
442
+ await (this.driver as RedisCache).disconnect();
443
+ } else if (
444
+ "destroy" in this.driver &&
445
+ typeof this.driver.destroy === "function"
446
+ ) {
447
+ (this.driver as MemoryCache).destroy();
448
+ }
449
+ this._isConnected = false;
450
+ }
451
+
452
+ /**
453
+ * Check if connected
454
+ */
455
+ get isConnected(): boolean {
456
+ return this._isConnected;
457
+ }
458
+
459
+ /**
460
+ * Get the driver type
461
+ */
462
+ getDriverType(): "redis" | "memory" {
463
+ return this.driverType;
464
+ }
465
+
466
+ /**
467
+ * Get a value
468
+ */
469
+ async get<T = unknown>(key: string): Promise<T | null> {
470
+ const startTime = this.getTimestamp();
471
+ const fullKey = this.keyPrefix + key;
472
+
473
+ try {
474
+ const value = await this.driver.get(fullKey);
475
+ const latency = this.getTimestamp() - startTime;
476
+
477
+ if (value === null || value === undefined) {
478
+ this.updateMetrics("get", false, latency, false);
479
+ return null;
480
+ }
481
+
482
+ // Try to parse JSON
483
+ try {
484
+ const parsed = JSON.parse(value) as T;
485
+ this.updateMetrics("get", true, latency, false);
486
+ return parsed;
487
+ } catch {
488
+ this.updateMetrics("get", true, latency, false);
489
+ return value as T;
490
+ }
491
+ } catch (error) {
492
+ const latency = this.getTimestamp() - startTime;
493
+ this.updateMetrics("get", false, latency, true);
494
+ throw error;
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Set a value
500
+ */
501
+ async set<T>(key: string, value: T, ttl?: number): Promise<void> {
502
+ const startTime = this.getTimestamp();
503
+ const fullKey = this.keyPrefix + key;
504
+ const serialized =
505
+ typeof value === "string" ? value : JSON.stringify(value);
506
+
507
+ try {
508
+ await this.driver.set(fullKey, serialized, ttl ?? this.defaultTTL);
509
+ const latency = this.getTimestamp() - startTime;
510
+ this.updateMetrics("set", undefined, latency, false);
511
+ } catch (error) {
512
+ const latency = this.getTimestamp() - startTime;
513
+ this.updateMetrics("set", undefined, latency, true);
514
+ throw error;
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Delete a value
520
+ */
521
+ async delete(key: string): Promise<boolean> {
522
+ const startTime = this.getTimestamp();
523
+ const fullKey = this.keyPrefix + key;
524
+
525
+ try {
526
+ const result = await this.driver.delete(fullKey);
527
+ const latency = this.getTimestamp() - startTime;
528
+ this.updateMetrics("delete", undefined, latency, false);
529
+ return result;
530
+ } catch (error) {
531
+ const latency = this.getTimestamp() - startTime;
532
+ this.updateMetrics("delete", undefined, latency, true);
533
+ throw error;
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Check if key exists
539
+ */
540
+ async has(key: string): Promise<boolean> {
541
+ const fullKey = this.keyPrefix + key;
542
+ return this.driver.has(fullKey);
543
+ }
544
+
545
+ /**
546
+ * Increment a value
547
+ */
548
+ async increment(key: string, by = 1): Promise<number> {
549
+ const fullKey = this.keyPrefix + key;
550
+ if (by === 1) {
551
+ return this.driver.incr(fullKey);
552
+ }
553
+ // For non-1 increments, get and set
554
+ const current = (await this.get<number>(key)) ?? 0;
555
+ const newValue = current + by;
556
+ await this.set(key, newValue);
557
+ return newValue;
558
+ }
559
+
560
+ /**
561
+ * Decrement a value
562
+ */
563
+ async decrement(key: string, by = 1): Promise<number> {
564
+ return this.increment(key, -by);
565
+ }
566
+
567
+ /**
568
+ * Get remaining TTL
569
+ */
570
+ async ttl(key: string): Promise<number> {
571
+ const fullKey = this.keyPrefix + key;
572
+ if (this.driver.ttl) {
573
+ return this.driver.ttl(fullKey);
574
+ }
575
+ return -1;
576
+ }
577
+
578
+ /**
579
+ * Set expiration on a key
580
+ */
581
+ async expire(key: string, ttl: number): Promise<boolean> {
582
+ const fullKey = this.keyPrefix + key;
583
+ if (this.driver.expire) {
584
+ return this.driver.expire(fullKey, ttl);
585
+ }
586
+ return false;
587
+ }
588
+
589
+ /**
590
+ * Set multiple values
591
+ */
592
+ async mset(values: Record<string, unknown>, ttl?: number): Promise<void> {
593
+ for (const [key, value] of Object.entries(values)) {
594
+ await this.set(key, value, ttl);
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Get multiple values
600
+ */
601
+ async mget<T = unknown>(keys: string[]): Promise<(T | null)[]> {
602
+ return Promise.all(keys.map((key) => this.get<T>(key)));
603
+ }
604
+
605
+ /**
606
+ * Clear all keys with prefix
607
+ */
608
+ async clear(): Promise<void> {
609
+ await this.driver.clear();
610
+ }
611
+
612
+ /**
613
+ * Get or set (cache-aside pattern)
614
+ */
615
+ async getOrSet<T>(
616
+ key: string,
617
+ factory: () => Promise<T>,
618
+ ttl?: number,
619
+ ): Promise<T> {
620
+ const cached = await this.get<T>(key);
621
+
622
+ if (cached !== null) {
623
+ return cached;
624
+ }
625
+
626
+ const value = await factory();
627
+ await this.set(key, value, ttl);
628
+ return value;
629
+ }
630
+
631
+ /**
632
+ * Delete multiple keys
633
+ */
634
+ async mdelete(keys: string[]): Promise<void> {
635
+ for (const key of keys) {
636
+ await this.delete(key);
637
+ }
638
+ }
639
+
640
+ /**
641
+ * Publish a message to a channel (Redis only)
642
+ */
643
+ async publish(channel: string, message: string): Promise<number> {
644
+ if (this.driver.publish) {
645
+ return this.driver.publish(channel, message);
646
+ }
647
+ console.warn("Publish only available with Redis driver");
648
+ return 0;
649
+ }
650
+
651
+ /**
652
+ * Subscribe to a channel (Redis only)
653
+ */
654
+ async subscribe(
655
+ channel: string,
656
+ callback: (message: string) => void,
657
+ ): Promise<void> {
658
+ if (this.driver.subscribe) {
659
+ return this.driver.subscribe(channel, callback);
660
+ }
661
+ console.warn("Subscribe only available with Redis driver");
662
+ }
663
+
664
+ /**
665
+ * Remember with lock (prevent cache stampede)
666
+ */
667
+ async remember<T>(
668
+ key: string,
669
+ factory: () => Promise<T>,
670
+ ttl?: number,
671
+ lockTimeout = 10,
672
+ ): Promise<T> {
673
+ const cached = await this.get<T>(key);
674
+ if (cached !== null) {
675
+ return cached;
676
+ }
677
+
678
+ // Try to acquire lock
679
+ const lockKey = `lock:${key}`;
680
+ const lockAcquired = await this.has(lockKey);
681
+
682
+ if (lockAcquired) {
683
+ // Wait and retry
684
+ await new Promise((resolve) => setTimeout(resolve, 100));
685
+ return this.remember(key, factory, ttl, lockTimeout);
686
+ }
687
+
688
+ // Set lock
689
+ await this.set(lockKey, "1", lockTimeout);
690
+
691
+ try {
692
+ const value = await factory();
693
+ await this.set(key, value, ttl);
694
+ return value;
695
+ } finally {
696
+ // Release lock
697
+ await this.delete(lockKey);
698
+ }
699
+ }
700
+ }
701
+
702
+ // ============= Session Store =============
703
+
704
+ export class SessionStore {
705
+ private cache: Cache;
706
+ private ttl: number;
707
+
708
+ constructor(options: SessionStoreOptions = {}) {
709
+ this.cache = new Cache({
710
+ keyPrefix: options.prefix ?? "session:",
711
+ ttl: options.ttl ?? 86400, // 1 day default
712
+ driver: options.driver ?? "memory",
713
+ url: options.url,
714
+ });
715
+ this.ttl = options.ttl ?? 86400;
716
+ }
717
+
718
+ /**
719
+ * Initialize the session store
720
+ */
721
+ async init(): Promise<void> {
722
+ await this.cache.connect();
723
+ }
724
+
725
+ /**
726
+ * Create a new session
727
+ */
728
+ async create(data: SessionData): Promise<string> {
729
+ const sessionId = crypto.randomUUID();
730
+ await this.cache.set(sessionId, data, this.ttl);
731
+ return sessionId;
732
+ }
733
+
734
+ /**
735
+ * Get session data
736
+ */
737
+ async get(sessionId: string): Promise<SessionData | null> {
738
+ return this.cache.get<SessionData>(sessionId);
739
+ }
740
+
741
+ /**
742
+ * Update session data
743
+ */
744
+ async update(sessionId: string, data: SessionData): Promise<void> {
745
+ const existing = await this.get(sessionId);
746
+ if (existing) {
747
+ await this.cache.set(sessionId, { ...existing, ...data }, this.ttl);
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Delete a session
753
+ */
754
+ async delete(sessionId: string): Promise<void> {
755
+ await this.cache.delete(sessionId);
756
+ }
757
+
758
+ /**
759
+ * Refresh session TTL
760
+ */
761
+ async refresh(sessionId: string): Promise<boolean> {
762
+ const data = await this.get(sessionId);
763
+ if (data) {
764
+ await this.cache.set(sessionId, data, this.ttl);
765
+ return true;
766
+ }
767
+ return false;
768
+ }
769
+
770
+ /**
771
+ * Check if session exists
772
+ */
773
+ async has(sessionId: string): Promise<boolean> {
774
+ return this.cache.has(sessionId);
775
+ }
776
+ }
777
+
778
+ // ============= Factory Functions =============
779
+
780
+ /**
781
+ * Create a cache instance
782
+ */
783
+ export function createCache(config?: CacheConfig): Cache {
784
+ const cache = new Cache(config);
785
+ return cache;
786
+ }
787
+
788
+ /**
789
+ * Create a session store
790
+ */
791
+ export function createSessionStore(
792
+ options?: SessionStoreOptions,
793
+ ): SessionStore {
794
+ return new SessionStore(options);
795
+ }