@nmvuong92/fluxe 0.4.0 → 0.6.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(),
@@ -0,0 +1,9 @@
1
+ export type Factory<T> = (c: Container) => T;
2
+ export interface Container {
3
+ register<T>(token: string, factory: Factory<T>): Container;
4
+ override<T>(token: string, factory: Factory<T>): Container;
5
+ has(token: string): boolean;
6
+ get<T>(token: string): T;
7
+ resolved(): string[];
8
+ }
9
+ export declare function createContainer(): Container;
@@ -0,0 +1,46 @@
1
+ // Copyright (c) 2026 nmvuong92
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /* Resolved Container — DI lười (lazy singleton). Register factory = O(1), KHÔNG instantiate;
4
+ * get() tạo lần đầu rồi memoize. Factory tự c.get(dep) → DI + thứ tự init tự nhiên (DFS) +
5
+ * phát hiện vòng (cycle). resolved() liệt kê token đã tạo → "chỉ module dùng mới bootstrap".
6
+ * DSA: Map provider + Map instance (O(1)); Set "đang giải" (cycle, O(depth)). */
7
+ export function createContainer() {
8
+ const providers = new Map();
9
+ const instances = new Map();
10
+ const resolving = new Set(); // DFS đang giải → bắt cycle
11
+ const c = {
12
+ register(token, factory) {
13
+ if (providers.has(token))
14
+ throw new Error(`Container: token '${token}' đã đăng ký (dùng override để ghi đè)`);
15
+ providers.set(token, factory);
16
+ return c;
17
+ },
18
+ override(token, factory) {
19
+ providers.set(token, factory);
20
+ instances.delete(token); // buộc tạo lại lần get sau
21
+ return c;
22
+ },
23
+ has: (token) => providers.has(token),
24
+ get(token) {
25
+ if (instances.has(token))
26
+ return instances.get(token); // memoized singleton
27
+ const f = providers.get(token);
28
+ if (!f)
29
+ throw new Error(`Container: chưa đăng ký token '${token}'`);
30
+ if (resolving.has(token)) {
31
+ throw new Error(`Container: phụ thuộc vòng (cycle) tại '${token}' — chuỗi: ${[...resolving, token].join(" → ")}`);
32
+ }
33
+ resolving.add(token);
34
+ try {
35
+ const inst = f(c); // factory có thể c.get(dep) → DI lười, thứ tự tự nhiên
36
+ instances.set(token, inst);
37
+ return inst;
38
+ }
39
+ finally {
40
+ resolving.delete(token);
41
+ }
42
+ },
43
+ resolved: () => [...instances.keys()],
44
+ };
45
+ return c;
46
+ }
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
@@ -6,6 +6,7 @@ export * from "./core/wiring.ts";
6
6
  export * from "./core/auth.ts";
7
7
  export * from "./core/env.ts";
8
8
  export * from "./core/config.ts";
9
+ export * from "./core/container.ts";
9
10
  export * from "./core/i18n.ts";
10
11
  export * from "./core/seo.ts";
11
12
  export * from "./core/broker.ts";
@@ -21,6 +22,6 @@ export { createMemoryStorage } from "./storage/memory.ts";
21
22
  export { createLocalStorage } from "./storage/local.ts";
22
23
  export { createS3Storage } from "./storage/s3.ts";
23
24
  export { createMemoryBackend } from "./backends/memory.ts";
24
- export { createHttpBackend } from "./backends/http.ts";
25
+ export { createSqliteBackend } from "./backends/sqlite.ts";
25
26
  export { createPostgresBackend } from "./backends/postgres.ts";
26
27
  export { makeServer } from "./server_factory.ts";
package/lib/index.js CHANGED
@@ -10,12 +10,13 @@ export * from "./core/wiring.js"; // backendFromManifest, backendsFromManifest
10
10
  export * from "./core/auth.js"; // session HMAC, scrypt password, CSRF, RBAC
11
11
  export * from "./core/env.js"; // loadEnv
12
12
  export * from "./core/config.js"; // FluxeConfig, loadConfig (default ← ENV FLUXE_* ← override)
