@nmvuong92/fluxe 0.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 (90) hide show
  1. package/README.md +88 -0
  2. package/lib/backends/http.d.ts +2 -0
  3. package/lib/backends/http.js +32 -0
  4. package/lib/backends/memory.d.ts +2 -0
  5. package/lib/backends/memory.js +21 -0
  6. package/lib/backends/postgres.d.ts +7 -0
  7. package/lib/backends/postgres.js +28 -0
  8. package/lib/backends/sqlite.d.ts +2 -0
  9. package/lib/backends/sqlite.js +27 -0
  10. package/lib/backends/types.d.ts +11 -0
  11. package/lib/backends/types.js +7 -0
  12. package/lib/core/auth.d.ts +12 -0
  13. package/lib/core/auth.js +54 -0
  14. package/lib/core/broker.d.ts +7 -0
  15. package/lib/core/broker.js +27 -0
  16. package/lib/core/chaos.d.ts +5 -0
  17. package/lib/core/chaos.js +11 -0
  18. package/lib/core/cli.d.ts +6 -0
  19. package/lib/core/cli.js +54 -0
  20. package/lib/core/client.d.ts +33 -0
  21. package/lib/core/client.js +108 -0
  22. package/lib/core/codegen.d.ts +7 -0
  23. package/lib/core/codegen.js +28 -0
  24. package/lib/core/engine.d.ts +28 -0
  25. package/lib/core/engine.js +1 -0
  26. package/lib/core/env.d.ts +2 -0
  27. package/lib/core/env.js +10 -0
  28. package/lib/core/errors.d.ts +19 -0
  29. package/lib/core/errors.js +39 -0
  30. package/lib/core/etag.d.ts +2 -0
  31. package/lib/core/etag.js +11 -0
  32. package/lib/core/jobs.d.ts +27 -0
  33. package/lib/core/jobs.js +54 -0
  34. package/lib/core/layouts.d.ts +5 -0
  35. package/lib/core/layouts.js +17 -0
  36. package/lib/core/nav.d.ts +29 -0
  37. package/lib/core/nav.js +53 -0
  38. package/lib/core/observe.d.ts +12 -0
  39. package/lib/core/observe.js +24 -0
  40. package/lib/core/panel.d.ts +3 -0
  41. package/lib/core/panel.js +46 -0
  42. package/lib/core/presence.d.ts +6 -0
  43. package/lib/core/presence.js +33 -0
  44. package/lib/core/ratelimit.d.ts +13 -0
  45. package/lib/core/ratelimit.js +32 -0
  46. package/lib/core/rendercache.d.ts +15 -0
  47. package/lib/core/rendercache.js +39 -0
  48. package/lib/core/resolver.d.ts +37 -0
  49. package/lib/core/resolver.js +42 -0
  50. package/lib/core/router.d.ts +6 -0
  51. package/lib/core/router.js +41 -0
  52. package/lib/core/seo.d.ts +10 -0
  53. package/lib/core/seo.js +27 -0
  54. package/lib/core/testing.d.ts +9 -0
  55. package/lib/core/testing.js +38 -0
  56. package/lib/core/validate.d.ts +4 -0
  57. package/lib/core/validate.js +17 -0
  58. package/lib/core/wiring.d.ts +8 -0
  59. package/lib/core/wiring.js +33 -0
  60. package/lib/hot/search.d.ts +9 -0
  61. package/lib/hot/search.js +16 -0
  62. package/lib/index.d.ts +20 -0
  63. package/lib/index.js +22 -0
  64. package/lib/react/DebugBar.d.ts +3 -0
  65. package/lib/react/DebugBar.js +57 -0
  66. package/lib/react/Link.d.ts +12 -0
  67. package/lib/react/Link.js +11 -0
  68. package/lib/react/Nav.d.ts +8 -0
  69. package/lib/react/Nav.js +7 -0
  70. package/lib/react/ThemeToggle.d.ts +1 -0
  71. package/lib/react/ThemeToggle.js +6 -0
  72. package/lib/react/index.d.ts +10 -0
  73. package/lib/react/index.js +11 -0
  74. package/lib/react/mutation.d.ts +5 -0
  75. package/lib/react/mutation.js +30 -0
  76. package/lib/react/nav-client.d.ts +10 -0
  77. package/lib/react/nav-client.js +76 -0
  78. package/lib/react/query.d.ts +9 -0
  79. package/lib/react/query.js +62 -0
  80. package/lib/react/repro.d.ts +4 -0
  81. package/lib/react/repro.js +19 -0
  82. package/lib/react/shell.d.ts +1 -0
  83. package/lib/react/shell.js +13 -0
  84. package/lib/react/store.d.ts +28 -0
  85. package/lib/react/store.js +31 -0
  86. package/lib/react/theme.d.ts +6 -0
  87. package/lib/react/theme.js +29 -0
  88. package/lib/server_factory.d.ts +12 -0
  89. package/lib/server_factory.js +293 -0
  90. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # fluxe
