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.
- package/BENCHMARK_REPORT.md +248 -86
- package/CARBONAH.md +4 -4
- package/CLAUDE.md +16 -1
- package/COMPARISON.md +58 -46
- package/README.md +60 -6
- package/package.json +2 -1
- package/packages/cli/src/bin.ts +8 -0
- package/packages/cli/src/commands/generate.ts +237 -0
- package/packages/core/gallery/queue/meta.json +1 -1
- package/packages/core/gallery/queue/src/lib/queueDb.ts +32 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +28 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +28 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +20 -10
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +25 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +36 -6
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +160 -0
- package/packages/core/src/cache.ts +402 -10
- package/packages/core/src/index.ts +5 -2
- package/packages/core/src/messenger.ts +118 -36
- package/packages/core/src/queue.ts +172 -92
- package/packages/core/src/response.ts +46 -0
- package/packages/core/src/router.ts +94 -1
- package/packages/core/src/server.ts +66 -7
- package/packages/core/src/types.ts +20 -1
- package/packages/core/src/websocketConnection.ts +16 -0
- package/packages/frond/src/engine.ts +184 -6
- package/packages/orm/src/baseModel.ts +274 -20
- package/packages/orm/src/cachedDatabase.ts +180 -0
- package/packages/orm/src/index.ts +4 -0
- package/packages/orm/src/model.ts +1 -0
- 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-
|
|
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,
|
|
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** |
|
|
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),
|
|
61
|
+
| **Other** | Service runner, localization (i18n), cache (memory/Redis/file), messenger (.env driven), HTTP constants, health check, configurable error pages |
|
|
62
62
|
|
|
63
|
-
**
|
|
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-
|
|
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.
|
|
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",
|
package/packages/cli/src/bin.ts
CHANGED
|
@@ -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": "/
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
}
|