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
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  </p>
19
19
 
20
20
  <p align="center">
21
- <img src="https://img.shields.io/badge/tests-1247%20passing-brightgreen" alt="Tests">
21
+ <img src="https://img.shields.io/badge/tests-1669%20passing-brightgreen" alt="Tests">
22
22
  <img src="https://img.shields.io/badge/carbonah-A%2B%20rated-00cc44" alt="Carbonah A+">
23
23
  <img src="https://img.shields.io/badge/zero--dep-core-blue" alt="Zero Dependencies">
24
24
  <img src="https://img.shields.io/badge/node-20%2B-blue" alt="Node 20+">
@@ -50,17 +50,17 @@ Every feature is built from scratch -- no npm install, no node_modules bloat, no
50
50
  | **HTTP** | Native `node:http` server, file-based + programmatic routing, path params (`{id}`, `[...slug]`), middleware pipeline, CORS, rate limiting, graceful shutdown |
51
51
  | **Templates** | Frond engine (Twig-compatible), inheritance, partials, 53+ filters, macros, fragment caching, sandboxing |
52
52
  | **ORM** | Active Record, typed fields with validation, soft delete, relationships (`hasOne`/`hasMany`/`belongsTo`), scopes, result caching, auto-CRUD |
53
- | **Database** | SQLite, PostgreSQL, MySQL, MSSQL/SQL Server, Firebird -- unified adapter interface, `driver://host:port/database` connection strings |
53
+ | **Database** | SQLite, PostgreSQL, MySQL, MSSQL/SQL Server, Firebird -- unified adapter interface, query caching (TINA4_DB_CACHE=true for 4x speedup) |
54
54
  | **Auth** | Zero-dep JWT (HS256 + RS256), sessions (file backend), PBKDF2 password hashing, form tokens |
55
55
  | **API** | Swagger/OpenAPI auto-generation, GraphQL with schema builder and GraphiQL IDE |
56
- | **Background** | File-backed queue with priority, delayed jobs, retry, batch processing |
56
+ | **Background** | Queue (SQLite/RabbitMQ/Kafka) with priority, delayed jobs, retry, batch processing |
57
57
  | **Real-time** | Native WebSocket (RFC 6455), per-path routing, connection manager, broadcast |
58
58
  | **Frontend** | tina4-css (~24 KB), frond.js helper, SCSS compiler, live reload, CSS hot-reload |
59
59
  | **DX** | Dev admin dashboard, error overlay, request inspector, hot-reload, Carbonah green benchmarks |
60
60
  | **Data** | Migrations with rollback, 26+ fake data generators, ORM and table seeders |
61
- | **Other** | Service runner, localization (i18n), in-memory cache (TTL/tags/LRU), HTTP constants, health check, configurable error pages |
61
+ | **Other** | Service runner, localization (i18n), cache (memory/Redis/file), messenger (.env driven), HTTP constants, health check, configurable error pages |
62
62
 
63
- **580 tests across all modules. All Carbonah benchmarks rated A+.**
63
+ **1,669 tests across 38 built-in features. Zero dependencies. All Carbonah benchmarks rated A+.**
64
64
 
