@nmvuong92/fluxe 0.5.0 → 0.7.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/README.md CHANGED
@@ -4,12 +4,12 @@
4
4
  [![npm](https://img.shields.io/npm/v/@nmvuong92/fluxe.svg)](https://www.npmjs.com/package/@nmvuong92/fluxe)
5
5
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
6
6
 
7
- Khung fullstack **polyglot** dựa trên triết **RCA Resolved Cell Architecture**:
8
- *logic chỉ phụ thuộc HỢP ĐỒNG; mọi quyết định vận hành (ngôn ngữ, render, transport,
9
- backend, scale) là **kết quả được GIẢI** bởi engine, không viết tay.*
7
+ Khung fullstack tối giản — **một runtime TypeScript** (chạy trên `node:http` zero-dep)dựa
8
+ trên triết lý **RCA — Resolved Cell Architecture**: *logic chỉ phụ thuộc HỢP ĐỒNG; mọi quyết định
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 từ TS Go Rust, đổi render static ↔ island, gộp nhiều backend trong một
12
- > app per-cell — tất cả chỉ sửa `app/profiles.ts`, **cell & frontend không đổi một dòng**.
11
+ > Đổi backend data `memory sqlite postgres`, đổi render `static ↔ island`, gộp nhiều backend
12
+ > trong một app per-cell — tất cả chỉ sửa `app/profiles.ts`, **cell & frontend không đổi một dòng**.
13
13
 
14
14
  ## Cài & dùng (npm)
15
15
 
@@ -34,7 +34,7 @@ import { rpc } from "@nmvuong92/fluxe/client";
34
34
 
35
35
  ```bash
36
36
  npm install
37
- npm run fx -- build # resolve + prerender + bundle (1 schema → TS/Go/Rust qua fx gen)
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
39
  npm run test:all # typecheck + 107 unit + 28 integration — TẤT CẢ XANH
40
40
  ```
@@ -47,10 +47,9 @@ 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
- profiles.ts CHỌN backend per môi trường + per-cell (memory/go/rust)
51
- contract.ts schema → codegen TS/Go/Rust
50
+ profiles.ts CHỌN backend data per môi trường + per-cell (memory/sqlite/postgres)
51
+ contract.ts schema → codegen types TS
52
52
  env.ts env có kiểu, validate fail-fast lúc boot
53
- native/ service Go/Rust CỦA DEV (backend/host/hot/actor)
54
53
 
55
54
  src/ ← ENGINE (không đụng) — Resolution Plane
56
55
  core/ resolver · router · errors · auth · validate · codegen · layouts ·
@@ -63,28 +62,34 @@ folder. Engine không bao giờ import ngược vào `app/`.
63
62
 
64
63
  ## Tính năng (tất cả TDD + chạy thật)
65
64
 
66
- - **Render** — static (0 JS) · island hydrate · SPA nav (Inertia) · static-prerender (Go phục vụ thẳng) · API mode `?json=1`
65
+ - **Render** — static (0 JS) · island hydrate · SPA nav (Inertia) · static-prerender · API mode `?json=1`
67
66
  - **Routing** — động `[param]` → `ctx.input` · **nested layouts** · SEO (head/canonical/OG/JSON-LD per cell, `/sitemap.xml`, `/robots.txt`)
68
67
  - **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
69
- - **Data** — backend polyglot per-cell · DB **SQLite thật** + adapter **Postgres** · **codegen contract** 1 schema → TS/Go/Rust
68
+ - **Data** — backend TS per-cell (`memory`/`sqlite`/`postgres`) · DB **SQLite thật** (`node:sqlite`) + **Postgres** (inject client `pg`) · **codegen contract** 1 schema → types TS
70
69
  - **Mutations DX** — `RpcError` có cấu trúc · `mutate()` optimistic + rollback · lỗi validation field-level
71
- - **Realtime (Trục 4g)** — **SSE channel** + pub/sub broker · live-update on action · **presence** (multi-tab) · **actor-Go** (BEAM-style: room=goroutine, supervisor restart)
70
+ - **Realtime (Trục 4g)** — **SSE channel** + pub/sub broker · live-update on action · **presence** (multi-tab)
72
71
  - **Async** — **job queue bền** (SQLite, retry → dead-letter)
73
72
  - **Observability** — request log + dashboard `/_fluxe` (RCA Resolution + Recent requests)
74
73
  - **Config** — env có kiểu, fail-fast lúc boot
75
74
  - **DX** — `fx` CLI · mock `Backend` test cực dễ · typecheck gate
76
75
 
77
- ## Polyglot 4 tầng (Go/Rust thật, đã chạy)
76
+ ## Backend data (driver TS in-process)
78
77
 
79
- | Tầng | Ngôn ngữ | Demo |
80
- |------|----------|------|
81
- | **Backend** (Todo CRUD) | Go · Rust | `./run-native.sh` |
82
- | **Host/edge** (proxy + static) | Go | `./run-host.sh` |
83
- | **Hot compute** (search) | Rust | `./run-hot.sh` |
84
- | **Actor/realtime** (room+supervisor) | Go | `./run-actor.sh` |
78
+ Backend chỉ **driver data TypeScript in-process** — chọn qua `app/profiles.ts`, cell không đổi:
85
79
 
86
- Engine viết bằng: **TS** (Resolver + SSR/cell) · **Go** (host/actor) · **Rust** (hot compute) —
87
- mỗi tầng đúng ngôn ngữ tối ưu, đúng §6d của [idea.md](idea.md).
80
+ | Driver | Ghi chú |
81
+ |--------|---------|
82
+ | `memory` | in-process, mặc định dev |
83
+ | `sqlite` | `node:sqlite` built-in, 0 dep, persist ra file (engine tự dựng) |
84
+ | `postgres` | production — **bạn tự inject client `pg`** (`npm i pg` + `DATABASE_URL`) |
85
+
86
+ ```ts
87
+ export const profiles = {
88
+ dev: { name: "dev", backend: "memory" },
89
+ sqlite: { name: "sqlite", backend: "sqlite" },
90
+ mixed: { name: "mixed", backend: "memory", cellBackends: { todos: "sqlite" } },
91
+ };
92
+ ```
88
93
 
89
94
  ## Triết lý
90
95
 
@@ -1,8 +1,8 @@
1
1
  // Copyright (c) 2026 nmvuong92
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  /* Broker pub/sub in-memory — nền của realtime channel (Trục 4g), bản 1-node.
4
- * Bản distributed: thay bằng NATS/Redis fan-out (cùng interface). Bản stateful:
5
- * actor-Go (app/native/actor-go). Đây là lõi đơn giản nhất, dependency-free. */
4
+ * Bản distributed: thay bằng NATS/Redis fan-out (cùng interface). Đây là lõi
5
+ * đơn giản nhất, dependency-free. */
6
6
  export function createBroker() {
7
7
  const subs = new Map();
8
8
  return {
package/lib/core/cli.js CHANGED
@@ -22,7 +22,7 @@ export const COMMANDS = {
22
22
  shell: () => `tsx scripts/config.ts`,
23
23
  },
24
24
  gen: {
25
- desc: "Codegen contract → types TS/Go/Rust (.fluxe/gen)",
25
+ desc: "Codegen contract → types TS (.fluxe/gen)",
26
26
  shell: () => `tsx scripts/codegen.ts`,
27
27
  },
28
28
  resolve: {
@@ -7,8 +7,6 @@ export declare class RpcError extends Error {
7
7
  export declare function parseRpcError(status: number, body: string): RpcError;
8
8
  export declare const setChaos: (v: string) => void;
9
9
  export declare const getChaos: () => string;
10
- export declare const setDevBackend: (v: string) => void;
11
- export declare const getDevBackend: () => string;
12
10
  export interface RpcMeta {
13
11
  resolution?: string;
14
12
  serverMs?: number;
@@ -36,13 +36,10 @@ function cookie(name) {
36
36
  const m = document.cookie.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
37
37
  return m ? decodeURIComponent(m[1]) : "";
38
38
  }
39
- /* DevTools config (chỉ ảnh hưởng dev): chaos injection + live backend swap. DebugBar set. */
39
+ /* DevTools config (chỉ ảnh hưởng dev): chaos injection. DebugBar set. */
40
40
  let _chaos = ""; // vd "delay=600;fail=0.3"
41
- let _devBackend = ""; // vd "go" | "rust" | "memory"
42
41
  export const setChaos = (v) => { _chaos = v; };
43
42
  export const getChaos = () => _chaos;
44
- export const setDevBackend = (v) => { _devBackend = v; };
45
- export const getDevBackend = () => _devBackend;
46
43
  let _lastMeta = {};
47
44
  export const lastRpcMeta = () => _lastMeta;
48
45
  export async function rpc(cell, action, input) {
@@ -50,8 +47,6 @@ export async function rpc(cell, action, input) {
50
47
  const headers = { "content-type": "application/json", "x-csrf-token": cookie("csrf") };
51
48
  if (_chaos)
52
49
  headers["x-fluxe-chaos"] = _chaos; // #1 chaos
53
- if (_devBackend)
54
- headers["x-fluxe-backend"] = _devBackend; // #5 live swap
55
50
  let res;
56
51
  try {
57
52
  res = await fetch(`/__action/${cell}/${action}`, { method: "POST", headers, body: JSON.stringify(input) });
@@ -3,5 +3,3 @@ export interface Schema {
3
3
  types: Record<string, Record<string, FieldType>>;
4
4
  }
5
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;
@@ -1,11 +1,7 @@
1
1
  // Copyright (c) 2026 nmvuong92
2
2
  // SPDX-License-Identifier: Apache-2.0
3
- /* Codegen contract polyglot — MỘT schema → types TS + Go + Rust (chữ ký RCA:
4
- * type-safe xuyên ngôn ngữ). Thuần (string-in/string-out), dễ test. */
5
- const pascal = (s) => s.charAt(0).toUpperCase() + s.slice(1);
3
+ /* Codegen contract — MỘT schema → types TS. Thuần (string-in/string-out), dễ test. */
6
4
  const TS = { string: "string", bool: "boolean", int: "number" };
7
- const GO = { string: "string", bool: "bool", int: "int" };
8
- const RS = { string: "String", bool: "bool", int: "i64" };
9
5
  export function genTS(s) {
10
6
  return Object.entries(s.types)
11
7
  .map(([name, fields]) => `export interface ${name} {\n` +
@@ -13,18 +9,3 @@ export function genTS(s) {
13
9
  `\n}`)
14
10
  .join("\n\n") + "\n";
15
11
  }
16
- export function genGo(s, pkg = "contract") {
17
- return `package ${pkg}\n\n` +
18
- Object.entries(s.types)
19
- .map(([name, fields]) => `type ${name} struct {\n` +
20
- Object.entries(fields).map(([f, t]) => `\t${pascal(f)} ${GO[t]} \`json:"${f}"\``).join("\n") +
21
- `\n}`)
22
- .join("\n\n") + "\n";
23
- }
24
- export function genRust(s) {
25
- return Object.entries(s.types)
26
- .map(([name, fields]) => `#[derive(Clone, Debug)]\npub struct ${name} {\n` +
27
- Object.entries(fields).map(([f, t]) => ` pub ${f}: ${RS[t]},`).join("\n") +
28
- `\n}`)
29
- .join("\n\n") + "\n";
30
- }
@@ -3,7 +3,7 @@ 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", "go", "rust"]>;
6
+ defaultBackend: z.ZodEnum<["memory", "sqlite", "postgres"]>;
7
7
  rateLimit: z.ZodObject<{
8
8
  capacity: z.ZodNumber;
9
9
  refillPerSec: z.ZodNumber;
@@ -42,7 +42,7 @@ declare const Schema: z.ZodObject<{
42
42
  env: "development" | "production" | "test";
43
43
  secret: string;
44
44
  port: number;
45
- defaultBackend: "memory" | "go" | "rust";
45
+ defaultBackend: "memory" | "sqlite" | "postgres";
46
46
  rateLimit: {
47
47
  capacity: number;
48
48
  refillPerSec: number;
@@ -61,7 +61,7 @@ declare const Schema: z.ZodObject<{
61
61
  env: "development" | "production" | "test";
62
62
  secret: string;
63
63
  port: number;
64
- defaultBackend: "memory" | "go" | "rust";
64
+ defaultBackend: "memory" | "sqlite" | "postgres";
65
65
  rateLimit: {
66
66
  capacity: number;
67
67
  refillPerSec: number;
@@ -9,7 +9,7 @@ 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", "go", "rust"]),
12
+ defaultBackend: z.enum(["memory", "sqlite", "postgres"]),
13
13
  rateLimit: z.object({
14
14
  capacity: z.coerce.number().int().positive(),
15
15
  refillPerSec: z.coerce.number().positive(),
@@ -1,31 +1,30 @@
1
1
  import type { ComponentType } from "react";
2
- import type { Backend } from "../backends/types";
3
2
  import type { HeadMeta } from "./seo";
4
3
  import type { Session } from "./auth";
5
4
  import type { TFn } from "./i18n";
6
- export interface Ctx<I> {
5
+ export interface Ctx<I, B = any> {
7
6
  input: I;
8
- backend: Backend;
7
+ backend: B;
9
8
  session?: Session | null;
10
9
  locale?: string;
11
10
  t?: TFn;
12
11
  }
13
- export type Loader<I, O> = (ctx: Ctx<I>) => Promise<O>;
14
- export type Action<I, O> = (ctx: Ctx<I>) => Promise<O>;
12
+ export type Loader<I, O, B = any> = (ctx: Ctx<I, B>) => Promise<O>;
13
+ export type Action<I, O, B = any> = (ctx: Ctx<I, B>) => Promise<O>;
15
14
  export type Hydration = "static" | "island";
16
- export interface CellDef<I, O> {
15
+ export interface CellDef<I, O, B = any> {
17
16
  id: string;
18
17
  route: string;
19
18
  hydration?: Hydration;
20
- loader: Loader<I, O>;
19
+ loader: Loader<I, O, B>;
21
20
  view: ComponentType<{
22
21
  data: O;
23
22
  }>;
24
- actions?: Record<string, Action<any, any>>;
23
+ actions?: Record<string, Action<any, any, B>>;
25
24
  head?: (data: O) => HeadMeta;
26
25
  layout?: string;
27
26
  requireAuth?: boolean;
28
27
  requireRole?: string;
29
28
  cache?: boolean;
30
29
  }
31
- export declare function defineCell<I, O>(c: CellDef<I, O>): CellDef<I, O>;
30
+ export declare function defineCell<I, O, B = any>(c: CellDef<I, O, B>): CellDef<I, O, B>;
package/lib/core/panel.js CHANGED
@@ -9,8 +9,6 @@ export function renderResolutionPanel(m, requests = []) {
9
9
  <td>${c.render.mode}</td>
10
10
  <td>${c.render.shipClientJs ? "✓ JS" : "0 JS"}</td>
11
11
  <td><span class="badge ${c.backend.language}">${c.backend.language}</span></td>
12
- <td>${c.backend.transport}</td>
13
- <td>${c.backend.endpoint ?? "—"}</td>
14
12
  </tr>`).join("");
15
13
  return `<!doctype html><html lang="vi"><head><meta charset="utf-8"><title>fluxe — RCA Resolution</title>
16
14
  <style>
@@ -22,13 +20,13 @@ export function renderResolutionPanel(m, requests = []) {
22
20
  th{color:#8a8aa0;font-weight:600;font-size:.78rem;text-transform:uppercase;letter-spacing:.04em}
23
21
  code{background:#1a1a2e;padding:.1rem .35rem;border-radius:4px;color:#a0a0ff}
24
22
  .badge{display:inline-block;padding:.1rem .55rem;border-radius:99px;font-size:.78rem}
25
- .go{background:#00add833;color:#5fd3f0}.rust{background:#dea58433;color:#e8b48f}.memory{background:#7c7cff33;color:#a0a0ff}
23
+ .memory{background:#7c7cff33;color:#a0a0ff}.sqlite{background:#00add833;color:#5fd3f0}.postgres{background:#dea58433;color:#e8b48f}
26
24
  </style></head>
27
25
  <body>
28
26
  <h1>RCA Resolution</h1>
29
- <div class="sub">profile <b>${m.profile}</b> · default backend <b>${m.backend.language}</b> (${m.backend.transport})</div>
27
+ <div class="sub">profile <b>${m.profile}</b> · default backend <b>${m.backend.language}</b> (in-process)</div>
30
28
  <table>
31
- <thead><tr><th>cell</th><th>route</th><th>render</th><th>JS</th><th>backend</th><th>transport</th><th>endpoint</th></tr></thead>
29
+ <thead><tr><th>cell</th><th>route</th><th>render</th><th>JS</th><th>backend</th></tr></thead>
32
30
  <tbody>${rows}
33
31
  </tbody>
34
32
  </table>
@@ -1,5 +1,5 @@
1
1
  export type RenderMode = "static" | "island";
2
- export type BackendKind = "memory" | "go" | "rust";
2
+ export type BackendKind = "memory" | "sqlite" | "postgres";
3
3
  export interface CellDecl {
4
4
  id: string;
5
5
  route: string;
@@ -8,16 +8,10 @@ export interface CellDecl {
8
8
  export interface ResolutionProfile {
9
9
  name: string;
10
10
  backend: BackendKind;
11
- endpoints?: {
12
- go?: string;
13
- rust?: string;
14
- };
15
11
  cellBackends?: Record<string, BackendKind>;
16
12
  }
17
13
  export interface BackendResolution {
18
14
  language: BackendKind;
19
- transport: "in-process" | "http";
20
- endpoint?: string;
21
15
  }
22
16
  export interface CellResolution {
23
17
  id: string;
@@ -1,17 +1,11 @@
1
- const ALLOWED = ["memory", "go", "rust"];
2
- // Giải một BackendKind → BackendResolution (validate endpoint nếu http). Dùng chung
3
- // cho default app-level lẫn override per-cell.
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
4
  function resolveBackend(kind, profile) {
5
5
  if (!ALLOWED.includes(kind)) {
6
6
  throw new Error(`profile "${profile.name}": backend không hợp lệ: ${kind}`);
7
7
  }
8
- if (kind === "memory")
9
- return { language: "memory", transport: "in-process" };
10
- const endpoint = profile.endpoints?.[kind];
11
- if (!endpoint) {
12
- throw new Error(`profile "${profile.name}": backend "${kind}" cần endpoints.${kind}`);
13
- }
14
- return { language: kind, transport: "http", endpoint };
8
+ return { language: kind };
15
9
  }
16
10
  export function resolve(cells, profile) {
17
11
  const ids = new Set(cells.map((c) => c.id));
@@ -1,22 +1,24 @@
1
1
  import { createMemoryBackend } from "../backends/memory.js";
2
- import { createHttpBackend } from "../backends/http.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.)
3
5
  function buildBackend(b) {
4
6
  if (b.language === "memory")
5
7
  return createMemoryBackend();
6
- if (!b.endpoint)
7
- throw new Error(`manifest backend "${b.language}" thiếu endpoint`);
8
- return createHttpBackend(b.language, b.endpoint);
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)`);
9
11
  }
10
12
  // Backend app-level (default) — giữ cho code cũ/đơn giản.
11
13
  export function backendFromManifest(m) {
12
14
  return buildBackend(m.backend);
13
15
  }
14
- // Dựng backend per-cell, DEDUP theo key `language:endpoint` → cells cùng resolution
16
+ // Dựng backend per-cell, DEDUP theo `language` → cells cùng resolution
15
17
  // chia sẻ MỘT instance (vd memory dùng chung một store).
16
18
  export function backendsFromManifest(m) {
17
19
  const cache = new Map();
18
20
  const make = (b) => {
19
- const key = `${b.language}:${b.endpoint ?? ""}`;
21
+ const key = b.language;
20
22
  let inst = cache.get(key);
21
23
  if (!inst) {
22
24
  inst = buildBackend(b);
package/lib/index.d.ts CHANGED
@@ -22,6 +22,6 @@ export { createMemoryStorage } from "./storage/memory.ts";
22
22
  export { createLocalStorage } from "./storage/local.ts";
23
23
  export { createS3Storage } from "./storage/s3.ts";
24
24
  export { createMemoryBackend } from "./backends/memory.ts";
25
- export { createHttpBackend } from "./backends/http.ts";
25
+ export { createSqliteBackend } from "./backends/sqlite.ts";
26
26
  export { createPostgresBackend } from "./backends/postgres.ts";
27
27
  export { makeServer } from "./server_factory.ts";
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ export * from "./core/seo.js"; // renderHead, renderSitemap, renderRobots, HeadM
16
16
  export * from "./core/broker.js"; // pub/sub
17
17
  export * from "./core/presence.js"; // ai online theo topic
18
18
  export * from "./core/ratelimit.js"; // token-bucket + LRU
19
- export * from "./core/codegen.js"; // genTS/genGo/genRust
19
+ export * from "./core/codegen.js"; // genTS
20
20
  export * from "./core/layouts.js"; // layoutChain, LayoutMeta
21
21
  export * from "./core/router.js"; // makeRouter
22
22
  export * from "./core/testing.js"; // createTestBackend
@@ -26,6 +26,6 @@ export { createMemoryStorage } from "./storage/memory.js";
26
26
  export { createLocalStorage } from "./storage/local.js";
27
27
  export { createS3Storage } from "./storage/s3.js"; // adapter tham chiếu (cần @aws-sdk/client-s3)
28
28
  export { createMemoryBackend } from "./backends/memory.js";
29
- export { createHttpBackend } from "./backends/http.js";
30
- export { createPostgresBackend } from "./backends/postgres.js";
29
+ export { createSqliteBackend } from "./backends/sqlite.js";
30
+ export { createPostgresBackend } from "./backends/postgres.js"; // user tự inject client `pg`
31
31
  export { makeServer } from "./server_factory.js";
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { createElement as h, useState, useSyncExternalStore } from "react";
4
4
  import { debug } from "./store";
5
- import { setChaos, getChaos, setDevBackend, getDevBackend } from "../core/client";
5
+ import { setChaos, getChaos } from "../core/client";
6
6
  import { reproTest } from "./repro";
7
7
  const EMPTY = [];
8
8
  const DOT = { pending: "#e3b341", ok: "#3fb950", error: "#f85149" };
@@ -29,9 +29,8 @@ export function DebugBar() {
29
29
  }, `⚡ fluxe · ${events.length}${errs ? ` · ${errs}✗` : ""}`));
30
30
  }
31
31
  const chaosOn = getChaos() !== "";
32
- const be = getDevBackend();
33
- // #1 Chaos toggle + #5 backend swap
34
- const controls = h("div", { style: { display: "flex", gap: 6, alignItems: "center", padding: "6px 10px", background: "#0d1117", borderBottom: "1px solid #21262d", flexWrap: "wrap" } }, h("button", { onClick: () => { setChaos(chaosOn ? "" : CHAOS); force((x) => x + 1); }, style: ctrlBtn(chaosOn), title: CHAOS }, chaosOn ? "🔥 Chaos ON" : "Chaos"), h("span", { style: { color: "#7d8590" } }, "backend:"), ...["", "memory", "go", "rust"].map((v) => h("button", { key: v || "auto", onClick: () => { setDevBackend(v); force((x) => x + 1); }, style: ctrlBtn(be === v) }, v || "auto")));
32
+ // #1 Chaos toggle
33
+ const controls = h("div", { style: { display: "flex", gap: 6, alignItems: "center", padding: "6px 10px", background: "#0d1117", borderBottom: "1px solid #21262d", flexWrap: "wrap" } }, h("button", { onClick: () => { setChaos(chaosOn ? "" : CHAOS); force((x) => x + 1); }, style: ctrlBtn(chaosOn), title: CHAOS }, chaosOn ? "🔥 Chaos ON" : "Chaos"));
35
34
  const rows = events.length === 0
36
35
  ? [h("div", { key: "e", style: { color: "#7d8590", padding: 8 } }, "Tương tác đi — query/mutation sẽ hiện ở đây.")]
37
36
  : events.map((e) => h("div", { key: e.id, onClick: () => { setSel(sel === e.id ? null : e.id); setCopied(false); },
@@ -15,5 +15,6 @@ export declare function makeServer(manifest: ResolutionManifest, cells: CellDef<
15
15
  i18n?: I18n;
16
16
  storage?: Storage;
17
17
  config?: FluxeConfig;
18
+ backend?: unknown;
18
19
  }): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
19
20
  export {};
@@ -25,22 +25,6 @@ import { resolveLocale, makeT } from "./core/i18n.js";
25
25
  import { parseMultipart, boundaryFromContentType } from "./core/multipart.js";
26
26
  import { makeKey } from "./storage/types.js";
27
27
  import { loadConfig } from "./core/config.js";
28
- import { createMemoryBackend } from "./backends/memory.js";
29
- import { createHttpBackend } from "./backends/http.js";
30
- // Build backend theo ngôn ngữ (cho live swap trong devtools).
31
- // Map ngôn ngữ → service HTTP demo (cùng hợp đồng list/add/toggle). Override bằng <LANG>_URL.
32
- const DEV_BACKENDS = {
33
- go: "http://127.0.0.1:8081",
34
- rust: "http://127.0.0.1:8082",
35
- python: "http://127.0.0.1:8083",
36
- hono: "http://127.0.0.1:8084",
37
- dotnet: "http://127.0.0.1:8085",
38
- java: "http://127.0.0.1:8086",
39
- };
40
- function devBackend(lang) {
41
- const url = process.env[`${lang.toUpperCase()}_URL`] ?? DEV_BACKENDS[lang];
42
- return url ? createHttpBackend(lang, url) : createMemoryBackend();
43
- }
44
28
  import { randomUUID, randomBytes } from "node:crypto";
45
29
  const DEV = process.env.NODE_ENV !== "production";
46
30
  const SECRET = process.env.FLUXE_SECRET ?? "dev-secret-change-me";
@@ -114,9 +98,11 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
114
98
  // Cells được TIÊM từ app (DI) — engine không import ngược vào app/. Thêm trang = sửa app/app.ts.
115
99
  const matchRoute = makeRouter(cells);
116
100
  const byId = new Map(cells.map((c) => [c.id, c]));
117
- // Backend GIẢI per-cell từ manifest (Resolution Plane) cell/frontend giữ nguyên.
118
- const backends = backendsFromManifest(manifest);
119
- const backendFor = (id) => backends.byCell.get(id) ?? backends.default;
101
+ // Backend USER-OWNED (app/backend.ts) inject qua opts.backend dùng cho mọi cell.
102
+ // Nếu app không truyền → fallback driver built-in giải từ manifest (memory/sqlite) cho quick-start.
103
+ const userBackend = opts.backend;
104
+ const backends = userBackend === undefined ? backendsFromManifest(manifest) : null;
105
+ const backendFor = (id) => userBackend ?? backends.byCell.get(id) ?? backends.default;
120
106
  // Resolved Container: service realtime đăng ký LƯỜI — chỉ tạo khi thật sự dùng (SSE/action).
121
107
  // App không realtime → broker/presence KHÔNG bao giờ bootstrap. resolved() ở /_fluxe/stats.
122
108
  const container = createContainer();
@@ -286,15 +272,10 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
286
272
  return res.end("no action");
287
273
  }
288
274
  const t0 = Date.now();
289
- // DevTools (DEV): #5 live backend swap + #3 resolution.
290
- let backend = backendFor(cellId);
275
+ // DevTools (DEV): #3 resolution (backend TS in-process do manifest giải).
276
+ const backend = backendFor(cellId);
291
277
  const r = manifest.cells[cellId]?.backend;
292
- let resolution = r ? `${r.language}/${r.transport}` : "memory/in-process";
293
- if (DEV && req.headers["x-fluxe-backend"]) {
294
- const lang = String(req.headers["x-fluxe-backend"]);
295
- backend = devBackend(lang);
296
- resolution = `${lang}/${lang === "memory" ? "in-process" : "http"} (swap)`;
297
- }
278
+ const resolution = r ? `${r.language}/in-process` : "memory/in-process";
298
279
  // #1 Chaos (DEV): inject delay + lỗi giả lập để test UX.
299
280
  if (DEV && req.headers["x-fluxe-chaos"]) {
300
281
  const c = parseChaos(String(req.headers["x-fluxe-chaos"]));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nmvuong92/fluxe",
3
- "version": "0.5.0",
4
- "description": "fluxe — khung fullstack tối giản polyglot (RCA: Resolved Cell Architecture).",
3
+ "version": "0.7.0",
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",
7
7
  "type": "module",
@@ -1,2 +0,0 @@
1
- import type { Backend } from "./types";
2
- export declare function createHttpBackend(name: string, baseUrl: string): Backend;
@@ -1,32 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
1
- export interface SearchHit {
2
- item: string;
3
- score: number;
4
- }
5
- export interface SearchService {
6
- name: string;
7
- search(items: string[], query: string): Promise<SearchHit[]>;
8
- }
9
- export declare function createRustSearch(baseUrl: string): SearchService;
package/lib/hot/search.js DELETED
@@ -1,16 +0,0 @@
1
- export function createRustSearch(baseUrl) {
2
- const base = baseUrl.replace(/\/$/, "");
3
- return {
4
- name: "rust-hot",
5
- async search(items, query) {
6
- const r = await fetch(`${base}/search?q=${encodeURIComponent(query)}`, {
7
- method: "POST",
8
- headers: { "content-type": "text/plain" },
9
- body: items.join("\n"),
10
- });
11
- if (!r.ok)
12
- throw new Error(`hot search → HTTP ${r.status}`);
13
- return (await r.json());
14
- },
15
- };
16
- }