@nmvuong92/fluxe 0.7.0 → 0.8.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
@@ -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`, đổi render `static island`, gộp nhiều backend
12
- > trong một app per-celltất cả chỉ sửa `app/profiles.ts`, **cell & frontend không đổi một dòng**.
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, createMemoryBackend } from "@nmvuong92/fluxe";
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, backends, seo, broker, ratelimit, codegen… |
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` · `/sqlite` | queue/dead-letter · SQLite backend (cần `--experimental-sqlite`) |
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 + 107 unit + 28 integration — TẤT CẢ XANH
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,11 @@ 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 data per môi trường + per-cell (memory/sqlite/postgres)
50
+ backend.ts TẦNG DATA của bạn: interface domain + chọn driver (memory/sqlite/postgres)
51
+ profiles.ts profile resolve render mode (static/island) per môi trường
51
52
  contract.ts schema → codegen types TS
52
53
  env.ts env có kiểu, validate fail-fast lúc boot
54
+ app.ts registry cell — sinh tự động (fx sync), đừng sửa
53
55
 
54
56
  src/ ← ENGINE (không đụng) — Resolution Plane
55
57
  core/ resolver · router · errors · auth · validate · codegen · layouts ·
@@ -57,15 +59,15 @@ src/ ← ENGINE (không đụng) — Resolution Plane
57
59
  server_factory.ts runtime ráp cell + giải manifest
58
60
  ```
59
61
 
60
- **Quy tắc:** "backend nào đang chạy" do `app/profiles.ts` (config) quyết, **không** do vị trí
61
- folder. Engine không bao giờ import ngược vào `app/`.
62
+ **Quy tắc:** backend **tầng data của bạn** ở `app/backend.ts` (interface domain + chọn driver),
63
+ inject qua `makeServer(…, { backend })`. Engine không bao giờ import ngược vào `app/`.
62
64
 
63
65
  ## Tính năng (tất cả TDD + chạy thật)
64
66
 
65
67
  - **Render** — static (0 JS) · island hydrate · SPA nav (Inertia) · static-prerender · API mode `?json=1`
66
68
  - **Routing** — động `[param]` → `ctx.input` · **nested layouts** · SEO (head/canonical/OG/JSON-LD per cell, `/sitemap.xml`, `/robots.txt`)
67
69
  - **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 TS per-cell (`memory`/`sqlite`/`postgres`) · DB **SQLite thật** (`node:sqlite`) + **Postgres** (inject client `pg`) · **codegen contract** 1 schema → types TS
70
+ - **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
71
  - **Mutations DX** — `RpcError` có cấu trúc · `mutate()` optimistic + rollback · lỗi validation field-level
70
72
  - **Realtime (Trục 4g)** — **SSE channel** + pub/sub broker · live-update on action · **presence** (multi-tab)
71
73
  - **Async** — **job queue bền** (SQLite, retry → dead-letter)
@@ -73,24 +75,54 @@ folder. Engine không bao giờ import ngược vào `app/`.
73
75
  - **Config** — env có kiểu, fail-fast lúc boot
74
76
  - **DX** — `fx` CLI · mock `Backend` test cực dễ · typecheck gate
75
77
 
76
- ## Backend data (driver TS in-process)
78
+ ## Backend data (user-owned, TS in-process)
77
79
 
78
- Backend chỉ là **driver data TypeScript in-process** — chọn qua `app/profiles.ts`, cell không đổi:
80
+ Backend là **tầng data của bạn** — định nghĩa interface domain ở `app/backend.ts` + chọn driver
81
+ **TS in-process** dưới đây, inject qua `makeServer(…, { backend })`. Cell chỉ thấy interface:
79
82
 
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`) |
83
+ | Driver | Bạn tự implement bằng |
84
+ |--------|------------------------|
85
+ | `memory` | object in-RAM mặc định dev |
86
+ | `sqlite` | `node:sqlite` built-in (`DatabaseSync`), 0 dep, persist ra file (cần `--experimental-sqlite`) |
87
+ | `postgres` | `npm i pg`, dùng `Pool`/`Client` trực tiếp (`DATABASE_URL`) |
85
88
 
