@nmvuong92/fluxe 0.7.0 → 0.9.1
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/README.md +56 -22
- package/lib/adapters/express.d.ts +3 -0
- package/lib/adapters/express.js +7 -0
- package/lib/adapters/hono.d.ts +3 -0
- package/lib/adapters/hono.js +48 -0
- package/lib/adapters/nest.d.ts +3 -0
- package/lib/adapters/nest.js +9 -0
- package/lib/core/cli.js +1 -1
- package/lib/core/config.d.ts +0 -4
- package/lib/core/config.js +0 -3
- package/lib/core/panel.js +2 -5
- package/lib/core/resolver.d.ts +0 -8
- package/lib/core/resolver.js +1 -19
- package/lib/index.d.ts +1 -7
- package/lib/index.js +2 -7
- package/lib/server_factory.d.ts +5 -2
- package/lib/server_factory.js +14 -11
- package/package.json +51 -12
- package/lib/backends/memory.d.ts +0 -2
- package/lib/backends/memory.js +0 -21
- package/lib/backends/postgres.d.ts +0 -7
- package/lib/backends/postgres.js +0 -28
- package/lib/backends/sqlite.d.ts +0 -2
- package/lib/backends/sqlite.js +0 -29
- package/lib/backends/types.d.ts +0 -11
- package/lib/backends/types.js +0 -9
- package/lib/core/testing.d.ts +0 -9
- package/lib/core/testing.js +0 -38
- package/lib/core/wiring.d.ts +0 -8
- package/lib/core/wiring.js +0 -35
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@ Khung fullstack tối giản — **một runtime TypeScript** (chạy trên `nod
|
|
|
8
8
|
trên triết lý **RCA — Resolved Cell Architecture**: *logic chỉ phụ thuộc HỢP ĐỒNG; mọi quyết định
|
|
9
9
|
vận hành (render, backend data) là **kết quả được GIẢI** bởi engine, không viết tay.*
|
|
10
10
|
|
|
11
|
-
> Đổi backend data `memory ↔ sqlite ↔ postgres
|
|
12
|
-
>
|
|
11
|
+
> Đổi backend data `memory ↔ sqlite ↔ postgres` chỉ một dòng trong `app/backend.ts`, đổi render
|
|
12
|
+
> `static ↔ island` qua profile — **cell & frontend không đổi một dòng**.
|
|
13
13
|
|
|
14
14
|
## Cài & dùng (npm)
|
|
15
15
|
|
|
@@ -18,17 +18,17 @@ npm i @nmvuong92/fluxe react react-dom zod
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
|
-
import { defineCell, withInput, makeServer
|
|
21
|
+
import { defineCell, withInput, makeServer } from "@nmvuong92/fluxe";
|
|
22
22
|
import { useQuery, useMutation, Link, Nav, ThemeToggle } from "@nmvuong92/fluxe/react";
|
|
23
23
|
import { rpc } from "@nmvuong92/fluxe/client";
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
| Import | Nội dung |
|
|
27
27
|
|--------|----------|
|
|
28
|
-
| `@nmvuong92/fluxe` | engine: defineCell, makeServer, resolver, auth, validate,
|
|
28
|
+
| `@nmvuong92/fluxe` | engine: defineCell, makeServer, resolver, auth, validate, seo, broker, ratelimit, codegen… (KHÔNG có driver data — backend là của bạn ở `app/backend.ts`) |
|
|
29
29
|
| `@nmvuong92/fluxe/react` | useQuery, useMutation, Link, Nav, ThemeToggle, useTheme, DebugBar |
|
|
30
30
|
| `@nmvuong92/fluxe/client` | rpc, RpcError, mutate, revalidate, subscribe |
|
|
31
|
-
| `@nmvuong92/fluxe/jobs`
|
|
31
|
+
| `@nmvuong92/fluxe/jobs` | queue/dead-letter (cần `--experimental-sqlite`) |
|
|
32
32
|
|
|
33
33
|
## Chạy nhanh
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ import { rpc } from "@nmvuong92/fluxe/client";
|
|
|
36
36
|
npm install
|
|
37
37
|
npm run fx -- build # resolve + prerender + bundle (1 schema → types TS qua fx gen)
|
|
38
38
|
npm run fx -- dev # http://localhost:5180
|
|
39
|
-
npm run test:all # typecheck +
|
|
39
|
+
npm run test:all # typecheck + 144 unit + integration (selftest2) — TẤT CẢ XANH
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
`fx`: `gen · resolve · prerender · build · dev · test · jobs`.
|
|
@@ -47,9 +47,12 @@ npm run test:all # typecheck + 107 unit + 28 integration — TẤT C
|
|
|
47
47
|
app/ ← DEV sở hữu (sửa thoải mái) — Contract Plane
|
|
48
48
|
cells/ trang/feature (route + loader + view + action/head/layout/guard)
|
|
49
49
|
layouts/ layout dùng chung (nested)
|
|
50
|
-
|
|
50
|
+
server.ts server entry — Express/Hono/Nest mount fluxe (mặc định Express)
|
|
51
|
+
backend.ts TẦNG DATA của bạn: interface domain + chọn driver (memory/sqlite/postgres)
|
|
52
|
+
profiles.ts profile resolve render mode (static/island) per môi trường
|
|
51
53
|
contract.ts schema → codegen types TS
|
|
52
54
|
env.ts env có kiểu, validate fail-fast lúc boot
|
|
55
|
+
app.ts registry cell — sinh tự động (fx sync), đừng sửa
|
|
53
56
|
|
|
54
57
|
src/ ← ENGINE (không đụng) — Resolution Plane
|
|
55
58
|
core/ resolver · router · errors · auth · validate · codegen · layouts ·
|
|
@@ -57,15 +60,16 @@ src/ ← ENGINE (không đụng) — Resolution Plane
|
|
|
57
60
|
server_factory.ts runtime ráp cell + giải manifest
|
|
58
61
|
```
|
|
59
62
|
|
|
60
|
-
**Quy tắc:**
|
|
61
|
-
|
|
63
|
+
**Quy tắc:** backend là **tầng data của bạn** ở `app/backend.ts` (interface domain + chọn driver),
|
|
64
|
+
inject qua `makeServer(…, { backend })`. Engine không bao giờ import ngược vào `app/`.
|
|
62
65
|
|
|
63
66
|
## Tính năng (tất cả TDD + chạy thật)
|
|
64
67
|
|
|
68
|
+
- **Server** — chạy zero-config (`makeServer`, node:http) HOẶC nhúng vào **Express/Hono/Nest** qua adapter (`@nmvuong92/fluxe/express|hono|nest`)
|
|
65
69
|
- **Render** — static (0 JS) · island hydrate · SPA nav (Inertia) · static-prerender · API mode `?json=1`
|
|
66
70
|
- **Routing** — động `[param]` → `ctx.input` · **nested layouts** · SEO (head/canonical/OG/JSON-LD per cell, `/sitemap.xml`, `/robots.txt`)
|
|
67
71
|
- **Bảo mật (đầy đủ)** — input validation (Zod) · auth password **scrypt** · **RBAC** · **CSRF** double-submit · **rate-limit** token-bucket · error handling không-leak + structured
|
|
68
|
-
- **Data** — backend
|
|
72
|
+
- **Data** — backend **user-owned** ở `app/backend.ts` (bạn tự định nghĩa interface + implement bằng `node:sqlite`/`pg`/ORM trực tiếp), inject qua `makeServer(…, { backend })` · engine 0 driver · **codegen contract** 1 schema → types TS
|
|
69
73
|
- **Mutations DX** — `RpcError` có cấu trúc · `mutate()` optimistic + rollback · lỗi validation field-level
|
|
70
74
|
- **Realtime (Trục 4g)** — **SSE channel** + pub/sub broker · live-update on action · **presence** (multi-tab)
|
|
71
75
|
- **Async** — **job queue bền** (SQLite, retry → dead-letter)
|
|
@@ -73,24 +77,54 @@ folder. Engine không bao giờ import ngược vào `app/`.
|
|
|
73
77
|
- **Config** — env có kiểu, fail-fast lúc boot
|
|
74
78
|
- **DX** — `fx` CLI · mock `Backend` test cực dễ · typecheck gate
|
|
75
79
|
|
|
76
|
-
## Backend data (
|
|
80
|
+
## Backend data (user-owned, TS in-process)
|
|
77
81
|
|
|
78
|
-
Backend
|
|
82
|
+
Backend là **tầng data của bạn** — định nghĩa interface domain ở `app/backend.ts` + chọn driver
|
|
83
|
+
**TS in-process** dưới đây, inject qua `makeServer(…, { backend })`. Cell chỉ thấy interface:
|
|
79
84
|
|
|
80
|
-
| Driver |
|
|
81
|
-
|
|
82
|
-
| `memory` | in-
|
|
83
|
-
| `sqlite` | `node:sqlite` built-in, 0 dep, persist ra file (
|
|
84
|
-
| `postgres` |
|
|
85
|
+
| Driver | Bạn tự implement bằng |
|
|
86
|
+
|--------|------------------------|
|
|
87
|
+
| `memory` | object in-RAM — mặc định dev |
|
|
88
|
+
| `sqlite` | `node:sqlite` built-in (`DatabaseSync`), 0 dep, persist ra file (cần `--experimental-sqlite`) |
|
|
89
|
+
| `postgres` | `npm i pg`, dùng `Pool`/`Client` trực tiếp (`DATABASE_URL`) |
|
|
85
90
|
|
|
86
91
|
```ts
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
// app/backend.ts — engine không biết gì
|
|
93
|
+
import { DatabaseSync } from "node:sqlite";
|
|
94
|
+
|
|
95
|
+
export interface Todo { id: string; title: string; done: boolean }
|
|
96
|
+
export interface Backend {
|
|
97
|
+
name: string;
|
|
98
|
+
listTodos(): Promise<Todo[]>;
|
|
99
|
+
addTodo(title: string): Promise<Todo>;
|
|
100
|
+
toggleTodo(id: string): Promise<Todo[]>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function memoryBackend(): Backend {
|
|
104
|
+
let items: Todo[] = [];
|
|
105
|
+
let seq = 0;
|
|
106
|
+
return {
|
|
107
|
+
name: "memory",
|
|
108
|
+
async listTodos() { return items; },
|
|
109
|
+
async addTodo(title) { const t = { id: String(++seq), title, done: false }; items.push(t); return t; },
|
|
110
|
+
async toggleTodo(id) { items = items.map((t) => t.id === id ? { ...t, done: !t.done } : t); return items; },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function sqliteBackend(path = ":memory:"): Backend {
|
|
115
|
+
const db = new DatabaseSync(path);
|
|
116
|
+
db.exec(`CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, done INTEGER DEFAULT 0)`);
|
|
117
|
+
// … CRUD bằng db.prepare(...)
|
|
118
|
+
return { name: "sqlite" } as any;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const backend: Backend = process.env.FLUXE_SQLITE_PATH
|
|
122
|
+
? sqliteBackend(process.env.FLUXE_SQLITE_PATH) // đổi 1 dòng = đổi nơi lưu
|
|
123
|
+
: memoryBackend();
|
|
92
124
|
```
|
|
93
125
|
|
|
126
|
+
Nguồn khác (REST/ORM/Redis)? Tự implement `Backend` là xong — engine không quan tâm bên dưới.
|
|
127
|
+
|
|
94
128
|
## Triết lý
|
|
95
129
|
|
|
96
130
|
Toàn bộ định hướng, các trục chiến lược, tenets và nguyên lý RCA: xem **[idea.md](idea.md)**.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createHandler } from "../server_factory.js";
|
|
2
|
+
/* Mount fluxe như middleware (đặt SAU các route riêng của bạn — fluxe là catch-all):
|
|
3
|
+
* app.use(fluxe(manifest, cells, layouts, { backend })); */
|
|
4
|
+
export function fluxe(...args) {
|
|
5
|
+
const handler = createHandler(...args);
|
|
6
|
+
return (req, res, next) => { handler(req, res).catch(next); };
|
|
7
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Copyright (c) 2026 nmvuong92
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/* Adapter Hono (chạy trên Node qua @hono/node-server). `hono` + `@hono/node-server` là peerDependency.
|
|
4
|
+
* Hono yêu cầu handler trả về Web `Response`; còn createHandler ghi vào node res. Ta dùng một
|
|
5
|
+
* "capture shim": chạy handler vào res đệm (buffer) rồi dựng Response chuẩn → Hono ghi sạch,
|
|
6
|
+
* không đụng nội bộ node-server. (Đánh đổi: buffer toàn response thay vì stream — chấp nhận được
|
|
7
|
+
* cho lớp adapter; muốn stream thì dùng makeServer/Express.) */
|
|
8
|
+
import { Writable } from "node:stream";
|
|
9
|
+
import { createHandler } from "../server_factory.js";
|
|
10
|
+
class CaptureRes extends Writable {
|
|
11
|
+
statusCode = 200;
|
|
12
|
+
headersSent = false;
|
|
13
|
+
headers = {};
|
|
14
|
+
chunks = [];
|
|
15
|
+
writeHead(status, headers) {
|
|
16
|
+
this.statusCode = status;
|
|
17
|
+
if (headers)
|
|
18
|
+
for (const k of Object.keys(headers))
|
|
19
|
+
this.headers[k.toLowerCase()] = headers[k];
|
|
20
|
+
this.headersSent = true;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
setHeader(k, v) { this.headers[k.toLowerCase()] = v; return this; }
|
|
24
|
+
getHeader(k) { return this.headers[k.toLowerCase()]; }
|
|
25
|
+
_write(chunk, _enc, cb) { this.chunks.push(Buffer.from(chunk)); cb(); }
|
|
26
|
+
}
|
|
27
|
+
/* Mount fluxe như catch-all (đặt SAU route Hono riêng của bạn):
|
|
28
|
+
* app.use("*", fluxe(manifest, cells, layouts, { backend }));
|
|
29
|
+
* serve({ fetch: app.fetch, port: 5180 }); // từ @hono/node-server */
|
|
30
|
+
export function fluxe(...args) {
|
|
31
|
+
const handler = createHandler(...args);
|
|
32
|
+
return async (c) => {
|
|
33
|
+
const { incoming } = c.env;
|
|
34
|
+
const res = new CaptureRes();
|
|
35
|
+
const finished = new Promise((resolve) => res.on("finish", resolve));
|
|
36
|
+
await handler(incoming, res);
|
|
37
|
+
await finished;
|
|
38
|
+
const headers = new Headers();
|
|
39
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
40
|
+
if (Array.isArray(v))
|
|
41
|
+
v.forEach((x) => headers.append(k, String(x)));
|
|
42
|
+
else
|
|
43
|
+
headers.set(k, String(v));
|
|
44
|
+
}
|
|
45
|
+
const body = Buffer.concat(res.chunks);
|
|
46
|
+
return new Response(body.length ? body : null, { status: res.statusCode, headers });
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHandler } from "../server_factory.js";
|
|
2
|
+
/* Functional middleware — mount global (khuyên dùng, catch-all đặt sau route Nest):
|
|
3
|
+
* const app = await NestFactory.create(AppModule);
|
|
4
|
+
* app.use(fluxeMiddleware(manifest, cells, layouts, { backend }));
|
|
5
|
+
* (hoặc consumer.apply(...).forRoutes("{*splat}") trong module — Nest 11 dùng wildcard mới). */
|
|
6
|
+
export function fluxeMiddleware(...args) {
|
|
7
|
+
const handler = createHandler(...args);
|
|
8
|
+
return (req, res, next) => { handler(req, res).catch(next); };
|
|
9
|
+
}
|
package/lib/core/cli.js
CHANGED
|
@@ -47,7 +47,7 @@ export const COMMANDS = {
|
|
|
47
47
|
},
|
|
48
48
|
dev: {
|
|
49
49
|
desc: "Sync + resolve + build client + chạy server",
|
|
50
|
-
shell: (a) => `${SYNC} && tsx scripts/resolve.ts ${p(a)} && ${ESBUILD} && tsx
|
|
50
|
+
shell: (a) => `${SYNC} && tsx scripts/resolve.ts ${p(a)} && ${ESBUILD} && tsx app/server.ts`,
|
|
51
51
|
},
|
|
52
52
|
test: {
|
|
53
53
|
desc: "Sync + typecheck + unit + integration",
|
package/lib/core/config.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ declare const Schema: z.ZodObject<{
|
|
|
3
3
|
env: z.ZodEnum<["development", "production", "test"]>;
|
|
4
4
|
secret: z.ZodString;
|
|
5
5
|
port: z.ZodNumber;
|
|
6
|
-
defaultBackend: z.ZodEnum<["memory", "sqlite", "postgres"]>;
|
|
7
6
|
rateLimit: z.ZodObject<{
|
|
8
7
|
capacity: z.ZodNumber;
|
|
9
8
|
refillPerSec: z.ZodNumber;
|
|
@@ -42,7 +41,6 @@ declare const Schema: z.ZodObject<{
|
|
|
42
41
|
env: "development" | "production" | "test";
|
|
43
42
|
secret: string;
|
|
44
43
|
port: number;
|
|
45
|
-
defaultBackend: "memory" | "sqlite" | "postgres";
|
|
46
44
|
rateLimit: {
|
|
47
45
|
capacity: number;
|
|
48
46
|
refillPerSec: number;
|
|
@@ -61,7 +59,6 @@ declare const Schema: z.ZodObject<{
|
|
|
61
59
|
env: "development" | "production" | "test";
|
|
62
60
|
secret: string;
|
|
63
61
|
port: number;
|
|
64
|
-
defaultBackend: "memory" | "sqlite" | "postgres";
|
|
65
62
|
rateLimit: {
|
|
66
63
|
capacity: number;
|
|
67
64
|
refillPerSec: number;
|
|
@@ -83,7 +80,6 @@ export declare const ENV_KEYS: {
|
|
|
83
80
|
readonly NODE_ENV: "env";
|
|
84
81
|
readonly FLUXE_SECRET: "secret";
|
|
85
82
|
readonly "PORT (ho\u1EB7c FLUXE_PORT)": "port";
|
|
86
|
-
readonly FLUXE_BACKEND: "defaultBackend";
|
|
87
83
|
readonly FLUXE_RATELIMIT_CAPACITY: "rateLimit.capacity";
|
|
88
84
|
readonly FLUXE_RATELIMIT_REFILL: "rateLimit.refillPerSec";
|
|
89
85
|
readonly FLUXE_RATELIMIT_MAX_KEYS: "rateLimit.maxKeys";
|
package/lib/core/config.js
CHANGED
|
@@ -9,7 +9,6 @@ const Schema = z.object({
|
|
|
9
9
|
env: z.enum(["development", "production", "test"]),
|
|
10
10
|
secret: z.string().min(8),
|
|
11
11
|
port: z.coerce.number().int().positive(),
|
|
12
|
-
defaultBackend: z.enum(["memory", "sqlite", "postgres"]),
|
|
13
12
|
rateLimit: z.object({
|
|
14
13
|
capacity: z.coerce.number().int().positive(),
|
|
15
14
|
refillPerSec: z.coerce.number().positive(),
|
|
@@ -24,7 +23,6 @@ export const ENV_KEYS = {
|
|
|
24
23
|
"NODE_ENV": "env",
|
|
25
24
|
"FLUXE_SECRET": "secret",
|
|
26
25
|
"PORT (hoặc FLUXE_PORT)": "port",
|
|
27
|
-
"FLUXE_BACKEND": "defaultBackend",
|
|
28
26
|
"FLUXE_RATELIMIT_CAPACITY": "rateLimit.capacity",
|
|
29
27
|
"FLUXE_RATELIMIT_REFILL": "rateLimit.refillPerSec",
|
|
30
28
|
"FLUXE_RATELIMIT_MAX_KEYS": "rateLimit.maxKeys",
|
|
@@ -52,7 +50,6 @@ export function loadConfig(source = process.env, overrides = {}) {
|
|
|
52
50
|
env: source.NODE_ENV || "development",
|
|
53
51
|
secret: source.FLUXE_SECRET || "dev-secret-change-me",
|
|
54
52
|
port: num(source, "PORT", num(source, "FLUXE_PORT", 5180)),
|
|
55
|
-
defaultBackend: source.FLUXE_BACKEND || "memory",
|
|
56
53
|
rateLimit: {
|
|
57
54
|
capacity: num(source, "FLUXE_RATELIMIT_CAPACITY", 30),
|
|
58
55
|
refillPerSec: num(source, "FLUXE_RATELIMIT_REFILL", 10),
|
package/lib/core/panel.js
CHANGED
|
@@ -8,7 +8,6 @@ export function renderResolutionPanel(m, requests = []) {
|
|
|
8
8
|
<td><code>${c.route}</code></td>
|
|
9
9
|
<td>${c.render.mode}</td>
|
|
10
10
|
<td>${c.render.shipClientJs ? "✓ JS" : "0 JS"}</td>
|
|
11
|
-
<td><span class="badge ${c.backend.language}">${c.backend.language}</span></td>
|
|
12
11
|
</tr>`).join("");
|
|
13
12
|
return `<!doctype html><html lang="vi"><head><meta charset="utf-8"><title>fluxe — RCA Resolution</title>
|
|
14
13
|
<style>
|
|
@@ -19,14 +18,12 @@ export function renderResolutionPanel(m, requests = []) {
|
|
|
19
18
|
th,td{text-align:left;padding:.5rem .75rem;border-bottom:1px solid #2a2a40}
|
|
20
19
|
th{color:#8a8aa0;font-weight:600;font-size:.78rem;text-transform:uppercase;letter-spacing:.04em}
|
|
21
20
|
code{background:#1a1a2e;padding:.1rem .35rem;border-radius:4px;color:#a0a0ff}
|
|
22
|
-
.badge{display:inline-block;padding:.1rem .55rem;border-radius:99px;font-size:.78rem}
|
|
23
|
-
.memory{background:#7c7cff33;color:#a0a0ff}.sqlite{background:#00add833;color:#5fd3f0}.postgres{background:#dea58433;color:#e8b48f}
|
|
24
21
|
</style></head>
|
|
25
22
|
<body>
|
|
26
23
|
<h1>RCA Resolution</h1>
|
|
27
|
-
<div class="sub">profile <b>${m.profile}</b> ·
|
|
24
|
+
<div class="sub">profile <b>${m.profile}</b> · data = backend user-owned (app/backend.ts)</div>
|
|
28
25
|
<table>
|
|
29
|
-
<thead><tr><th>cell</th><th>route</th><th>render</th><th>JS</th
|
|
26
|
+
<thead><tr><th>cell</th><th>route</th><th>render</th><th>JS</th></tr></thead>
|
|
30
27
|
<tbody>${rows}
|
|
31
28
|
</tbody>
|
|
32
29
|
</table>
|
package/lib/core/resolver.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export type RenderMode = "static" | "island";
|
|
2
|
-
export type BackendKind = "memory" | "sqlite" | "postgres";
|
|
3
2
|
export interface CellDecl {
|
|
4
3
|
id: string;
|
|
5
4
|
route: string;
|
|
@@ -7,11 +6,6 @@ export interface CellDecl {
|
|
|
7
6
|
}
|
|
8
7
|
export interface ResolutionProfile {
|
|
9
8
|
name: string;
|
|
10
|
-
backend: BackendKind;
|
|
11
|
-
cellBackends?: Record<string, BackendKind>;
|
|
12
|
-
}
|
|
13
|
-
export interface BackendResolution {
|
|
14
|
-
language: BackendKind;
|
|
15
9
|
}
|
|
16
10
|
export interface CellResolution {
|
|
17
11
|
id: string;
|
|
@@ -20,12 +14,10 @@ export interface CellResolution {
|
|
|
20
14
|
mode: RenderMode;
|
|
21
15
|
shipClientJs: boolean;
|
|
22
16
|
};
|
|
23
|
-
backend: BackendResolution;
|
|
24
17
|
}
|
|
25
18
|
export interface ResolutionManifest {
|
|
26
19
|
version: 1;
|
|
27
20
|
profile: string;
|
|
28
|
-
backend: BackendResolution;
|
|
29
21
|
cells: Record<string, CellResolution>;
|
|
30
22
|
}
|
|
31
23
|
export declare function resolve(cells: CellDecl[], profile: ResolutionProfile): ResolutionManifest;
|
package/lib/core/resolver.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
const ALLOWED = ["memory", "sqlite", "postgres"];
|
|
2
|
-
// Giải một BackendKind → BackendResolution. Mọi driver TS đều in-process (0 roundtrip).
|
|
3
|
-
// Dùng chung cho default app-level lẫn override per-cell.
|
|
4
|
-
function resolveBackend(kind, profile) {
|
|
5
|
-
if (!ALLOWED.includes(kind)) {
|
|
6
|
-
throw new Error(`profile "${profile.name}": backend không hợp lệ: ${kind}`);
|
|
7
|
-
}
|
|
8
|
-
return { language: kind };
|
|
9
|
-
}
|
|
10
1
|
export function resolve(cells, profile) {
|
|
11
|
-
const ids = new Set(cells.map((c) => c.id));
|
|
12
|
-
for (const id of Object.keys(profile.cellBackends ?? {})) {
|
|
13
|
-
if (!ids.has(id)) {
|
|
14
|
-
throw new Error(`profile "${profile.name}": cellBackends trỏ cell không tồn tại: ${id}`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
const backend = resolveBackend(profile.backend, profile); // default app-level
|
|
18
2
|
const out = {};
|
|
19
3
|
const seenRoutes = new Set();
|
|
20
4
|
for (const c of cells) {
|
|
@@ -23,14 +7,12 @@ export function resolve(cells, profile) {
|
|
|
23
7
|
if (seenRoutes.has(c.route))
|
|
24
8
|
throw new Error(`route trùng: ${c.route}`);
|
|
25
9
|
seenRoutes.add(c.route);
|
|
26
|
-
const kind = profile.cellBackends?.[c.id] ?? profile.backend;
|
|
27
10
|
const mode = c.hydration ?? "island"; // default island
|
|
28
11
|
out[c.id] = {
|
|
29
12
|
id: c.id,
|
|
30
13
|
route: c.route,
|
|
31
14
|
render: { mode, shipClientJs: mode === "island" },
|
|
32
|
-
backend: resolveBackend(kind, profile),
|
|
33
15
|
};
|
|
34
16
|
}
|
|
35
|
-
return { version: 1, profile: profile.name,
|
|
17
|
+
return { version: 1, profile: profile.name, cells: out };
|
|
36
18
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ export * from "./core/engine.ts";
|
|
|
2
2
|
export * from "./core/validate.ts";
|
|
3
3
|
export * from "./core/errors.ts";
|
|
4
4
|
export * from "./core/resolver.ts";
|
|
5
|
-
export * from "./core/wiring.ts";
|
|
6
5
|
export * from "./core/auth.ts";
|
|
7
6
|
export * from "./core/env.ts";
|
|
8
7
|
export * from "./core/config.ts";
|
|
@@ -15,13 +14,8 @@ export * from "./core/ratelimit.ts";
|
|
|
15
14
|
export * from "./core/codegen.ts";
|
|
16
15
|
export * from "./core/layouts.ts";
|
|
17
16
|
export * from "./core/router.ts";
|
|
18
|
-
export * from "./core/testing.ts";
|
|
19
|
-
export * from "./backends/types.ts";
|
|
20
17
|
export * from "./storage/types.ts";
|
|
21
18
|
export { createMemoryStorage } from "./storage/memory.ts";
|
|
22
19
|
export { createLocalStorage } from "./storage/local.ts";
|
|
23
20
|
export { createS3Storage } from "./storage/s3.ts";
|
|
24
|
-
export {
|
|
25
|
-
export { createSqliteBackend } from "./backends/sqlite.ts";
|
|
26
|
-
export { createPostgresBackend } from "./backends/postgres.ts";
|
|
27
|
-
export { makeServer } from "./server_factory.ts";
|
|
21
|
+
export { makeServer, createHandler, type NodeHandler, type MakeServerOpts } from "./server_factory.ts";
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,6 @@ export * from "./core/engine.js"; // defineCell, Ctx, CellDef, Loader, Action, H
|
|
|
6
6
|
export * from "./core/validate.js"; // validateInput, withInput
|
|
7
7
|
export * from "./core/errors.js"; // FluxeError, ErrorPayload, toErrorPayload, renderErrorPage
|
|
8
8
|
export * from "./core/resolver.js"; // resolve, ResolutionProfile/Manifest, CellDecl, RenderMode…
|
|
9
|
-
export * from "./core/wiring.js"; // backendFromManifest, backendsFromManifest
|
|
10
9
|
export * from "./core/auth.js"; // session HMAC, scrypt password, CSRF, RBAC
|
|
11
10
|
export * from "./core/env.js"; // loadEnv
|
|
12
11
|
export * from "./core/config.js"; // FluxeConfig, loadConfig (default ← ENV FLUXE_* ← override)
|
|
@@ -19,13 +18,9 @@ export * from "./core/ratelimit.js"; // token-bucket + LRU
|
|
|
19
18
|
export * from "./core/codegen.js"; // genTS
|
|
20
19
|
export * from "./core/layouts.js"; // layoutChain, LayoutMeta
|
|
21
20
|
export * from "./core/router.js"; // makeRouter
|
|
22
|
-
export * from "./core/testing.js"; // createTestBackend
|
|
23
|
-
export * from "./backends/types.js"; // Backend, Todo
|
|
24
21
|
export * from "./storage/types.js"; // Storage, PutResult, GetResult, safeKey, makeKey
|
|
25
22
|
export { createMemoryStorage } from "./storage/memory.js";
|
|
26
23
|
export { createLocalStorage } from "./storage/local.js";
|
|
27
24
|
export { createS3Storage } from "./storage/s3.js"; // adapter tham chiếu (cần @aws-sdk/client-s3)
|
|
28
|
-
export {
|
|
29
|
-
|
|
30
|
-
export { createPostgresBackend } from "./backends/postgres.js"; // user tự inject client `pg`
|
|
31
|
-
export { makeServer } from "./server_factory.js";
|
|
25
|
+
export { makeServer, createHandler } from "./server_factory.js";
|
|
26
|
+
// Backend = USER-OWNED (app/backend.ts) — engine KHÔNG ship driver/domain data nào.
|
package/lib/server_factory.d.ts
CHANGED
|
@@ -11,10 +11,13 @@ type LayoutMap = Record<string, LayoutEntry>;
|
|
|
11
11
|
import { type I18n } from "./core/i18n.ts";
|
|
12
12
|
import { type Storage } from "./storage/types.ts";
|
|
13
13
|
import { type FluxeConfig } from "./core/config.ts";
|
|
14
|
-
export
|
|
14
|
+
export interface MakeServerOpts {
|
|
15
15
|
i18n?: I18n;
|
|
16
16
|
storage?: Storage;
|
|
17
17
|
config?: FluxeConfig;
|
|
18
18
|
backend?: unknown;
|
|
19
|
-
}
|
|
19
|
+
}
|
|
20
|
+
export type NodeHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<unknown>;
|
|
21
|
+
export declare function createHandler(manifest: ResolutionManifest, cells: CellDef<any, any>[], layouts?: LayoutMap, opts?: MakeServerOpts): NodeHandler;
|
|
22
|
+
export declare function makeServer(manifest: ResolutionManifest, cells: CellDef<any, any>[], layouts?: LayoutMap, opts?: MakeServerOpts): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
20
23
|
export {};
|
package/lib/server_factory.js
CHANGED
|
@@ -5,7 +5,6 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
5
5
|
import { PassThrough } from "node:stream";
|
|
6
6
|
import { createElement as h } from "react";
|
|
7
7
|
import { renderToPipeableStream } from "react-dom/server";
|
|
8
|
-
import { backendsFromManifest } from "./core/wiring.js";
|
|
9
8
|
import { renderResolutionPanel } from "./core/panel.js";
|
|
10
9
|
import { makeRouter } from "./core/router.js";
|
|
11
10
|
import { layoutChain } from "./core/layouts.js";
|
|
@@ -75,7 +74,9 @@ function renderBodyToString(node) {
|
|
|
75
74
|
});
|
|
76
75
|
});
|
|
77
76
|
}
|
|
78
|
-
|
|
77
|
+
/* createHandler — lõi request framework-agnostic: trả về handler Node (req,res).
|
|
78
|
+
* Dùng trực tiếp cho adapter Express/Hono/Nest; makeServer chỉ bọc bằng http.createServer. */
|
|
79
|
+
export function createHandler(manifest, cells, layouts = {}, opts = {}) {
|
|
79
80
|
const i18n = opts.i18n;
|
|
80
81
|
const storage = opts.storage;
|
|
81
82
|
const config = opts.config ?? loadConfig(); // default ← ENV (FLUXE_*) ← override
|
|
@@ -99,10 +100,8 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
99
100
|
const matchRoute = makeRouter(cells);
|
|
100
101
|
const byId = new Map(cells.map((c) => [c.id, c]));
|
|
101
102
|
// Backend USER-OWNED (app/backend.ts) inject qua opts.backend → dùng cho mọi cell.
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
const backends = userBackend === undefined ? backendsFromManifest(manifest) : null;
|
|
105
|
-
const backendFor = (id) => userBackend ?? backends.byCell.get(id) ?? backends.default;
|
|
103
|
+
// Engine KHÔNG ship/giải backend; cell nào dùng backend mà app quên truyền → undefined (lỗi rõ ràng).
|
|
104
|
+
const backendFor = (_id) => opts.backend;
|
|
106
105
|
// Resolved Container: service realtime đăng ký LƯỜI — chỉ tạo khi thật sự dùng (SSE/action).
|
|
107
106
|
// App không realtime → broker/presence KHÔNG bao giờ bootstrap. resolved() ở /_fluxe/stats.
|
|
108
107
|
const container = createContainer();
|
|
@@ -112,7 +111,7 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
112
111
|
const recorder = createRecorder(); // request log — chạy mỗi request → eager (luôn dùng)
|
|
113
112
|
const renderCache = createRenderCache({ maxKeys: config.renderCache.maxKeys }); // FLUXE_RENDERCACHE_MAX_KEYS
|
|
114
113
|
let clientJs; // ý A: đọc dist/client.js 1 lần (zero-copy: tái dùng buffer)
|
|
115
|
-
return
|
|
114
|
+
return async (req, res) => {
|
|
116
115
|
const url = new URL(req.url, "http://localhost");
|
|
117
116
|
const start = Date.now();
|
|
118
117
|
res.on("finish", () => recorder.record({ method: req.method ?? "?", path: url.pathname, status: res.statusCode, ms: Date.now() - start, ts: start }));
|
|
@@ -272,10 +271,10 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
272
271
|
return res.end("no action");
|
|
273
272
|
}
|
|
274
273
|
const t0 = Date.now();
|
|
275
|
-
// DevTools (DEV): #3 resolution (
|
|
274
|
+
// DevTools (DEV): #3 resolution (render mode của cell; data = backend user-owned).
|
|
276
275
|
const backend = backendFor(cellId);
|
|
277
|
-
const
|
|
278
|
-
const resolution =
|
|
276
|
+
const render = manifest.cells[cellId]?.render;
|
|
277
|
+
const resolution = render ? `${render.mode}/${backend?.name ?? "no-backend"}` : "island";
|
|
279
278
|
// #1 Chaos (DEV): inject delay + lỗi giả lập để test UX.
|
|
280
279
|
if (DEV && req.headers["x-fluxe-chaos"]) {
|
|
281
280
|
const c = parseChaos(String(req.headers["x-fluxe-chaos"]));
|
|
@@ -354,5 +353,9 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
354
353
|
// Action (rpc) luôn nhận lỗi dạng JSON.
|
|
355
354
|
sendError(res, wantsJson || url.pathname.startsWith("/__action/"), err);
|
|
356
355
|
}
|
|
357
|
-
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/* makeServer — đường zero-config: bọc createHandler bằng http.createServer (giữ API cũ). */
|
|
359
|
+
export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
360
|
+
return http.createServer(createHandler(manifest, cells, layouts, opts));
|
|
358
361
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nmvuong92/fluxe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "fluxe — khung fullstack tối giản, một runtime TS (RCA: Resolved Cell Architecture).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "nmvuong92",
|
|
@@ -30,9 +30,17 @@
|
|
|
30
30
|
"types": "./lib/core/jobs.d.ts",
|
|
31
31
|
"default": "./lib/core/jobs.js"
|
|
32
32
|
},
|
|
33
|
-
"./
|
|
34
|
-
"types": "./lib/
|
|
35
|
-
"default": "./lib/
|
|
33
|
+
"./express": {
|
|
34
|
+
"types": "./lib/adapters/express.d.ts",
|
|
35
|
+
"default": "./lib/adapters/express.js"
|
|
36
|
+
},
|
|
37
|
+
"./hono": {
|
|
38
|
+
"types": "./lib/adapters/hono.d.ts",
|
|
39
|
+
"default": "./lib/adapters/hono.js"
|
|
40
|
+
},
|
|
41
|
+
"./nest": {
|
|
42
|
+
"types": "./lib/adapters/nest.d.ts",
|
|
43
|
+
"default": "./lib/adapters/nest.js"
|
|
36
44
|
}
|
|
37
45
|
},
|
|
38
46
|
"files": [
|
|
@@ -48,8 +56,8 @@
|
|
|
48
56
|
"typecheck": "tsc --noEmit",
|
|
49
57
|
"sync": "tsx scripts/sync.ts",
|
|
50
58
|
"build:client": "npm run sync && esbuild src/client.tsx --bundle --format=esm --outfile=dist/client.js --jsx=automatic --loader:.tsx=tsx",
|
|
51
|
-
"dev": "npm run build:client && tsx
|
|
52
|
-
"dev:
|
|
59
|
+
"dev": "npm run build:client && tsx app/server.ts",
|
|
60
|
+
"dev:node": "npm run build:client && tsx src/server.tsx",
|
|
53
61
|
"test": "tsx src/selftest2.ts",
|
|
54
62
|
"test:unit": "node --experimental-sqlite --import tsx --test '{src,app}/**/*.test.ts'",
|
|
55
63
|
"test:cells": "node --experimental-sqlite --import tsx --test 'app/cells/**/*.test.ts'",
|
|
@@ -67,16 +75,47 @@
|
|
|
67
75
|
},
|
|
68
76
|
"peerDependencies": {
|
|
69
77
|
"react": "^18 || ^19",
|
|
70
|
-
"react-dom": "^18 || ^19"
|
|
78
|
+
"react-dom": "^18 || ^19",
|
|
79
|
+
"express": "^4 || ^5",
|
|
80
|
+
"hono": "^4",
|
|
81
|
+
"@hono/node-server": "^1 || ^2",
|
|
82
|
+
"@nestjs/common": "^10 || ^11",
|
|
83
|
+
"@nestjs/core": "^10 || ^11"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"express": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"hono": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"@hono/node-server": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"@nestjs/common": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"@nestjs/core": {
|
|
99
|
+
"optional": true
|
|
100
|
+
}
|
|
71
101
|
},
|
|
72
102
|
"devDependencies": {
|
|
73
|
-
"
|
|
74
|
-
"
|
|
103
|
+
"@hono/node-server": "^2.0.6",
|
|
104
|
+
"@nestjs/common": "^11.1.27",
|
|
105
|
+
"@nestjs/core": "^11.1.27",
|
|
106
|
+
"@nestjs/platform-express": "^11.1.27",
|
|
107
|
+
"@types/express": "^5.0.6",
|
|
108
|
+
"@types/node": "^20",
|
|
109
|
+
"@types/react": "^18",
|
|
110
|
+
"@types/react-dom": "^18",
|
|
75
111
|
"esbuild": "^0.23",
|
|
112
|
+
"express": "^5.2.1",
|
|
113
|
+
"hono": "^4.12.27",
|
|
76
114
|
"react": "^18",
|
|
77
115
|
"react-dom": "^18",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
116
|
+
"reflect-metadata": "^0.2.2",
|
|
117
|
+
"rxjs": "^7.8.2",
|
|
118
|
+
"tsx": "^4",
|
|
119
|
+
"typescript": "^5"
|
|
81
120
|
}
|
|
82
121
|
}
|
package/lib/backends/memory.d.ts
DELETED
package/lib/backends/memory.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/* Backend #1: in-memory. Đại diện cho "TS thuần". */
|
|
2
|
-
export function createMemoryBackend() {
|
|
3
|
-
let todos = [
|
|
4
|
-
{ id: "1", title: "Học kiến trúc fullstack", done: true },
|
|
5
|
-
{ id: "2", title: "Dựng PoC switch backend", done: false },
|
|
6
|
-
];
|
|
7
|
-
let seq = 3;
|
|
8
|
-
return {
|
|
9
|
-
name: "memory",
|
|
10
|
-
async listTodos() { return todos; },
|
|
11
|
-
async addTodo(title) {
|
|
12
|
-
const t = { id: String(seq++), title, done: false };
|
|
13
|
-
todos = [...todos, t];
|
|
14
|
-
return t;
|
|
15
|
-
},
|
|
16
|
-
async toggleTodo(id) {
|
|
17
|
-
todos = todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
|
|
18
|
-
return todos;
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
}
|
package/lib/backends/postgres.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Nhận sẵn client (đã connect) để testable + không buộc import pg ở repo này.
|
|
2
|
-
export function createPostgresBackend(client) {
|
|
3
|
-
const toTodo = (r) => ({ id: String(r.id), title: r.title, done: !!r.done });
|
|
4
|
-
return {
|
|
5
|
-
name: "postgres",
|
|
6
|
-
async listTodos() {
|
|
7
|
-
const { rows } = await client.query("SELECT * FROM todos ORDER BY id");
|
|
8
|
-
return rows.map(toTodo);
|
|
9
|
-
},
|
|
10
|
-
async addTodo(title) {
|
|
11
|
-
const { rows } = await client.query("INSERT INTO todos (title, done) VALUES ($1, false) RETURNING *", [title]);
|
|
12
|
-
return toTodo(rows[0]);
|
|
13
|
-
},
|
|
14
|
-
async toggleTodo(id) {
|
|
15
|
-
await client.query("UPDATE todos SET done = NOT done WHERE id = $1", [id]);
|
|
16
|
-
const { rows } = await client.query("SELECT * FROM todos ORDER BY id");
|
|
17
|
-
return rows.map(toTodo);
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
/* Cách dùng thật (khi có pg + server):
|
|
22
|
-
* import { Client } from "pg";
|
|
23
|
-
* const client = new Client(process.env.DATABASE_URL);
|
|
24
|
-
* await client.connect();
|
|
25
|
-
* await client.query(`CREATE TABLE IF NOT EXISTS todos (
|
|
26
|
-
* id SERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false)`);
|
|
27
|
-
* const backend = createPostgresBackend(client);
|
|
28
|
-
*/
|
package/lib/backends/sqlite.d.ts
DELETED
package/lib/backends/sqlite.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 nmvuong92
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
import { DatabaseSync } from "node:sqlite";
|
|
4
|
-
/* Backend DB THẬT — SQLite qua node:sqlite (built-in, 0 dep, persist ra file).
|
|
5
|
-
* Chạy cần cờ: node --experimental-sqlite … Cùng interface Backend → switch như mọi backend. */
|
|
6
|
-
export function createSqliteBackend(path = ":memory:") {
|
|
7
|
-
const db = new DatabaseSync(path);
|
|
8
|
-
db.exec(`CREATE TABLE IF NOT EXISTS todos (
|
|
9
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
-
title TEXT NOT NULL,
|
|
11
|
-
done INTEGER NOT NULL DEFAULT 0
|
|
12
|
-
)`);
|
|
13
|
-
const toTodo = (r) => ({ id: String(r.id), title: r.title, done: !!r.done });
|
|
14
|
-
const all = () => db.prepare("SELECT * FROM todos ORDER BY id").all().map(toTodo);
|
|
15
|
-
return {
|
|
16
|
-
name: "sqlite",
|
|
17
|
-
async listTodos() {
|
|
18
|
-
return all();
|
|
19
|
-
},
|
|
20
|
-
async addTodo(title) {
|
|
21
|
-
const info = db.prepare("INSERT INTO todos (title) VALUES (?)").run(title);
|
|
22
|
-
return toTodo(db.prepare("SELECT * FROM todos WHERE id = ?").get(info.lastInsertRowid));
|
|
23
|
-
},
|
|
24
|
-
async toggleTodo(id) {
|
|
25
|
-
db.prepare("UPDATE todos SET done = 1 - done WHERE id = ?").run(Number(id));
|
|
26
|
-
return all();
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
package/lib/backends/types.d.ts
DELETED
package/lib/backends/types.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 nmvuong92
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/* ============================================================
|
|
4
|
-
* Backend Adapter — interface chuẩn để SWITCH backend.
|
|
5
|
-
* loader/action chỉ biết tới interface này, không biết
|
|
6
|
-
* dữ liệu đến từ memory, Postgres, hay một service Go từ xa.
|
|
7
|
-
* Đổi backend = thay một implementation, frontend & cell không đổi.
|
|
8
|
-
* ============================================================ */
|
|
9
|
-
export {};
|
package/lib/core/testing.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { Backend, Todo } from "../backends/types";
|
|
2
|
-
export interface TestBackend extends Backend {
|
|
3
|
-
calls: {
|
|
4
|
-
method: string;
|
|
5
|
-
args: unknown[];
|
|
6
|
-
}[];
|
|
7
|
-
failNext(method: "listTodos" | "addTodo" | "toggleTodo", error?: Error): void;
|
|
8
|
-
}
|
|
9
|
-
export declare function createTestBackend(initial?: Todo[]): TestBackend;
|
package/lib/core/testing.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export function createTestBackend(initial = []) {
|
|
2
|
-
let todos = initial.map((t) => ({ ...t }));
|
|
3
|
-
let seq = initial.length + 1;
|
|
4
|
-
const calls = [];
|
|
5
|
-
const failures = {};
|
|
6
|
-
const guard = (method) => {
|
|
7
|
-
const e = failures[method];
|
|
8
|
-
if (e) {
|
|
9
|
-
failures[method] = undefined;
|
|
10
|
-
throw e;
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
return {
|
|
14
|
-
name: "test",
|
|
15
|
-
calls,
|
|
16
|
-
failNext(method, error = new Error(`test fail: ${method}`)) {
|
|
17
|
-
failures[method] = error;
|
|
18
|
-
},
|
|
19
|
-
async listTodos() {
|
|
20
|
-
calls.push({ method: "listTodos", args: [] });
|
|
21
|
-
guard("listTodos");
|
|
22
|
-
return todos.map((t) => ({ ...t }));
|
|
23
|
-
},
|
|
24
|
-
async addTodo(title) {
|
|
25
|
-
calls.push({ method: "addTodo", args: [title] });
|
|
26
|
-
guard("addTodo");
|
|
27
|
-
const t = { id: String(seq++), title, done: false };
|
|
28
|
-
todos = [...todos, t];
|
|
29
|
-
return { ...t };
|
|
30
|
-
},
|
|
31
|
-
async toggleTodo(id) {
|
|
32
|
-
calls.push({ method: "toggleTodo", args: [id] });
|
|
33
|
-
guard("toggleTodo");
|
|
34
|
-
todos = todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
|
|
35
|
-
return todos.map((t) => ({ ...t }));
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
package/lib/core/wiring.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Backend } from "../backends/types";
|
|
2
|
-
import type { ResolutionManifest } from "./resolver.ts";
|
|
3
|
-
export declare function backendFromManifest(m: ResolutionManifest): Backend;
|
|
4
|
-
export interface ManifestBackends {
|
|
5
|
-
byCell: Map<string, Backend>;
|
|
6
|
-
default: Backend;
|
|
7
|
-
}
|
|
8
|
-
export declare function backendsFromManifest(m: ResolutionManifest): ManifestBackends;
|
package/lib/core/wiring.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { createMemoryBackend } from "../backends/memory.js";
|
|
2
|
-
import { createSqliteBackend } from "../backends/sqlite.js";
|
|
3
|
-
// Driver TS in-process, engine dựng được zero-dep. (postgres cần client `pg` → user tự inject,
|
|
4
|
-
// không phải kind auto-resolve.)
|
|
5
|
-
function buildBackend(b) {
|
|
6
|
-
if (b.language === "memory")
|
|
7
|
-
return createMemoryBackend();
|
|
8
|
-
if (b.language === "sqlite")
|
|
9
|
-
return createSqliteBackend(process.env.FLUXE_SQLITE_PATH ?? ":memory:");
|
|
10
|
-
throw new Error(`manifest backend "${b.language}" không dựng được tự động (chỉ memory | sqlite)`);
|
|
11
|
-
}
|
|
12
|
-
// Backend app-level (default) — giữ cho code cũ/đơn giản.
|
|
13
|
-
export function backendFromManifest(m) {
|
|
14
|
-
return buildBackend(m.backend);
|
|
15
|
-
}
|
|
16
|
-
// Dựng backend per-cell, DEDUP theo `language` → cells cùng resolution
|
|
17
|
-
// chia sẻ MỘT instance (vd memory dùng chung một store).
|
|
18
|
-
export function backendsFromManifest(m) {
|
|
19
|
-
const cache = new Map();
|
|
20
|
-
const make = (b) => {
|
|
21
|
-
const key = b.language;
|
|
22
|
-
let inst = cache.get(key);
|
|
23
|
-
if (!inst) {
|
|
24
|
-
inst = buildBackend(b);
|
|
25
|
-
cache.set(key, inst);
|
|
26
|
-
}
|
|
27
|
-
return inst;
|
|
28
|
-
};
|
|
29
|
-
const def = make(m.backend);
|
|
30
|
-
const byCell = new Map();
|
|
31
|
-
for (const id of Object.keys(m.cells)) {
|
|
32
|
-
byCell.set(id, make(m.cells[id].backend));
|
|
33
|
-
}
|
|
34
|
-
return { byCell, default: def };
|
|
35
|
-
}
|