13
+ export * from "./core/container.js"; // createContainer — Resolved Container (DI lười, chỉ-used-bootstrap)
13
14
  export * from "./core/i18n.js"; // createI18n, resolveLocale, translate, makeT, t(key, vars)
14
15
  export * from "./core/seo.js"; // renderHead, renderSitemap, renderRobots, HeadMeta
15
16
  export * from "./core/broker.js"; // pub/sub
16
17
  export * from "./core/presence.js"; // ai online theo topic
17
18
  export * from "./core/ratelimit.js"; // token-bucket + LRU
18
- export * from "./core/codegen.js"; // genTS/genGo/genRust
19
+ export * from "./core/codegen.js"; // genTS
19
20
  export * from "./core/layouts.js"; // layoutChain, LayoutMeta
20
21
  export * from "./core/router.js"; // makeRouter
21
22
  export * from "./core/testing.js"; // createTestBackend
@@ -25,6 +26,6 @@ export { createMemoryStorage } from "./storage/memory.js";
25
26
  export { createLocalStorage } from "./storage/local.js";
26
27
  export { createS3Storage } from "./storage/s3.js"; // adapter tham chiếu (cần @aws-sdk/client-s3)
27
28
  export { createMemoryBackend } from "./backends/memory.js";
28
- export { createHttpBackend } from "./backends/http.js";
29
- 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`
30
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); },
@@ -14,6 +14,7 @@ import { FluxeError, toErrorPayload, renderErrorPage } from "./core/errors.js";
14
14
  import { signSession, verifySession, parseCookie, hasRole, hashPassword, verifyPassword, newCsrfToken } from "./core/auth.js";
15
15
  import { validateInput } from "./core/validate.js";
16
16
  import { createBroker } from "./core/broker.js";
17
+ import { createContainer } from "./core/container.js";
17
18
  import { createRateLimiter } from "./core/ratelimit.js";
18
19
  import { createRecorder } from "./core/observe.js";
19
20
  import { createPresence } from "./core/presence.js";
@@ -24,22 +25,6 @@ import { resolveLocale, makeT } from "./core/i18n.js";
24
25
  import { parseMultipart, boundaryFromContentType } from "./core/multipart.js";
25
26
  import { makeKey } from "./storage/types.js";
26
27
  import { loadConfig } from "./core/config.js";
27
- import { createMemoryBackend } from "./backends/memory.js";
28
- import { createHttpBackend } from "./backends/http.js";
29
- // Build backend theo ngôn ngữ (cho live swap trong devtools).
30
- // Map ngôn ngữ → service HTTP demo (cùng hợp đồng list/add/toggle). Override bằng <LANG>_URL.
31
- const DEV_BACKENDS = {
32
- go: "http://127.0.0.1:8081",
33
- rust: "http://127.0.0.1:8082",
34
- python: "http://127.0.0.1:8083",
35
- hono: "http://127.0.0.1:8084",
36
- dotnet: "http://127.0.0.1:8085",
37
- java: "http://127.0.0.1:8086",
38
- };
39
- function devBackend(lang) {
40
- const url = process.env[`${lang.toUpperCase()}_URL`] ?? DEV_BACKENDS[lang];
41
- return url ? createHttpBackend(lang, url) : createMemoryBackend();
42
- }
43
28
  import { randomUUID, randomBytes } from "node:crypto";
44
29
  const DEV = process.env.NODE_ENV !== "production";
45
30
  const SECRET = process.env.FLUXE_SECRET ?? "dev-secret-change-me";
@@ -116,10 +101,13 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
116
101
  // Backend GIẢI per-cell từ manifest (Resolution Plane) — cell/frontend giữ nguyên.
117
102
  const backends = backendsFromManifest(manifest);
118
103
  const backendFor = (id) => backends.byCell.get(id) ?? backends.default;
119
- const broker = createBroker(); // realtime pub/sub (Trục 4g, bản 1-node)
104
+ // Resolved Container: service realtime đăng LƯỜI chỉ tạo khi thật sự dùng (SSE/action).
105
+ // App không realtime → broker/presence KHÔNG bao giờ bootstrap. resolved() ở /_fluxe/stats.
106
+ const container = createContainer();
107
+ container.register("broker", () => createBroker());
108
+ container.register("presence", () => createPresence());
120
109
  const actionLimit = createRateLimiter(config.rateLimit); // per-IP cho action (FLUXE_RATELIMIT_*)
121
- const recorder = createRecorder(); // request log (observability)
122
- const presence = createPresence(); // ai đang online per topic (Trục 4g)
110
+ const recorder = createRecorder(); // request log — chạy mỗi request → eager (luôn dùng)
123
111
  const renderCache = createRenderCache({ maxKeys: config.renderCache.maxKeys }); // FLUXE_RENDERCACHE_MAX_KEYS
124
112
  let clientJs; // ý A: đọc dist/client.js 1 lần (zero-copy: tái dùng buffer)
125
113
  return http.createServer(async (req, res) => {
@@ -160,7 +148,7 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
160
148
  const m = process.memoryUsage();
161
149
  const c = process.cpuUsage();
162
150
  res.writeHead(200, { "content-type": "application/json" });
163
- return res.end(JSON.stringify({ rss: m.rss, heapUsed: m.heapUsed, cpuUser: c.user, cpuSystem: c.system, uptimeMs: Math.round(process.uptime() * 1000) }));
151
+ return res.end(JSON.stringify({ rss: m.rss, heapUsed: m.heapUsed, cpuUser: c.user, cpuSystem: c.system, uptimeMs: Math.round(process.uptime() * 1000), bootstrapped: container.resolved() }));
164
152
  }
165
153
  if (url.pathname === "/_fluxe/requests") {
166
154
  // Observability: log request gần đây (timing/status). Prod: gate sau auth.
@@ -246,6 +234,9 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
246
234
  const id = url.searchParams.get("id");
247
235
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
248
236
  res.write(`event: ready\ndata: {"topic":"${topic}"}\n\n`);
237
+ // Lần đầu có client SSE → broker + presence mới bootstrap (lazy qua container).
238
+ const broker = container.get("broker");
239
+ const presence = container.get("presence");
249
240
  const offBroker = broker.subscribe(topic, (data) => res.write(`data: ${JSON.stringify(data)}\n\n`));
250
241
  let offPresence = () => { };
251
242
  if (id) {
@@ -279,15 +270,10 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
279
270
  return res.end("no action");
280
271
  }
281
272
  const t0 = Date.now();
282
- // DevTools (DEV): #5 live backend swap + #3 resolution.
283
- let backend = backendFor(cellId);
273
+ // DevTools (DEV): #3 resolution (backend TS in-process do manifest giải).
274
+ const backend = backendFor(cellId);
284
275
  const r = manifest.cells[cellId]?.backend;
285
- let resolution = r ? `${r.language}/${r.transport}` : "memory/in-process";
286
- if (DEV && req.headers["x-fluxe-backend"]) {
287
- const lang = String(req.headers["x-fluxe-backend"]);
288
- backend = devBackend(lang);
289
- resolution = `${lang}/${lang === "memory" ? "in-process" : "http"} (swap)`;
290
- }
276
+ const resolution = r ? `${r.language}/in-process` : "memory/in-process";
291
277
  // #1 Chaos (DEV): inject delay + lỗi giả lập để test UX.
292
278
  if (DEV && req.headers["x-fluxe-chaos"]) {
293
279
  const c = parseChaos(String(req.headers["x-fluxe-chaos"]));
@@ -301,7 +287,7 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
301
287
  if (schema)
302
288
  input = validateInput(schema, input); // sai → FluxeError 400 (caught)
303
289
  const out = await fn({ input, backend, session });
304
- broker.publish(cellId, { action: name, out }); // realtime: báo client khác
290
+ container.get("broker").publish(cellId, { action: name, out }); // realtime: báo client khác (broker lazy)
305
291
  res.writeHead(200, { "content-type": "application/json", "x-fluxe-resolution": resolution, "x-fluxe-server-ms": String(Date.now() - t0) });
306
292
  return res.end(JSON.stringify(out));
307
293
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nmvuong92/fluxe",
3
- "version": "0.4.0",
4
- "description": "fluxe — khung fullstack tối giản polyglot (RCA: Resolved Cell Architecture).",
3
+ "version": "0.6.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
- }