2
+
3
+ Khung fullstack **polyglot** dựa trên triết lý **RCA — Resolved Cell Architecture**:
4
+ *logic chỉ phụ thuộc HỢP ĐỒNG; mọi quyết định vận hành (ngôn ngữ, render, transport,
5
+ backend, scale) là **kết quả được GIẢI** bởi engine, không viết tay.*
6
+
7
+ > Đổi backend từ TS → Go → Rust, đổi render static ↔ island, gộp nhiều backend trong một
8
+ > app per-cell — tất cả chỉ sửa `app/profiles.ts`, **cell & frontend không đổi một dòng**.
9
+
10
+ ## Cài & dùng (npm)
11
+
12
+ ```bash
13
+ npm i @nmvuong92/fluxe react react-dom zod
14
+ ```
15
+
16
+ ```ts
17
+ import { defineCell, withInput, makeServer, createMemoryBackend } from "@nmvuong92/fluxe";
18
+ import { useQuery, useMutation, Link, Nav, ThemeToggle } from "@nmvuong92/fluxe/react";
19
+ import { rpc } from "@nmvuong92/fluxe/client";
20
+ ```
21
+
22
+ | Import | Nội dung |
23
+ |--------|----------|
24
+ | `@nmvuong92/fluxe` | engine: defineCell, makeServer, resolver, auth, validate, backends, seo, broker, ratelimit, codegen… |
25
+ | `@nmvuong92/fluxe/react` | useQuery, useMutation, Link, Nav, ThemeToggle, useTheme, DebugBar |
26
+ | `@nmvuong92/fluxe/client` | rpc, RpcError, mutate, revalidate, subscribe |
27
+ | `@nmvuong92/fluxe/jobs` · `/sqlite` | queue/dead-letter · SQLite backend (cần `--experimental-sqlite`) |
28
+
29
+ ## Chạy nhanh
30
+
31
+ ```bash
32
+ npm install
33
+ npm run fx -- build # resolve + prerender + bundle (1 schema → TS/Go/Rust qua fx gen)
34
+ npm run fx -- dev # http://localhost:5180
35
+ npm run test:all # typecheck + 107 unit + 28 integration — TẤT CẢ XANH
36
+ ```
37
+
38
+ `fx`: `gen · resolve · prerender · build · dev · test · jobs`.
39
+
40
+ ## Cấu trúc — ranh giới DEV vs ENGINE
41
+
42
+ ```
43
+ app/ ← DEV sở hữu (sửa thoải mái) — Contract Plane
44
+ cells/ trang/feature (route + loader + view + action/head/layout/guard)
45
+ layouts/ layout dùng chung (nested)
46
+ profiles.ts CHỌN backend per môi trường + per-cell (memory/go/rust)
47
+ contract.ts schema → codegen TS/Go/Rust
48
+ env.ts env có kiểu, validate fail-fast lúc boot
49
+ native/ service Go/Rust CỦA DEV (backend/host/hot/actor)
50
+
51
+ src/ ← ENGINE (không đụng) — Resolution Plane
52
+ core/ resolver · router · errors · auth · validate · codegen · layouts ·
53
+ broker · presence · jobs · ratelimit · observe · panel · seo · cli · ...
54
+ server_factory.ts runtime ráp cell + giải manifest
55
+ ```
56
+
57
+ **Quy tắc:** "backend nào đang chạy" do `app/profiles.ts` (config) quyết, **không** do vị trí
58
+ folder. Engine không bao giờ import ngược vào `app/`.
59
+
60
+ ## Tính năng (tất cả TDD + chạy thật)
61
+
62
+ - **Render** — static (0 JS) · island hydrate · SPA nav (Inertia) · static-prerender (Go phục vụ thẳng) · API mode `?json=1`
63
+ - **Routing** — động `[param]` → `ctx.input` · **nested layouts** · SEO (head/canonical/OG/JSON-LD per cell, `/sitemap.xml`, `/robots.txt`)
64
+ - **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
65
+ - **Data** — backend polyglot per-cell · DB **SQLite thật** + adapter **Postgres** · **codegen contract** 1 schema → TS/Go/Rust
66
+ - **Mutations DX** — `RpcError` có cấu trúc · `mutate()` optimistic + rollback · lỗi validation field-level
67
+ - **Realtime (Trục 4g)** — **SSE channel** + pub/sub broker · live-update on action · **presence** (multi-tab) · **actor-Go** (BEAM-style: room=goroutine, supervisor restart)
68
+ - **Async** — **job queue bền** (SQLite, retry → dead-letter)
69
+ - **Observability** — request log + dashboard `/_fluxe` (RCA Resolution + Recent requests)
70
+ - **Config** — env có kiểu, fail-fast lúc boot
71
+ - **DX** — `fx` CLI · mock `Backend` test cực dễ · typecheck gate
72
+
73
+ ## Polyglot 4 tầng (Go/Rust thật, đã chạy)
74
+
75
+ | Tầng | Ngôn ngữ | Demo |
76
+ |------|----------|------|
77
+ | **Backend** (Todo CRUD) | Go · Rust | `./run-native.sh` |
78
+ | **Host/edge** (proxy + static) | Go | `./run-host.sh` |
79
+ | **Hot compute** (search) | Rust | `./run-hot.sh` |
80
+ | **Actor/realtime** (room+supervisor) | Go | `./run-actor.sh` |
81
+
82
+ Engine viết bằng: **TS** (Resolver + SSR/cell) · **Go** (host/actor) · **Rust** (hot compute) —
83
+ mỗi tầng đúng ngôn ngữ tối ưu, đúng §6d của [idea.md](idea.md).
84
+
85
+ ## Triết lý
86
+
87
+ 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)**.
88
+ Spec + plan: `docs/superpowers/`.
@@ -0,0 +1,2 @@
1
+ import type { Backend } from "./types";
2
+ export declare function createHttpBackend(name: string, baseUrl: string): Backend;
@@ -0,0 +1,32 @@
1
+ /* Backend #3/#4: gọi một service THẬT qua HTTP (Go hoặc Rust).
2
+ * Cùng interface Backend → cell + frontend KHÔNG đổi một dòng.
3
+ * Service chỉ cần tôn trọng "hợp đồng" 3 endpoint:
4
+ * GET /todos → Todo[]
5
+ * POST /todos {title} → Todo
6
+ * POST /todos/{id}/toggle → Todo[]
7
+ */
8
+ export function createHttpBackend(name, baseUrl) {
9
+ const base = baseUrl.replace(/\/$/, "");
10
+ async function j(path, init) {
11
+ const r = await fetch(base + path, init);
12
+ if (!r.ok)
13
+ throw new Error(`${name} backend ${path} → HTTP ${r.status}`);
14
+ return (await r.json());
15
+ }
16
+ return {
17
+ name,
18
+ listTodos() {
19
+ return j("/todos");
20
+ },
21
+ addTodo(title) {
22
+ return j("/todos", {
23
+ method: "POST",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify({ title }),
26
+ });
27
+ },
28
+ toggleTodo(id) {
29
+ return j(`/todos/${encodeURIComponent(id)}/toggle`, { method: "POST" });
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,2 @@
1
+ import type { Backend } from "./types";
2
+ export declare function createMemoryBackend(): Backend;
@@ -0,0 +1,21 @@
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
+ }
@@ -0,0 +1,7 @@
1
+ import type { Backend } from "./types";
2
+ export interface PgClientLike {
3
+ query(sql: string, params?: unknown[]): Promise<{
4
+ rows: any[];
5
+ }>;
6
+ }
7
+ export declare function createPostgresBackend(client: PgClientLike): Backend;
@@ -0,0 +1,28 @@
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
+ */
@@ -0,0 +1,2 @@
1
+ import type { Backend } from "./types";
2
+ export declare function createSqliteBackend(path?: string): Backend;
@@ -0,0 +1,27 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ /* Backend DB THẬT — SQLite qua node:sqlite (built-in, 0 dep, persist ra file).
3
+ * Chạy cần cờ: node --experimental-sqlite … Cùng interface Backend → switch như mọi backend. */
4
+ export function createSqliteBackend(path = ":memory:") {
5
+ const db = new DatabaseSync(path);
6
+ db.exec(`CREATE TABLE IF NOT EXISTS todos (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ title TEXT NOT NULL,
9
+ done INTEGER NOT NULL DEFAULT 0
10
+ )`);
11
+ const toTodo = (r) => ({ id: String(r.id), title: r.title, done: !!r.done });
12
+ const all = () => db.prepare("SELECT * FROM todos ORDER BY id").all().map(toTodo);
13
+ return {
14
+ name: "sqlite",
15
+ async listTodos() {
16
+ return all();
17
+ },
18
+ async addTodo(title) {
19
+ const info = db.prepare("INSERT INTO todos (title) VALUES (?)").run(title);
20
+ return toTodo(db.prepare("SELECT * FROM todos WHERE id = ?").get(info.lastInsertRowid));
21
+ },
22
+ async toggleTodo(id) {
23
+ db.prepare("UPDATE todos SET done = 1 - done WHERE id = ?").run(Number(id));
24
+ return all();
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,11 @@
1
+ export interface Todo {
2
+ id: string;
3
+ title: string;
4
+ done: boolean;
5
+ }
6
+ export interface Backend {
7
+ name: string;
8
+ listTodos(): Promise<Todo[]>;
9
+ addTodo(title: string): Promise<Todo>;
10
+ toggleTodo(id: string): Promise<Todo[]>;
11
+ }
@@ -0,0 +1,7 @@
1
+ /* ============================================================
2
+ * Backend Adapter — interface chuẩn để SWITCH backend.
3
+ * loader/action chỉ biết tới interface này, không biết
4
+ * dữ liệu đến từ memory, Postgres, hay một service Go từ xa.
5
+ * Đổi backend = thay một implementation, frontend & cell không đổi.
6
+ * ============================================================ */
7
+ export {};
@@ -0,0 +1,12 @@
1
+ export interface Session {
2
+ user: string;
3
+ roles?: string[];
4
+ [k: string]: unknown;
5
+ }
6
+ export declare function hasRole(session: Session | null, role: string): boolean;
7
+ export declare function signSession(payload: Session, secret: string): string;
8
+ export declare function verifySession(token: string | undefined, secret: string): Session | null;
9
+ export declare function hashPassword(password: string): string;
10
+ export declare function verifyPassword(password: string, stored: string): boolean;
11
+ export declare function newCsrfToken(): string;
12
+ export declare function parseCookie(header: string | undefined): Record<string, string>;
@@ -0,0 +1,54 @@
1
+ import { createHmac, timingSafeEqual, scryptSync, randomBytes } from "node:crypto";
2
+ export function hasRole(session, role) {
3
+ return !!session?.roles?.includes(role);
4
+ }
5
+ export function signSession(payload, secret) {
6
+ const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
7
+ const sig = createHmac("sha256", secret).update(body).digest("base64url");
8
+ return `${body}.${sig}`;
9
+ }
10
+ export function verifySession(token, secret) {
11
+ if (!token)
12
+ return null;
13
+ const [body, sig] = token.split(".");
14
+ if (!body || !sig)
15
+ return null;
16
+ const expected = createHmac("sha256", secret).update(body).digest("base64url");
17
+ const a = Buffer.from(sig), b = Buffer.from(expected);
18
+ if (a.length !== b.length || !timingSafeEqual(a, b))
19
+ return null;
20
+ try {
21
+ return JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ /* Password hashing — scrypt (built-in, memory-hard). Format: scrypt$salt$hash.
28
+ * (Production: Argon2id qua `npm i argon2`; scrypt là chuẩn built-in, an toàn.) */
29
+ export function hashPassword(password) {
30
+ const salt = randomBytes(16);
31
+ const hash = scryptSync(password, salt, 32);
32
+ return `scrypt$${salt.toString("hex")}$${hash.toString("hex")}`;
33
+ }
34
+ export function verifyPassword(password, stored) {
35
+ const [algo, saltHex, hashHex] = stored.split("$");
36
+ if (algo !== "scrypt" || !saltHex || !hashHex)
37
+ return false;
38
+ const hash = scryptSync(password, Buffer.from(saltHex, "hex"), 32);
39
+ const expected = Buffer.from(hashHex, "hex");
40
+ return hash.length === expected.length && timingSafeEqual(hash, expected);
41
+ }
42
+ /* CSRF token (double-submit cookie): cookie csrf + header x-csrf-token phải khớp. */
43
+ export function newCsrfToken() {
44
+ return randomBytes(24).toString("hex");
45
+ }
46
+ export function parseCookie(header) {
47
+ const out = {};
48
+ for (const part of (header ?? "").split(";")) {
49
+ const i = part.indexOf("=");
50
+ if (i > 0)
51
+ out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim());
52
+ }
53
+ return out;
54
+ }
@@ -0,0 +1,7 @@
1
+ export type Subscriber = (data: unknown) => void;
2
+ export interface Broker {
3
+ subscribe(topic: string, fn: Subscriber): () => void;
4
+ publish(topic: string, data: unknown): void;
5
+ count(topic: string): number;
6
+ }
7
+ export declare function createBroker(): Broker;
@@ -0,0 +1,27 @@
1
+ /* Broker pub/sub in-memory — nền của realtime channel (Trục 4g), bản 1-node.
2
+ * Bản distributed: thay bằng NATS/Redis fan-out (cùng interface). Bản stateful:
3
+ * actor-Go (app/native/actor-go). Đây là lõi đơn giản nhất, dependency-free. */
4
+ export function createBroker() {
5
+ const subs = new Map();
6
+ return {
7
+ subscribe(topic, fn) {
8
+ let set = subs.get(topic);
9
+ if (!set) {
10
+ set = new Set();
11
+ subs.set(topic, set);
12
+ }
13
+ set.add(fn);
14
+ return () => {
15
+ set.delete(fn);
16
+ if (set.size === 0)
17
+ subs.delete(topic);
18
+ };
19
+ },
20
+ publish(topic, data) {
21
+ subs.get(topic)?.forEach((fn) => fn(data));
22
+ },
23
+ count(topic) {
24
+ return subs.get(topic)?.size ?? 0;
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,5 @@
1
+ export interface Chaos {
2
+ delayMs: number;
3
+ failRate: number;
4
+ }
5
+ export declare function parseChaos(header: string | undefined): Chaos;
@@ -0,0 +1,11 @@
1
+ export function parseChaos(header) {
2
+ const out = { delayMs: 0, failRate: 0 };
3
+ for (const part of (header ?? "").split(";")) {
4
+ const [k, v] = part.split("=");
5
+ if (k?.trim() === "delay")
6
+ out.delayMs = Math.max(0, Number(v) || 0);
7
+ if (k?.trim() === "fail")
8
+ out.failRate = Math.min(1, Math.max(0, Number(v) || 0));
9
+ }
10
+ return out;
11
+ }
@@ -0,0 +1,6 @@
1
+ export interface Command {
2
+ desc: string;
3
+ shell: (args: string[]) => string;
4
+ }
5
+ export declare const COMMANDS: Record<string, Command>;
6
+ export declare function renderUsage(): string;
@@ -0,0 +1,54 @@
1
+ /* fx CLI — registry lệnh thuần (testable). bin/fx.ts dispatch + spawn. */
2
+ const SYNC = "tsx scripts/sync.ts"; // auto-discovery: quét app/cells/* → app/app.ts
3
+ const ESBUILD = "esbuild src/client.tsx --bundle --format=esm --outfile=dist/client.js --jsx=automatic --loader:.tsx=tsx";
4
+ const p = (a) => a[0] ?? "dev"; // profile mặc định
5
+ export const COMMANDS = {
6
+ init: {
7
+ desc: "Scaffold app/ mới (env, profiles, layout, cell home) — chỉ tạo file còn thiếu",
8
+ shell: () => `tsx scripts/init.ts`,
9
+ },
10
+ new: {
11
+ desc: "Tạo cell mới: fx new <id> [--island] (auto-discovery tự đăng ký)",
12
+ shell: (a) => `tsx scripts/new-cell.ts ${a.join(" ")}`,
13
+ },
14
+ sync: {
15
+ desc: "Auto-discovery: quét app/cells/* → app/app.ts (dev/resolve tự gọi)",
16
+ shell: () => SYNC,
17
+ },
18
+ gen: {
19
+ desc: "Codegen contract → types TS/Go/Rust (.fluxe/gen)",
20
+ shell: () => `tsx scripts/codegen.ts`,
21
+ },
22
+ resolve: {
23
+ desc: "Sinh .fluxe/resolution.json từ profile",
24
+ shell: (a) => `${SYNC} && tsx scripts/resolve.ts ${p(a)}`,
25
+ },
26
+ jobs: {
27
+ desc: "Demo job queue (enqueue + worker drain + dead-letter)",
28
+ shell: () => `node --experimental-sqlite --import tsx scripts/jobs-demo.ts`,
29
+ },
30
+ bench: {
31
+ desc: "Benchmark RPS/QPS + latency p50/p99 + RAM/CPU",
32
+ shell: (a) => `${SYNC} && tsx scripts/resolve.ts dev && npm run --silent build:client && tsx scripts/bench.ts ${a.join(" ")}`,
33
+ },
34
+ prerender: {
35
+ desc: "Prerender cell static → .fluxe/static.json",
36
+ shell: (a) => `${SYNC} && tsx scripts/prerender.ts ${p(a)}`,
37
+ },
38
+ build: {
39
+ desc: "Build đầy đủ: sync + resolve + prerender + client bundle",
40
+ shell: (a) => `${SYNC} && tsx scripts/resolve.ts ${p(a)} && tsx scripts/prerender.ts ${p(a)} && ${ESBUILD}`,
41
+ },
42
+ dev: {
43
+ desc: "Sync + resolve + build client + chạy server",
44
+ shell: (a) => `${SYNC} && tsx scripts/resolve.ts ${p(a)} && ${ESBUILD} && tsx src/server.tsx`,
45
+ },
46
+ test: {
47
+ desc: "Sync + typecheck + unit + integration",
48
+ shell: () => `${SYNC} && tsc --noEmit && node --experimental-sqlite --import tsx --test '{src,app}/**/*.test.ts' && tsx src/selftest2.ts`,
49
+ },
50
+ };
51
+ export function renderUsage() {
52
+ const lines = Object.entries(COMMANDS).map(([n, c]) => ` fx ${n.padEnd(10)} ${c.desc}`);
53
+ return `fluxe CLI\n\nLệnh:\n${lines.join("\n")}\n`;
54
+ }
@@ -0,0 +1,33 @@
1
+ export declare class RpcError extends Error {
2
+ code: string;
3
+ status: number;
4
+ details?: unknown;
5
+ constructor(code: string, message: string, status: number, details?: unknown);
6
+ }
7
+ export declare function parseRpcError(status: number, body: string): RpcError;
8
+ export declare const setChaos: (v: string) => void;
9
+ export declare const getChaos: () => string;
10
+ export declare const setDevBackend: (v: string) => void;
11
+ export declare const getDevBackend: () => string;
12
+ export interface RpcMeta {
13
+ resolution?: string;
14
+ serverMs?: number;
15
+ clientMs?: number;
16
+ }
17
+ export declare const lastRpcMeta: () => RpcMeta;
18
+ export declare function rpc<T = any>(cell: string, action: string, input: unknown): Promise<T>;
19
+ export declare function mutate<T>(opts: {
20
+ optimistic?: () => void;
21
+ run: () => Promise<T>;
22
+ rollback?: () => void;
23
+ }): Promise<T>;
24
+ export declare function fetchPageProps(url: string): Promise<{
25
+ cell: string;
26
+ data: unknown;
27
+ layout?: string;
28
+ }>;
29
+ export declare function revalidate(): Promise<{
30
+ cell: string;
31
+ data: unknown;
32
+ }>;
33
+ export declare function subscribe(topic: string, onData: (data: any) => void): () => void;
@@ -0,0 +1,108 @@
1
+ /* ============================================================
2
+ * Client runtime — Inertia-style + mutations DX.
3
+ * - rpc(): gọi action, ném RpcError CÓ CẤU TRÚC (code/message/details) khi lỗi.
4
+ * - mutate(): optimistic + rollback khi lỗi.
5
+ * - revalidate(): refetch props trang hiện tại sau khi mutate.
6
+ * ============================================================ */
7
+ export class RpcError extends Error {
8
+ code;
9
+ status;
10
+ details;
11
+ constructor(code, message, status, details) {
12
+ super(message);
13
+ this.name = "RpcError";
14
+ this.code = code;
15
+ this.status = status;
16
+ this.details = details;
17
+ }
18
+ }
19
+ // Pure: dựng RpcError từ status + body trả về (server trả {error:{code,message,details}}).
20
+ export function parseRpcError(status, body) {
21
+ try {
22
+ const e = JSON.parse(body)?.error;
23
+ if (e?.code)
24
+ return new RpcError(e.code, e.message ?? "Lỗi", status, e.details);
25
+ }
26
+ catch {
27
+ /* không phải JSON */
28
+ }
29
+ return new RpcError("http", `HTTP ${status}`, status);
30
+ }
31
+ function cookie(name) {
32
+ if (typeof document === "undefined")
33
+ return "";
34
+ const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
35
+ return m ? decodeURIComponent(m[1]) : "";
36
+ }
37
+ /* DevTools config (chỉ ảnh hưởng dev): chaos injection + live backend swap. DebugBar set. */
38
+ let _chaos = ""; // vd "delay=600;fail=0.3"
39
+ let _devBackend = ""; // vd "go" | "rust" | "memory"
40
+ export const setChaos = (v) => { _chaos = v; };
41
+ export const getChaos = () => _chaos;
42
+ export const setDevBackend = (v) => { _devBackend = v; };
43
+ export const getDevBackend = () => _devBackend;
44
+ let _lastMeta = {};
45
+ export const lastRpcMeta = () => _lastMeta;
46
+ export async function rpc(cell, action, input) {
47
+ const t0 = typeof performance !== "undefined" ? performance.now() : 0;
48
+ const headers = { "content-type": "application/json", "x-csrf-token": cookie("csrf") };
49
+ if (_chaos)
50
+ headers["x-fluxe-chaos"] = _chaos; // #1 chaos
51
+ if (_devBackend)
52
+ headers["x-fluxe-backend"] = _devBackend; // #5 live swap
53
+ let res;
54
+ try {
55
+ res = await fetch(`/__action/${cell}/${action}`, { method: "POST", headers, body: JSON.stringify(input) });
56
+ }
57
+ catch {
58
+ _lastMeta = {};
59
+ throw new RpcError("network", "Mất kết nối máy chủ", 0);
60
+ }
61
+ const hget = (k) => res.headers?.get?.(k) ?? null;
62
+ _lastMeta = {
63
+ resolution: hget("x-fluxe-resolution") ?? undefined,
64
+ serverMs: Number(hget("x-fluxe-server-ms")) || undefined,
65
+ clientMs: Math.round((typeof performance !== "undefined" ? performance.now() : 0) - t0),
66
+ };
67
+ if (!res.ok)
68
+ throw parseRpcError(res.status, await res.text());
69
+ return res.json();
70
+ }
71
+ // Optimistic update: chạy optimistic() ngay, run() ngầm; lỗi → rollback() + ném lại.
72
+ export async function mutate(opts) {
73
+ opts.optimistic?.();
74
+ try {
75
+ return await opts.run();
76
+ }
77
+ catch (e) {
78
+ opts.rollback?.();
79
+ throw e;
80
+ }
81
+ }
82
+ /* SPA navigation kiểu Inertia: lấy props JSON rồi để runtime render lại */
83
+ export async function fetchPageProps(url) {
84
+ const res = await fetch(url, { headers: { "x-fluxe": "1" } });
85
+ if (!res.ok)
86
+ throw new Error(`fetchPageProps ${url} → ${res.status}`);
87
+ const ct = res.headers.get("content-type") ?? "";
88
+ if (!ct.includes("application/json"))
89
+ throw new Error("không phải trang cell"); // file tĩnh → hard nav
90
+ return res.json();
91
+ }
92
+ /* Revalidate: refetch props trang hiện tại (gọi sau mutate để đồng bộ data). */
93
+ export async function revalidate() {
94
+ return fetchPageProps(location.pathname + location.search);
95
+ }
96
+ /* Realtime: subscribe topic qua SSE. Trả hàm hủy. (Trục 4g) */
97
+ export function subscribe(topic, onData) {
98
+ if (typeof EventSource === "undefined")
99
+ return () => { };
100
+ const es = new EventSource(`/__sse/${encodeURIComponent(topic)}`);
101
+ es.onmessage = (e) => {
102
+ try {
103
+ onData(JSON.parse(e.data));
104
+ }
105
+ catch { /* ignore */ }
106
+ };
107
+ return () => es.close();
108
+ }
@@ -0,0 +1,7 @@
1
+ export type FieldType = "string" | "bool" | "int";
2
+ export interface Schema {
3
+ types: Record<string, Record<string, FieldType>>;
4
+ }
5
+ export declare function genTS(s: Schema): string;
6
+ export declare function genGo(s: Schema, pkg?: string): string;
7
+ export declare function genRust(s: Schema): string;
@@ -0,0 +1,28 @@
1
+ /* Codegen contract polyglot — MỘT schema → types TS + Go + Rust (chữ ký RCA:
2
+ * type-safe xuyên ngôn ngữ). Thuần (string-in/string-out), dễ test. */
3
+ const pascal = (s) => s.charAt(0).toUpperCase() + s.slice(1);
4
+ const TS = { string: "string", bool: "boolean", int: "number" };
5
+ const GO = { string: "string", bool: "bool", int: "int" };
6
+ const RS = { string: "String", bool: "bool", int: "i64" };
7
+ export function genTS(s) {
8
+ return Object.entries(s.types)
9
+ .map(([name, fields]) => `export interface ${name} {\n` +
10
+ Object.entries(fields).map(([f, t]) => ` ${f}: ${TS[t]};`).join("\n") +
11
+ `\n}`)
12
+ .join("\n\n") + "\n";
13
+ }
14
+ export function genGo(s, pkg = "contract") {
15
+ return `package ${pkg}\n\n` +
16
+ Object.entries(s.types)
17
+ .map(([name, fields]) => `type ${name} struct {\n` +
18
+ Object.entries(fields).map(([f, t]) => `\t${pascal(f)} ${GO[t]} \`json:"${f}"\``).join("\n") +
19
+ `\n}`)
20
+ .join("\n\n") + "\n";
21
+ }
22
+ export function genRust(s) {
23
+ return Object.entries(s.types)
24
+ .map(([name, fields]) => `#[derive(Clone, Debug)]\npub struct ${name} {\n` +
25
+ Object.entries(fields).map(([f, t]) => ` pub ${f}: ${RS[t]},`).join("\n") +
26
+ `\n}`)
27
+ .join("\n\n") + "\n";
28
+ }
@@ -0,0 +1,28 @@
1
+ import type { ComponentType } from "react";
2
+ import type { Backend } from "../backends/types";
3
+ import type { HeadMeta } from "./seo";
4
+ import type { Session } from "./auth";
5
+ export interface Ctx<I> {
6
+ input: I;
7
+ backend: Backend;
8
+ session?: Session | null;
9
+ }
10
+ export type Loader<I, O> = (ctx: Ctx<I>) => Promise<O>;
11
+ export type Action<I, O> = (ctx: Ctx<I>) => Promise<O>;
12
+ export type Hydration = "static" | "island";
13
+ export interface CellDef<I, O> {
14
+ id: string;
15
+ route: string;
16
+ hydration?: Hydration;
17
+ loader: Loader<I, O>;
18
+ view: ComponentType<{
19
+ data: O;
20
+ }>;
21
+ actions?: Record<string, Action<any, any>>;
22
+ head?: (data: O) => HeadMeta;
23
+ layout?: string;
24
+ requireAuth?: boolean;
25
+ requireRole?: string;
26
+ cache?: boolean;
27
+ }
28
+ export declare function defineCell<I, O>(c: CellDef<I, O>): CellDef<I, O>;
@@ -0,0 +1 @@
1
+ export function defineCell(c) { return c; }
@@ -0,0 +1,2 @@
1
+ import type { ZodType } from "zod";
2
+ export declare function loadEnv<T>(schema: ZodType<T>, source?: Record<string, string | undefined>): T;
@@ -0,0 +1,10 @@
1
+ /* Config/env có kiểu (4k) — validate process.env theo schema, FAIL-FAST lúc boot
2
+ * (thiếu/sai biến → chặn ngay, không chết giữa prod). Coerce string→number/bool qua Zod. */
3
+ export function loadEnv(schema, source = process.env) {
4
+ const r = schema.safeParse(source);
5
+ if (!r.success) {
6
+ const issues = r.error.issues.map((i) => ` ${i.path.join(".") || "(env)"}: ${i.message}`).join("\n");
7
+ throw new Error(`Cấu hình env không hợp lệ (fail-fast lúc boot):\n${issues}`);
8
+ }
9
+ return r.data;
10
+ }