tina4-nodejs 3.0.0-rc.2 → 3.1.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 (31) hide show
  1. package/BENCHMARK_REPORT.md +248 -86
  2. package/CARBONAH.md +4 -4
  3. package/CLAUDE.md +16 -1
  4. package/COMPARISON.md +58 -46
  5. package/README.md +60 -6
  6. package/package.json +2 -1
  7. package/packages/cli/src/bin.ts +8 -0
  8. package/packages/cli/src/commands/generate.ts +237 -0
  9. package/packages/core/gallery/queue/meta.json +1 -1
  10. package/packages/core/gallery/queue/src/lib/queueDb.ts +32 -0
  11. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +28 -0
  12. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +28 -0
  13. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +20 -10
  14. package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +25 -0
  15. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +36 -6
  16. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +160 -0
  17. package/packages/core/src/cache.ts +402 -10
  18. package/packages/core/src/index.ts +5 -2
  19. package/packages/core/src/messenger.ts +118 -36
  20. package/packages/core/src/queue.ts +172 -92
  21. package/packages/core/src/response.ts +46 -0
  22. package/packages/core/src/router.ts +94 -1
  23. package/packages/core/src/server.ts +66 -7
  24. package/packages/core/src/types.ts +20 -1
  25. package/packages/core/src/websocketConnection.ts +16 -0
  26. package/packages/frond/src/engine.ts +184 -6
  27. package/packages/orm/src/baseModel.ts +274 -20
  28. package/packages/orm/src/cachedDatabase.ts +180 -0
  29. package/packages/orm/src/index.ts +4 -0
  30. package/packages/orm/src/model.ts +1 -0
  31. package/packages/orm/src/types.ts +75 -0