86
89
  ```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
- };
90
+ // app/backend.ts engine không biết gì
91
+ import { DatabaseSync } from "node:sqlite";
92
+
93
+ export interface Todo { id: string; title: string; done: boolean }
94
+ export interface Backend {
95
+ name: string;
96
+ listTodos(): Promise<Todo[]>;
97
+ addTodo(title: string): Promise<Todo>;
98
+ toggleTodo(id: string): Promise<Todo[]>;
99
+ }
100
+
101
+ export function memoryBackend(): Backend {
102
+ let items: Todo[] = [];
103
+ let seq = 0;
104
+ return {
105
+ name: "memory",
106
+ async listTodos() { return items; },
107
+ async addTodo(title) { const t = { id: String(++seq), title, done: false }; items.push(t); return t; },
108
+ async toggleTodo(id) { items = items.map((t) => t.id === id ? { ...t, done: !t.done } : t); return items; },
109
+ };
110
+ }
111
+
112
+ export function sqliteBackend(path = ":memory:"): Backend {
113
+ const db = new DatabaseSync(path);
114
+ db.exec(`CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, done INTEGER DEFAULT 0)`);
115
+ // … CRUD bằng db.prepare(...)
116
+ return { name: "sqlite" } as any;
117
+ }
118
+
119
+ export const backend: Backend = process.env.FLUXE_SQLITE_PATH
120
+ ? sqliteBackend(process.env.FLUXE_SQLITE_PATH) // đổi 1 dòng = đổi nơi lưu
121
+ : memoryBackend();
92
122
  ```
93
123
 
124
+ Nguồn khác (REST/ORM/Redis)? Tự implement `Backend` là xong — engine không quan tâm bên dưới.
125
+
94
126
  ## Triết lý
95
127
 
96
128
  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)**.
@@ -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";
@@ -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> · default backend <b>${m.backend.language}</b> (in-process)</div>
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><th>backend</th></tr></thead>
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>
@@ -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;
@@ -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, backend, cells: out };
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 { createMemoryBackend } from "./backends/memory.ts";
25
- export { createSqliteBackend } from "./backends/sqlite.ts";
26
- export { createPostgresBackend } from "./backends/postgres.ts";
27
21
  export { makeServer } 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 { createMemoryBackend } from "./backends/memory.js";
29
- export { createSqliteBackend } from "./backends/sqlite.js";
30
- export { createPostgresBackend } from "./backends/postgres.js"; // user tự inject client `pg`
31
25
  export { makeServer } from "./server_factory.js";
26
+ // Backend = USER-OWNED (app/backend.ts) — engine KHÔNG ship driver/domain data nào.
@@ -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";
@@ -99,10 +98,8 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
99
98
  const matchRoute = makeRouter(cells);
100
99
  const byId = new Map(cells.map((c) => [c.id, c]));
101
100
  // 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;
101
+ // Engine KHÔNG ship/giải backend; cell nào dùng backend app quên truyền → undefined (lỗi ràng).
102
+ const backendFor = (_id) => opts.backend;
106
103
  // Resolved Container: service realtime đăng ký LƯỜI — chỉ tạo khi thật sự dùng (SSE/action).
107
104
  // App không realtime → broker/presence KHÔNG bao giờ bootstrap. resolved() ở /_fluxe/stats.
108
105
  const container = createContainer();
@@ -272,10 +269,10 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
272
269
  return res.end("no action");
273
270
  }
274
271
  const t0 = Date.now();
275
- // DevTools (DEV): #3 resolution (backend TS in-process do manifest giải).
272
+ // DevTools (DEV): #3 resolution (render mode của cell; data = backend user-owned).
276
273
  const backend = backendFor(cellId);
277
- const r = manifest.cells[cellId]?.backend;
278
- const resolution = r ? `${r.language}/in-process` : "memory/in-process";
274
+ const render = manifest.cells[cellId]?.render;
275
+ const resolution = render ? `${render.mode}/${backend?.name ?? "no-backend"}` : "island";
279
276
  // #1 Chaos (DEV): inject delay + lỗi giả lập để test UX.
280
277
  if (DEV && req.headers["x-fluxe-chaos"]) {
281
278
  const c = parseChaos(String(req.headers["x-fluxe-chaos"]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmvuong92/fluxe",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
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",
@@ -1,2 +0,0 @@
1
- import type { Backend } from "./types";
2
- export declare function createMemoryBackend(): Backend;
@@ -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
- }
@@ -1,7 +0,0 @@
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;
@@ -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
- */
@@ -1,2 +0,0 @@
1
- import type { Backend } from "./types";
2
- export declare function createSqliteBackend(path?: string): Backend;
@@ -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
- }
@@ -1,11 +0,0 @@
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
- }
@@ -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 {};
@@ -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;
@@ -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
- }
@@ -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;
@@ -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
- }