@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.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- 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
|
+
}
|