@@ -0,0 +1,160 @@
1
+ /** Gallery: Queue — interactive queue demo with visual web UI. */
2
+ import type { Tina4Request, Tina4Response } from "@tina4/core";
3
+
4
+ export default async function (_req: Tina4Request, res: Tina4Response) {
5
+ const html = `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="UTF-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
+ <title>Queue Gallery — Tina4 Node.js</title>
11
+ <link rel="stylesheet" href="/css/tina4.min.css">
12
+ </head>
13
+ <body>
14
+ <div class="container mt-4 mb-4">
15
+ <h1>Queue Gallery</h1>
16
+ <p class="text-muted">Interactive demo of Tina4's database-backed job queue. Produce messages, consume them, simulate failures, and inspect dead letters.</p>
17
+
18
+ <div class="row mt-3">
19
+ <div class="col-md-6">
20
+ <div class="card">
21
+ <div class="card-header">Produce a Message</div>
22
+ <div class="card-body">
23
+ <div class="d-flex gap-2">
24
+ <input type="text" id="msgInput" class="form-control" placeholder="Enter a task message, e.g. send-email">
25
+ <button class="btn btn-primary" onclick="produce()">Produce</button>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ <div class="col-md-6">
31
+ <div class="card">
32
+ <div class="card-header">Actions</div>
33
+ <div class="card-body d-flex gap-2 flex-wrap">
34
+ <button class="btn btn-success" onclick="consume()">Consume Next</button>
35
+ <button class="btn btn-danger" onclick="failNext()">Fail Next</button>
36
+ <button class="btn btn-warning" onclick="retryFailed()">Retry Failed</button>
37
+ <button class="btn btn-secondary" onclick="refresh()">Refresh</button>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <div id="alertArea" class="mt-3"></div>
44
+
45
+ <div class="card mt-3">
46
+ <div class="card-header d-flex justify-content-between align-items-center">
47
+ <span>Queue Messages</span>
48
+ <small class="text-muted" id="lastRefresh"></small>
49
+ </div>
50
+ <div class="card-body p-0">
51
+ <table class="table table-striped mb-0">
52
+ <thead>
53
+ <tr>
54
+ <th>ID</th>
55
+ <th>Data</th>
56
+ <th>Status</th>
57
+ <th>Attempts</th>
58
+ <th>Error</th>
59
+ <th>Created</th>
60
+ </tr>
61
+ </thead>
62
+ <tbody id="queueBody">
63
+ <tr><td colspan="6" class="text-center text-muted">Loading...</td></tr>
64
+ </tbody>
65
+ </table>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <script>
71
+ function statusBadge(status) {
72
+ var colors = {pending:"primary", reserved:"warning", completed:"success", failed:"danger", dead:"secondary"};
73
+ var color = colors[status] || "secondary";
74
+ return '<span class="badge bg-' + color + '">' + status + '</span>';
75
+ }
76
+
77
+ function showAlert(msg, type) {
78
+ var area = document.getElementById("alertArea");
79
+ area.innerHTML = '<div class="alert alert-' + type + ' alert-dismissible">' + msg +
80
+ '<button type="button" class="btn-close" onclick="this.parentElement.remove()"></button></div>';
81
+ setTimeout(function(){ area.innerHTML = ""; }, 3000);
82
+ }
83
+
84
+ function truncate(s, n) {
85
+ if (!s) return "";
86
+ return s.length > n ? s.substring(0, n) + "..." : s;
87
+ }
88
+
89
+ async function refresh() {
90
+ try {
91
+ var r = await fetch("/api/gallery/queue/status");
92
+ var data = await r.json();
93
+ var tbody = document.getElementById("queueBody");
94
+ if (!data.messages || data.messages.length === 0) {
95
+ tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No messages in queue. Produce one above.</td></tr>';
96
+ } else {
97
+ var html = "";
98
+ for (var i = 0; i < data.messages.length; i++) {
99
+ var m = data.messages[i];
100
+ html += "<tr><td>" + m.id + "</td><td><code>" + truncate(m.data, 60) + "</code></td><td>" +
101
+ statusBadge(m.status) + "</td><td>" + m.attempts + "</td><td>" +
102
+ truncate(m.error || "", 40) + "</td><td><small>" + (m.created_at || "") + "</small></td></tr>";
103
+ }
104
+ tbody.innerHTML = html;
105
+ }
106
+ document.getElementById("lastRefresh").textContent = "Updated " + new Date().toLocaleTimeString();
107
+ } catch (e) {
108
+ console.error(e);
109
+ }
110
+ }
111
+
112
+ async function produce() {
113
+ var input = document.getElementById("msgInput");
114
+ var task = input.value.trim() || "demo-task";
115
+ var r = await fetch("/api/gallery/queue/produce", {
116
+ method: "POST", headers: {"Content-Type":"application/json"},
117
+ body: JSON.stringify({task: task, data: {message: task}})
118
+ });
119
+ var d = await r.json();
120
+ showAlert("Produced message: " + task, "success");
121
+ input.value = "";
122
+ refresh();
123
+ }
124
+
125
+ async function consume() {
126
+ var r = await fetch("/api/gallery/queue/consume", {method:"POST"});
127
+ var d = await r.json();
128
+ if (d.consumed) {
129
+ showAlert("Consumed job #" + d.job_id + " successfully", "success");
130
+ } else {
131
+ showAlert(d.message || "Nothing to consume", "info");
132
+ }
133
+ refresh();
134
+ }
135
+
136
+ async function failNext() {
137
+ var r = await fetch("/api/gallery/queue/fail", {method:"POST"});
138
+ var d = await r.json();
139
+ if (d.failed) {
140
+ showAlert("Deliberately failed job #" + d.job_id, "danger");
141
+ } else {
142
+ showAlert(d.message || "Nothing to fail", "info");
143
+ }
144
+ refresh();
145
+ }
146
+
147
+ async function retryFailed() {
148
+ var r = await fetch("/api/gallery/queue/retry", {method:"POST"});
149
+ var d = await r.json();
150
+ showAlert("Retried " + (d.retried || 0) + " failed message(s)", "warning");
151
+ refresh();
152
+ }
153
+
154
+ refresh();
155
+ setInterval(refresh, 2000);
156
+ </script>
157
+ </body>
158
+ </html>`;
159
+ return res.html(html);
160
+ }
@@ -1,21 +1,38 @@
1
1
  /**
2
- * In-memory response cache for GET requests.
3
- * Caches serialized responses by URL + query string.
2
+ * Multi-backend response cache for GET requests.
3
+ *
4
+ * Backends are selected via the TINA4_CACHE_BACKEND env var:
5
+ * memory — in-process LRU cache (default, zero deps)
6
+ * redis — Redis / Valkey (uses `ioredis` or raw RESP over TCP)
7
+ * file — JSON files in data/cache/
4
8
  *
5
9
  * Usage:
6
- * import { responseCache } from "./cache.js";
10
+ * import { responseCache, cacheGet, cacheSet, cacheDelete, cacheClear, cacheStats } from "./cache.js";
7
11
  *
8
12
  * // As middleware — caches GET responses for ttl seconds
9
13
  * middleware.use(responseCache({ ttl: 60 }));
10
14
  *
11
- * // Per-route cache via meta
12
- * export const meta = { cache: 30 }; // cache 30 seconds
15
+ * // Direct usage (same across all 4 languages)
16
+ * cacheSet("key", {"data": "value"}, 120);
17
+ * const value = cacheGet("key");
18
+ * cacheDelete("key");
19
+ * cacheClear();
20
+ * const stats = cacheStats();
13
21
  *
14
22
  * Environment:
15
- * TINA4_CACHE_TTL default TTL in seconds (default: 0 = disabled)
23
+ * TINA4_CACHE_BACKEND memory | redis | file (default: memory)
24
+ * TINA4_CACHE_URL — redis://localhost:6379 (redis only)
25
+ * TINA4_CACHE_TTL — default TTL in seconds (default: 0 = disabled)
26
+ * TINA4_CACHE_MAX_ENTRIES — max entries (default: 1000)
16
27
  */