65
65
  For full documentation visit **[tina4.com](https://tina4.com)**.
66
66
 
@@ -533,9 +533,14 @@ Set `TINA4_DEBUG_LEVEL=DEBUG` in `.env` to enable:
533
533
  ```bash
534
534
  npx tina4nodejs init [dir] # Scaffold a new project
535
535
  npx tina4nodejs serve [--port 7148] # Start dev server (default: 7148)
536
+ npx tina4nodejs serve --production # Auto-use cluster mode (multi-core)
536
537
  npx tina4nodejs migrate # Run pending migrations
537
538
  npx tina4nodejs migrate:create <desc> # Create a migration file
538
539
  npx tina4nodejs migrate:rollback # Rollback last batch
540
+ npx tina4nodejs generate model <name> # Generate model scaffold
541
+ npx tina4nodejs generate route <name> # Generate route scaffold
542
+ npx tina4nodejs generate migration <d> # Generate migration file
543
+ npx tina4nodejs generate middleware <n># Generate middleware scaffold
539
544
  npx tina4nodejs seed # Run seeders from src/seeds/
540
545
  npx tina4nodejs routes # List all registered routes
541
546
  npx tina4nodejs test # Run test suite
@@ -543,6 +548,55 @@ npx tina4nodejs build # Build distributable package
543
548
  npx tina4nodejs ai [--all] # Detect AI tools and install context
544
549
  ```
545
550
 
551
+ ### Production Server Auto-Detection
552
+
553
+ `tina4 serve` automatically detects and uses the best available production server:
554
+
555
+ - **Node.js**: cluster mode with multiple workers, otherwise single http server
556
+ - Use `npx tina4nodejs serve --production` to auto-use cluster mode
557
+
558
+ ### Scaffolding with `tina4 generate`
559
+
560
+ Quickly scaffold new components:
561
+
562
+ ```bash
563
+ npx tina4nodejs generate model User # Creates src/models/User.ts
564
+ npx tina4nodejs generate route users # Creates src/routes/api/users/
565
+ npx tina4nodejs generate migration "add age" # Creates migration SQL file
566
+ npx tina4nodejs generate middleware AuthLog # Creates middleware
567
+ ```
568
+
569
+ ### ORM Relationships & Eager Loading
570
+
571
+ ```typescript
572
+ // Relationships defined in model
573
+ static relationships = {
574
+ orders: { type: "hasMany", model: "Order", foreignKey: "userId" },
575
+ profile: { type: "hasOne", model: "Profile", foreignKey: "userId" },
576
+ customer: { type: "belongsTo", model: "Customer", foreignKey: "customerId" },
577
+ };
578
+
579
+ // Eager loading with include
580
+ const users = await db.query("SELECT * FROM users", [], { include: ["orders", "profile"] });
581
+ ```
582
+
583
+ ### DB Query Caching
584
+
585
+ Enable query caching for up to 4x speedup on read-heavy workloads:
586
+
587
+ ```bash
588
+ # .env
589
+ TINA4_DB_CACHE=true
590
+ ```
591
+
592
+ ### Frond Pre-Compilation
593
+
594
+ Templates are pre-compiled for 2.8x faster rendering.
595
+
596
+ ### Gallery
597
+
598
+ 7 interactive examples with **Try It** deploy.
599
+
546
600
  ## Environment
547
601
 
548
602
  ```bash
@@ -577,7 +631,7 @@ Full guides, API reference, and examples at **[tina4.com](https://tina4.com)**.
577
631
 
578
632
  ## License
579
633
 
580
- MIT (c) 2007-2025 Tina4 Stack
634
+ MIT (c) 2007-2026 Tina4 Stack
581
635
  https://opensource.org/licenses/MIT
582
636
 
583
637
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.0.0-rc.2",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "description": "This is not a framework. Tina4 for Node.js/TypeScript — zero deps, 38 built-in features.",
6
6
  "keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
@@ -15,6 +15,7 @@
15
15
  "packages/*"
16
16
  ],
17
17
  "main": "packages/core/src/index.ts",
18
+ "types": "packages/core/src/index.ts",
18
19
  "exports": {
19
20
  ".": "./packages/core/src/index.ts",
20
21
  "./orm": "./packages/orm/src/index.ts",
@@ -4,6 +4,7 @@ import { runMigrations } from "./commands/migrate.js";
4
4
  import { createMigration } from "./commands/migrateCreate.js";
5
5
  import { listRoutes } from "./commands/routes.js";
6
6
  import { runTests } from "./commands/test.js";
7
+ import { generate } from "./commands/generate.js";
7
8
 
8
9
  const args = process.argv.slice(2);
9
10
  const command = args[0];
@@ -18,6 +19,7 @@ const HELP = `
18
19
  tina4nodejs migrate:create <desc> Create a new migration file
19
20
  tina4nodejs routes List all registered routes
20
21
  tina4nodejs test [file] Run project tests
22
+ tina4nodejs generate <what> <name> Generate scaffolding (model, route, migration, middleware)
21
23
  tina4nodejs ai Detect AI coding tools and install context
22
24
  tina4nodejs help Show this help message
23
25
 
@@ -58,6 +60,12 @@ async function main(): Promise<void> {
58
60
  await runTests(args[1]);
59
61
  break;
60
62
  }
63
+ case "generate": {
64
+ const what = args[1];
65
+ const genName = args[2];
66
+ await generate(what, genName);
67
+ break;
68
+ }
61
69
  case "ai": {
62
70
  const { detectAi, installAiContext, installAllAiContext, aiStatusReport } = await import("../../core/src/ai.js");
63
71
  const root = args[1] || ".";
@@ -0,0 +1,237 @@
1
+ /**
2
+ * CLI command: generate — Scaffold models, routes, migrations, and middleware.
3
+ *
4
+ * Usage:
5
+ * tina4nodejs generate model User
6
+ * tina4nodejs generate route /api/users
7
+ * tina4nodejs generate migration create_users
8
+ * tina4nodejs generate middleware Auth
9
+ */
10
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
11
+ import { join, resolve } from "node:path";
12
+
13
+ export async function generate(what: string, name: string): Promise<void> {
14
+ if (!what || !name) {
15
+ console.error(" Usage: tina4nodejs generate <what> <name>");
16
+ console.error(" Generators: model, route, migration, middleware");
17
+ process.exit(1);
18
+ }
19
+
20
+ switch (what) {
21
+ case "model":
22
+ generateModel(name);
23
+ break;
24
+ case "route":
25
+ generateRoute(name);
26
+ break;
27
+ case "migration":
28
+ generateMigration(name);
29
+ break;
30
+ case "middleware":
31
+ generateMiddleware(name);
32
+ break;
33
+ default:
34
+ console.error(` Unknown generator: ${what}`);
35
+ console.error(" Available: model, route, migration, middleware");
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ // ── Helpers ──────────────────────────────────────────────────────
41
+
42
+ function ensureDir(dir: string): void {
43
+ if (!existsSync(dir)) {
44
+ mkdirSync(dir, { recursive: true });
45
+ }
46
+ }
47
+
48
+ function writeFileSafe(path: string, content: string): void {
49
+ if (existsSync(path)) {
50
+ console.error(` File already exists: ${path}`);
51
+ process.exit(1);
52
+ }
53
+ writeFileSync(path, content, "utf-8");
54
+ console.log(` Created ${path}`);
55
+ }
56
+
57
+ function toSnake(name: string): string {
58
+ return name.replace(/([A-Z])/g, (_, ch, i) => (i > 0 ? "_" : "") + ch.toLowerCase());
59
+ }
60
+
61
+ function toPlural(name: string): string {
62
+ const lower = name.toLowerCase();
63
+ if (lower.endsWith("s")) return lower;
64
+ if (lower.endsWith("y")) return lower.slice(0, -1) + "ies";
65
+ return lower + "s";
66
+ }
67
+
68
+ function toCamel(name: string): string {
69
+ return name.charAt(0).toLowerCase() + name.slice(1);
70
+ }
71
+
72
+ // ── Model ────────────────────────────────────────────────────────
73
+
74
+ function generateModel(name: string): void {
75
+ const dir = resolve("src/models");
76
+ ensureDir(dir);
77
+
78
+ const table = toPlural(name);
79
+ const path = join(dir, `${name}.ts`);
80
+
81
+ const content = `import { BaseModel } from "tina4-nodejs";
82
+
83
+ export class ${name} extends BaseModel {
84
+ static tableName = "${table}";
85
+ static fields = {
86
+ id: { type: "integer", primaryKey: true, autoIncrement: true },
87
+ name: { type: "string" },
88
+ email: { type: "string" },
89
+ };
90
+ }
91
+ `;
92
+
93
+ writeFileSafe(path, content);
94
+ }
95
+
96
+ // ── Route ────────────────────────────────────────────────────────
97
+
98
+ function generateRoute(name: string): void {
99
+ const routePath = name.replace(/^\//, "");
100
+ const base = resolve("src/routes", routePath);
101
+ const idDir = join(base, "[id]");
102
+ ensureDir(base);
103
+ ensureDir(idDir);
104
+
105
+ // GET list
106
+ writeFileSafe(
107
+ join(base, "get.ts"),
108
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
109
+
110
+ export const meta = { summary: "List all", tags: ["auto-generated"] };
111
+
112
+ export default async function (req: Tina4Request, res: Tina4Response) {
113
+ res.json({ data: [] });
114
+ }
115
+ `,
116
+ );
117
+
118
+ // POST create
119
+ writeFileSafe(
120
+ join(base, "post.ts"),
121
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
122
+
123
+ export const meta = { summary: "Create new", tags: ["auto-generated"] };
124
+
125
+ export default async function (req: Tina4Request, res: Tina4Response) {
126
+ res.json({ message: "created" }, 201);
127
+ }
128
+ `,
129
+ );
130
+
131
+ // GET by id
132
+ writeFileSafe(
133
+ join(idDir, "get.ts"),
134
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
135
+
136
+ export const meta = { summary: "Get by id", tags: ["auto-generated"] };
137
+
138
+ export default async function (req: Tina4Request, res: Tina4Response) {
139
+ const { id } = req.params;
140
+ res.json({ data: { id } });
141
+ }
142
+ `,
143
+ );
144
+
145
+ // PUT by id
146
+ writeFileSafe(
147
+ join(idDir, "put.ts"),
148
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
149
+
150
+ export const meta = { summary: "Update by id", tags: ["auto-generated"] };
151
+
152
+ export default async function (req: Tina4Request, res: Tina4Response) {
153
+ const { id } = req.params;
154
+ res.json({ message: "updated", id });
155
+ }
156
+ `,
157
+ );
158
+
159
+ // DELETE by id
160
+ writeFileSafe(
161
+ join(idDir, "delete.ts"),
162
+ `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
163
+
164
+ export const meta = { summary: "Delete by id", tags: ["auto-generated"] };
165
+
166
+ export default async function (req: Tina4Request, res: Tina4Response) {
167
+ const { id } = req.params;
168
+ res.json({ message: "deleted", id });
169
+ }
170
+ `,
171
+ );
172
+ }
173
+
174
+ // ── Migration ────────────────────────────────────────────────────
175
+
176
+ function generateMigration(name: string): void {
177
+ const dir = resolve("migrations");
178
+ ensureDir(dir);
179
+
180
+ const now = new Date();
181
+ const timestamp =
182
+ now.getFullYear().toString() +
183
+ String(now.getMonth() + 1).padStart(2, "0") +
184
+ String(now.getDate()).padStart(2, "0") +
185
+ String(now.getHours()).padStart(2, "0") +
186
+ String(now.getMinutes()).padStart(2, "0") +
187
+ String(now.getSeconds()).padStart(2, "0");
188
+
189
+ const table = toPlural(name.replace(/^create_/, ""));
190
+ const fileName = `${timestamp}_${name}.sql`;
191
+ const path = join(dir, fileName);
192
+
193
+ const content = `-- Migration: ${name}
194
+ -- Created: ${now.toISOString().replace("T", " ").replace(/\.\d+Z$/, "")}
195
+
196
+ CREATE TABLE ${table} (
197
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
198
+ name TEXT NOT NULL,
199
+ email TEXT NOT NULL,
200
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
201
+ );
202
+ `;
203
+
204
+ writeFileSafe(path, content);
205
+ }
206
+
207
+ // ── Middleware ────────────────────────────────────────────────────
208
+
209
+ function generateMiddleware(name: string): void {
210
+ const dir = resolve("src/middleware");
211
+ ensureDir(dir);
212
+
213
+ const snake = toSnake(name);
214
+ const camel = toCamel(name);
215
+ const path = join(dir, `${snake}.ts`);
216
+
217
+ const content = `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
218
+
219
+ /**
220
+ * ${name} middleware — checks for Authorization header.
221
+ */
222
+ export default async function ${camel}(
223
+ req: Tina4Request,
224
+ res: Tina4Response,
225
+ next: () => Promise<void>,
226
+ ): Promise<void> {
227
+ const auth = req.headers["authorization"];
228
+ if (!auth) {
229
+ res.json({ error: "Unauthorized" }, 401);
230
+ return;
231
+ }
232
+ await next();
233
+ }
234
+ `;
235
+
236
+ writeFileSafe(path, content);
237
+ }
@@ -1 +1 @@
1
- {"name": "Queue", "description": "Background job producer and consumer", "try_url": "/api/gallery/queue/produce"}
1
+ {"name": "Queue", "description": "Background job producer and consumer", "try_url": "/gallery/queue"}
@@ -0,0 +1,32 @@
1
+ /** Shared SQLite database helper for the queue gallery demo. */
2
+ import type { DatabaseAdapter } from "@tina4/orm";
3
+
4
+ let _db: DatabaseAdapter | null = null;
5
+
6
+ export const MAX_RETRIES = 3;
7
+
8
+ export async function getQueueDb(): Promise<DatabaseAdapter> {
9
+ if (_db) return _db;
10
+ const orm = await import("@tina4/orm");
11
+ _db = await orm.initDatabase({ type: "sqlite", path: "./data/gallery_queue.db" });
12
+ try {
13
+ _db.execute(`CREATE TABLE IF NOT EXISTS tina4_queue (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ topic TEXT NOT NULL,
16
+ data TEXT NOT NULL,
17
+ status TEXT NOT NULL DEFAULT 'pending',
18
+ priority INTEGER NOT NULL DEFAULT 0,
19
+ attempts INTEGER NOT NULL DEFAULT 0,
20
+ error TEXT,
21
+ available_at TEXT NOT NULL,
22
+ created_at TEXT NOT NULL,
23
+ completed_at TEXT,
24
+ reserved_at TEXT
25
+ )`);
26
+ } catch (_e) { /* table already exists */ }
27
+ return _db;
28
+ }
29
+
30
+ export function now(): string {
31
+ return new Date().toISOString();
32
+ }
@@ -0,0 +1,28 @@
1
+ /** Gallery: Queue — consume (complete) the next pending message. */
2
+ import type { Tina4Request, Tina4Response } from "@tina4/core";
3
+ import { getQueueDb, now } from "../../../../lib/queueDb.js";
4
+
5
+ export default async function (_req: Tina4Request, res: Tina4Response) {
6
+ try {
7
+ const db = await getQueueDb();
8
+ const ts = now();
9
+
10
+ const row = db.fetchOne<Record<string, unknown>>(
11
+ "SELECT * FROM tina4_queue WHERE topic = ? AND status = 'pending' AND available_at <= ? ORDER BY priority DESC, id ASC",
12
+ ["gallery-tasks", ts]
13
+ );
14
+
15
+ if (!row) {
16
+ return res.json({ consumed: false, message: "No pending messages to consume" });
17
+ }
18
+
19
+ db.execute(
20
+ "UPDATE tina4_queue SET status = 'completed', completed_at = ? WHERE id = ? AND status = 'pending'",
21
+ [ts, row.id]
22
+ );
23
+
24
+ return res.json({ consumed: true, job_id: row.id, data: row.data });
25
+ } catch (e: unknown) {
26
+ return res.json({ error: String(e) }, 500);
27
+ }
28
+ }
@@ -0,0 +1,28 @@
1
+ /** Gallery: Queue — deliberately fail the next pending message. */
2
+ import type { Tina4Request, Tina4Response } from "@tina4/core";
3
+ import { getQueueDb, now } from "../../../../lib/queueDb.js";
4
+
5
+ export default async function (_req: Tina4Request, res: Tina4Response) {
6
+ try {
7
+ const db = await getQueueDb();
8
+ const ts = now();
9
+
10
+ const row = db.fetchOne<Record<string, unknown>>(
11
+ "SELECT * FROM tina4_queue WHERE topic = ? AND status = 'pending' AND available_at <= ? ORDER BY priority DESC, id ASC",
12
+ ["gallery-tasks", ts]
13
+ );
14
+
15
+ if (!row) {
16
+ return res.json({ failed: false, message: "No pending messages to fail" });
17
+ }
18
+
19
+ db.execute(
20
+ "UPDATE tina4_queue SET status = 'failed', error = ?, attempts = attempts + 1 WHERE id = ?",
21
+ ["Deliberately failed via gallery demo", row.id]
22
+ );
23
+
24
+ return res.json({ failed: true, job_id: row.id, data: row.data });
25
+ } catch (e: unknown) {
26
+ return res.json({ error: String(e) }, 500);
27
+ }
28
+ }
@@ -1,16 +1,26 @@
1
- /** Gallery: Queue — produce a background job. */
1
+ /** Gallery: Queue — produce a message into the queue. */
2
2
  import type { Tina4Request, Tina4Response } from "@tina4/core";
3
+ import { getQueueDb, now } from "../../../../lib/queueDb.js";
3
4
 
4
5
  export default async function (req: Tina4Request, res: Tina4Response) {
5
- const body = (req.body as Record<string, unknown>) ?? {};
6
- const task = (body.task as string) ?? "default-task";
7
- const data = body.data ?? {};
6
+ try {
7
+ const body = (req.body as Record<string, unknown>) ?? {};
8
+ const task = (body.task as string) ?? "default-task";
9
+ const data = body.data ?? {};
10
+ const ts = now();
11
+ const payload = JSON.stringify({ task, data });
8
12
 
9
- // In a real app you would use:
10
- // import { Queue, Producer } from "@tina4/core";
11
- // const queue = new Queue(db, "gallery-tasks");
12
- // const producer = new Producer(queue);
13
- // producer.produce({ task, data });
13
+ const db = await getQueueDb();
14
+ db.execute(
15
+ "INSERT INTO tina4_queue (topic, data, status, priority, attempts, available_at, created_at) VALUES (?, ?, 'pending', 0, 0, ?, ?)",
16
+ ["gallery-tasks", payload, ts, ts]
17
+ );
14
18
 
15
- return res.json({ queued: true, task }, 201);
19
+ const row = db.fetchOne<{ last_id: number }>("SELECT last_insert_rowid() as last_id");
20
+ const jobId = row?.last_id ?? 0;
21
+
22
+ return res.json({ queued: true, task, job_id: jobId }, 201);
23
+ } catch (e: unknown) {
24
+ return res.json({ error: String(e) }, 500);
25
+ }
16
26
  }
@@ -0,0 +1,25 @@
1
+ /** Gallery: Queue — retry failed messages (re-queue under max retries). */
2
+ import type { Tina4Request, Tina4Response } from "@tina4/core";
3
+ import { getQueueDb, now, MAX_RETRIES } from "../../../../lib/queueDb.js";
4
+
5
+ export default async function (_req: Tina4Request, res: Tina4Response) {
6
+ try {
7
+ const db = await getQueueDb();
8
+ const ts = now();
9
+
10
+ db.execute(
11
+ "UPDATE tina4_queue SET status = 'pending', available_at = ? WHERE topic = ? AND status = 'failed' AND attempts < ?",
12
+ [ts, "gallery-tasks", MAX_RETRIES]
13
+ );
14
+
15
+ const row = db.fetchOne<{ cnt: number }>(
16
+ "SELECT COUNT(*) as cnt FROM tina4_queue WHERE topic = ? AND status = 'pending'",
17
+ ["gallery-tasks"]
18
+ );
19
+ const retried = row?.cnt ?? 0;
20
+
21
+ return res.json({ retried });
22
+ } catch (e: unknown) {
23
+ return res.json({ error: String(e) }, 500);
24
+ }
25
+ }
@@ -1,10 +1,40 @@
1
- /** Gallery: Queue — check queue status. */
1
+ /** Gallery: Queue — list all messages with statuses. */
2
2
  import type { Tina4Request, Tina4Response } from "@tina4/core";
3
+ import { getQueueDb, MAX_RETRIES } from "../../../../lib/queueDb.js";
3
4
 
4
5
  export default async function (_req: Tina4Request, res: Tina4Response) {
5
- return res.json({
6
- topic: "gallery-tasks",
7
- size: 0,
8
- note: "Connect a queue backend for live data",
9
- });
6
+ try {
7
+ const db = await getQueueDb();
8
+
9
+ const rows = db.fetch<Record<string, unknown>>(
10
+ "SELECT * FROM tina4_queue WHERE topic = ? ORDER BY id DESC",
11
+ ["gallery-tasks"],
12
+ 100
13
+ );
14
+
15
+ const counts: Record<string, number> = { pending: 0, reserved: 0, completed: 0, failed: 0 };
16
+ const messages = (rows ?? []).map((row) => {
17
+ const status = (row.status as string) ?? "pending";
18
+ const attempts = (row.attempts as number) ?? 0;
19
+ let displayStatus = status;
20
+ if (status === "failed" && attempts >= MAX_RETRIES) {
21
+ displayStatus = "dead";
22
+ }
23
+ if (status in counts) {
24
+ counts[status]++;
25
+ }
26
+ return {
27
+ id: row.id,
28
+ data: row.data ?? "",
29
+ status: displayStatus,
30
+ attempts,
31
+ error: row.error ?? "",
32
+ created_at: row.created_at ?? "",
33
+ };
34
+ });
35
+
36
+ return res.json({ topic: "gallery-tasks", messages, counts });
37
+ } catch (e: unknown) {
38
+ return res.json({ error: String(e) }, 500);
39
+ }
10
40
  }