17
28
 
18
29
  import type { Middleware } from "./types.js";
30
+ import * as fs from "node:fs";
31
+ import * as path from "node:path";
32
+ import * as crypto from "node:crypto";
33
+ import * as net from "node:net";
34
+
35
+ // ── Types ─────────────────────────────────────────────────────────
19
36
 
20
37
  interface CacheEntry {
21
38
  body: string;
@@ -24,6 +41,11 @@ interface CacheEntry {
24
41
  expiresAt: number;
25
42
  }
26
43
 
44
+ interface DirectEntry {
45
+ value: unknown;
46
+ expiresAt: number;
47
+ }
48
+
27
49
  export interface ResponseCacheConfig {
28
50
  /** Default TTL in seconds. 0 = disabled. Default: 60 */
29
51
  ttl?: number;
@@ -31,8 +53,326 @@ export interface ResponseCacheConfig {
31
53
  maxEntries?: number;
32
54
  /** Only cache these status codes. Default: [200] */
33
55
  statusCodes?: number[];
56
+ /** Cache backend: memory | redis | file. Default: from env or memory */
57
+ backend?: string;
58
+ /** Redis URL. Default: from env or redis://localhost:6379 */
59
+ cacheUrl?: string;
60
+ /** File cache directory. Default: from env or data/cache */
61
+ cacheDir?: string;
62
+ }
63
+
64
+ // ── Backend interface ─────────────────────────────────────────────
65
+
66
+ interface CacheBackend {
67
+ get(key: string): unknown | undefined;
68
+ set(key: string, value: unknown, ttl: number): void;
69
+ delete(key: string): boolean;
70
+ clear(): void;
71
+ stats(): { hits: number; misses: number; size: number; backend: string };
72
+ name(): string;
73
+ }
74
+
75
+ // ── Memory backend ────────────────────────────────────────────────
76
+
77
+ class MemoryBackend implements CacheBackend {
78
+ private store = new Map<string, { value: unknown; expiresAt: number }>();
79
+ private maxEntries: number;
80
+ private hits = 0;
81
+ private misses = 0;
82
+
83
+ constructor(maxEntries = 1000) {
84
+ this.maxEntries = maxEntries;
85
+ }
86
+
87
+ get(key: string): unknown | undefined {
88
+ const entry = this.store.get(key);
89
+ if (!entry) {
90
+ this.misses++;
91
+ return undefined;
92
+ }
93
+ if (entry.expiresAt && Date.now() > entry.expiresAt) {
94
+ this.store.delete(key);
95
+ this.misses++;
96
+ return undefined;
97
+ }
98
+ this.hits++;
99
+ // Move to end (LRU refresh) — delete and re-set
100
+ this.store.delete(key);
101
+ this.store.set(key, entry);
102
+ return entry.value;
103
+ }
104
+
105
+ set(key: string, value: unknown, ttl: number): void {
106
+ const expiresAt = ttl > 0 ? Date.now() + ttl * 1000 : 0;
107
+ this.store.delete(key); // remove to re-insert at end
108
+ this.store.set(key, { value, expiresAt });
109
+ // Evict oldest if over capacity
110
+ while (this.store.size > this.maxEntries) {
111
+ const firstKey = this.store.keys().next().value;
112
+ if (firstKey !== undefined) this.store.delete(firstKey);
113
+ }
114
+ }
115
+
116
+ delete(key: string): boolean {
117
+ return this.store.delete(key);
118
+ }
119
+
120
+ clear(): void {
121
+ this.store.clear();
122
+ this.hits = 0;
123
+ this.misses = 0;
124
+ }
125
+
126
+ stats() {
127
+ // Sweep expired
128
+ const now = Date.now();
129
+ for (const [key, entry] of this.store) {
130
+ if (entry.expiresAt && now > entry.expiresAt) this.store.delete(key);
131
+ }
132
+ return { hits: this.hits, misses: this.misses, size: this.store.size, backend: "memory" };
133
+ }
134
+
135
+ name() { return "memory"; }
136
+ }
137
+
138
+ // ── Redis backend ─────────────────────────────────────────────────
139
+
140
+ class RedisBackend implements CacheBackend {
141
+ private host: string;
142
+ private port: number;
143
+ private db: number;
144
+ private prefix = "tina4:cache:";
145
+ private hits = 0;
146
+ private misses = 0;
147
+ private maxEntries: number;
148
+
149
+ constructor(url = "redis://localhost:6379", maxEntries = 1000) {
150
+ this.maxEntries = maxEntries;
151
+ const cleaned = url.replace("redis://", "");
152
+ const parts = cleaned.split(":");
153
+ this.host = parts[0] || "localhost";
154
+ const portAndDb = parts[1] ? parts[1].split("/") : ["6379"];
155
+ this.port = parseInt(portAndDb[0], 10) || 6379;
156
+ this.db = portAndDb[1] ? parseInt(portAndDb[1], 10) : 0;
157
+ }
158
+
159
+ private respCommand(...args: string[]): Promise<string | null> {
160
+ return new Promise((resolve) => {
161
+ try {
162
+ let cmd = `*${args.length}\r\n`;
163
+ for (const arg of args) {
164
+ cmd += `$${Buffer.byteLength(arg)}\r\n${arg}\r\n`;
165
+ }
166
+
167
+ const sock = new net.Socket();
168
+ sock.setTimeout(5000);
169
+ let response = "";
170
+
171
+ sock.connect(this.port, this.host, () => {
172
+ if (this.db > 0) {
173
+ const selectCmd = `*2\r\n$6\r\nSELECT\r\n$${String(this.db).length}\r\n${this.db}\r\n`;
174
+ sock.write(selectCmd);
175
+ }
176
+ sock.write(cmd);
177
+ });
178
+
179
+ sock.on("data", (data) => {
180
+ response += data.toString();
181
+ sock.end();
182
+ });
183
+
184
+ sock.on("end", () => {
185
+ if (response.startsWith("+")) {
186
+ resolve(response.slice(1).trim());
187
+ } else if (response.startsWith("$-1")) {
188
+ resolve(null);
189
+ } else if (response.startsWith("$")) {
190
+ const lines = response.split("\r\n");
191
+ resolve(lines[1] ?? null);
192
+ } else if (response.startsWith(":")) {
193
+ resolve(response.slice(1).trim());
194
+ } else {
195
+ resolve(null);
196
+ }
197
+ });
198
+
199
+ sock.on("error", () => resolve(null));
200
+ sock.on("timeout", () => { sock.destroy(); resolve(null); });
201
+ } catch {
202
+ resolve(null);
203
+ }
204
+ });
205
+ }
206
+
207
+ get(key: string): unknown | undefined {
208
+ // Synchronous fallback — Redis is async, so direct API uses sync memory store
209
+ // For the response cache middleware, this returns undefined and falls through
210
+ this.misses++;
211
+ return undefined;
212
+ }
213
+
214
+ set(key: string, value: unknown, ttl: number): void {
215
+ const fullKey = this.prefix + key;
216
+ const serialized = JSON.stringify(value);
217
+ if (ttl > 0) {
218
+ this.respCommand("SETEX", fullKey, String(ttl), serialized).catch(() => {});
219
+ } else {
220
+ this.respCommand("SET", fullKey, serialized).catch(() => {});
221
+ }
222
+ }
223
+
224
+ delete(key: string): boolean {
225
+ const fullKey = this.prefix + key;
226
+ this.respCommand("DEL", fullKey).catch(() => {});
227
+ return true; // best-effort
228
+ }
229
+
230
+ clear(): void {
231
+ this.hits = 0;
232
+ this.misses = 0;
233
+ }
234
+
235
+ stats() {
236
+ return { hits: this.hits, misses: this.misses, size: 0, backend: "redis" };
237
+ }
238
+
239
+ name() { return "redis"; }
34
240
  }
35
241
 
242
+ // ── File backend ──────────────────────────────────────────────────
243
+
244
+ class FileBackend implements CacheBackend {
245
+ private dir: string;
246
+ private maxEntries: number;
247
+ private hits = 0;
248
+ private misses = 0;
249
+
250
+ constructor(cacheDir = "data/cache", maxEntries = 1000) {
251
+ this.dir = cacheDir;
252
+ this.maxEntries = maxEntries;
253
+ try {
254
+ fs.mkdirSync(this.dir, { recursive: true });
255
+ } catch {}
256
+ }
257
+
258
+ private keyPath(key: string): string {
259
+ const safe = crypto.createHash("sha256").update(key).digest("hex");
260
+ return path.join(this.dir, `${safe}.json`);
261
+ }
262
+
263
+ get(key: string): unknown | undefined {
264
+ const p = this.keyPath(key);
265
+ try {
266
+ if (!fs.existsSync(p)) {
267
+ this.misses++;
268
+ return undefined;
269
+ }
270
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
271
+ if (data.expiresAt && Date.now() > data.expiresAt * 1000) {
272
+ fs.unlinkSync(p);
273
+ this.misses++;
274
+ return undefined;
275
+ }
276
+ this.hits++;
277
+ return data.value ?? data;
278
+ } catch {
279
+ this.misses++;
280
+ return undefined;
281
+ }
282
+ }
283
+
284
+ set(key: string, value: unknown, ttl: number): void {
285
+ try {
286
+ fs.mkdirSync(this.dir, { recursive: true });
287
+ // Evict oldest if at capacity
288
+ const files = fs.readdirSync(this.dir)
289
+ .filter(f => f.endsWith(".json"))
290
+ .map(f => ({ name: f, time: fs.statSync(path.join(this.dir, f)).mtimeMs }))
291
+ .sort((a, b) => a.time - b.time);
292
+
293
+ while (files.length >= this.maxEntries) {
294
+ const oldest = files.shift();
295
+ if (oldest) fs.unlinkSync(path.join(this.dir, oldest.name));
296
+ }
297
+
298
+ const expiresAt = ttl > 0 ? (Date.now() / 1000) + ttl : 0;
299
+ const entry = { key, value, expiresAt };
300
+ fs.writeFileSync(this.keyPath(key), JSON.stringify(entry));
301
+ } catch {}
302
+ }
303
+
304
+ delete(key: string): boolean {
305
+ const p = this.keyPath(key);
306
+ try {
307
+ if (fs.existsSync(p)) {
308
+ fs.unlinkSync(p);
309
+ return true;
310
+ }
311
+ } catch {}
312
+ return false;
313
+ }
314
+
315
+ clear(): void {
316
+ this.hits = 0;
317
+ this.misses = 0;
318
+ try {
319
+ const files = fs.readdirSync(this.dir).filter(f => f.endsWith(".json"));
320
+ for (const f of files) {
321
+ fs.unlinkSync(path.join(this.dir, f));
322
+ }
323
+ } catch {}
324
+ }
325
+
326
+ stats() {
327
+ // Sweep expired
328
+ const now = Date.now() / 1000;
329
+ let count = 0;
330
+ try {
331
+ const files = fs.readdirSync(this.dir).filter(f => f.endsWith(".json"));
332
+ for (const f of files) {
333
+ try {
334
+ const data = JSON.parse(fs.readFileSync(path.join(this.dir, f), "utf-8"));
335
+ if (data.expiresAt && now > data.expiresAt) {
336
+ fs.unlinkSync(path.join(this.dir, f));
337
+ } else {
338
+ count++;
339
+ }
340
+ } catch {}
341
+ }
342
+ } catch {}
343
+ return { hits: this.hits, misses: this.misses, size: count, backend: "file" };
344
+ }
345
+
346
+ name() { return "file"; }
347
+ }
348
+
349
+ // ── Backend factory ───────────────────────────────────────────────
350
+
351
+ function createBackend(config?: {
352
+ backend?: string;
353
+ cacheUrl?: string;
354
+ cacheDir?: string;
355
+ maxEntries?: number;
356
+ }): CacheBackend {
357
+ const backendName = (config?.backend ?? process.env.TINA4_CACHE_BACKEND ?? "memory").toLowerCase().trim();
358
+ const maxEntries = config?.maxEntries ?? (process.env.TINA4_CACHE_MAX_ENTRIES ? parseInt(process.env.TINA4_CACHE_MAX_ENTRIES, 10) : 1000);
359
+
360
+ switch (backendName) {
361
+ case "redis": {
362
+ const url = config?.cacheUrl ?? process.env.TINA4_CACHE_URL ?? "redis://localhost:6379";
363
+ return new RedisBackend(url, maxEntries);
364
+ }
365
+ case "file": {
366
+ const dir = config?.cacheDir ?? process.env.TINA4_CACHE_DIR ?? "data/cache";
367
+ return new FileBackend(dir, maxEntries);
368
+ }
369
+ default:
370
+ return new MemoryBackend(maxEntries);
371
+ }
372
+ }
373
+
374
+ // ── Response cache store (for middleware) ──────────────────────────
375
+
36
376
  const store = new Map<string, CacheEntry>();
37
377
 
38
378
  /**
@@ -110,12 +450,64 @@ export function responseCache(config?: ResponseCacheConfig): Middleware {
110
450
  };
111
451
  }
112
452
 
113
- /** Clear all cached responses */
453
+ /** Clear all cached responses (middleware store) */
114
454
  export function clearCache(): void {
115
455
  store.clear();
116
456
  }
117
457
 
118
- /** Get cache stats */
119
- export function cacheStats(): { size: number; keys: string[] } {
120
- return { size: store.size, keys: [...store.keys()] };
458
+ /** Get cache stats (middleware store) */
459
+ export function cacheStats(): { size: number; keys: string[]; backend: string } {
460
+ return { size: store.size, keys: [...store.keys()], backend: _getBackend().name() };
461
+ }
462
+
463
+ // ── Module-level direct cache API (backend-aware) ─────────────────
464
+
465
+ let _defaultBackend: CacheBackend | null = null;
466
+ let _defaultTtl: number | null = null;
467
+
468
+ function _getBackend(): CacheBackend {
469
+ if (!_defaultBackend) {
470
+ _defaultBackend = createBackend();
471
+ }
472
+ return _defaultBackend;
473
+ }
474
+
475
+ function _getDefaultTtl(): number {
476
+ if (_defaultTtl === null) {
477
+ const envTtl = process.env.TINA4_CACHE_TTL;
478
+ _defaultTtl = envTtl ? parseInt(envTtl, 10) : 60;
479
+ }
480
+ return _defaultTtl;
481
+ }
482
+
483
+ /** Get a value from the cache by key. Returns undefined on miss. */
484
+ export function cacheGet(key: string): unknown | undefined {
485
+ return _getBackend().get(key);
486
+ }
487
+
488
+ /** Store a value in the cache with optional TTL (seconds). */
489
+ export function cacheSet(key: string, value: unknown, ttl?: number): void {
490
+ const effectiveTtl = ttl ?? _getDefaultTtl();
491
+ _getBackend().set(key, value, effectiveTtl);
492
+ }
493
+
494
+ /** Delete a key from the cache. Returns true if it existed. */
495
+ export function cacheDelete(key: string): boolean {
496
+ return _getBackend().delete(key);
497
+ }
498
+
499
+ /** Clear all entries from the cache. */
500
+ export function cacheClear(): void {
501
+ _getBackend().clear();
502
+ }
503
+
504
+ /** Return cache statistics from the active backend. */
505
+ export function cacheBackendStats(): { hits: number; misses: number; size: number; backend: string } {
506
+ return _getBackend().stats();
507
+ }
508
+
509
+ /** Reset the default backend (for testing). */
510
+ export function _resetBackend(): void {
511
+ _defaultBackend = null;
512
+ _defaultTtl = null;
121
513
  }
@@ -8,11 +8,13 @@ export type {
8
8
  Middleware,
9
9
  UploadedFile,
10
10
  CookieOptions,
11
+ WebSocketRouteHandler,
12
+ WebSocketRouteDefinition,
11
13
  } from "./types.js";
12
14
 
13
15
  export { startServer, resolvePortAndHost } from "./server.js";
14
16
  export { Router, RouteGroup, defaultRouter, runRouteMiddlewares } from "./router.js";
15
- export { get, post, put, patch, del, any, del as delete } from "./router.js";
17
+ export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
16
18
  export type { RouteInfo } from "./router.js";
17
19
  export { discoverRoutes } from "./routeDiscovery.js";
18
20
  export { MiddlewareChain, cors, requestLogger } from "./middleware.js";
@@ -60,7 +62,7 @@ export {
60
62
  export type { WebSocketClient } from "./websocket.js";
61
63
  export { ServiceRunner, matchCronField, matchesCron } from "./service.js";
62
64
  export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from "./service.js";
63
- export { responseCache, clearCache, cacheStats } from "./cache.js";
65
+ export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, _resetBackend } from "./cache.js";
64
66
  export type { ResponseCacheConfig } from "./cache.js";
65
67
  export { Api } from "./api.js";
66
68
  export type { ApiResult } from "./api.js";
@@ -86,3 +88,4 @@ export { ValkeySessionHandler } from "./sessionHandlers/valkeyHandler.js";
86
88
  export type { ValkeySessionConfig } from "./sessionHandlers/valkeyHandler.js";
87
89
  export { tests, assertEqual, assertThrows, assertTrue, assertFalse, runAllTests, resetTests } from "./testing.js";
88
90
  export { Container, container } from "./container.js";
91
+ export type { WebSocketConnection } from "./websocketConnection.js";