@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
@@ -0,0 +1,19 @@
1
+ export declare class FluxeError extends Error {
2
+ code: string;
3
+ status: number;
4
+ details?: unknown;
5
+ constructor(code: string, message: string, status?: number, details?: unknown);
6
+ }
7
+ export interface ErrorPayload {
8
+ status: number;
9
+ code: string;
10
+ message: string;
11
+ errorId?: string;
12
+ detail?: string;
13
+ details?: unknown;
14
+ }
15
+ export declare function toErrorPayload(err: unknown, opts: {
16
+ dev: boolean;
17
+ errorId: string;
18
+ }): ErrorPayload;
19
+ export declare function renderErrorPage(p: ErrorPayload): string;
@@ -0,0 +1,39 @@
1
+ /* Error handling (tenet T5 / Trục 4q):
2
+ * - FluxeError = domain error (giá trị có kiểu) → map status/code/message an toàn.
3
+ * - Lỗi khác = unexpected → 500 generic + errorId; detail chỉ ở dev (không leak prod). */
4
+ export class FluxeError extends Error {
5
+ code;
6
+ status;
7
+ details; // vd: danh sách lỗi field (validation)
8
+ constructor(code, message, status = 400, details) {
9
+ super(message);
10
+ this.name = "FluxeError";
11
+ this.code = code;
12
+ this.status = status;
13
+ this.details = details;
14
+ }
15
+ }
16
+ export function toErrorPayload(err, opts) {
17
+ if (err instanceof FluxeError) {
18
+ const p = { status: err.status, code: err.code, message: err.message };
19
+ if (err.details !== undefined)
20
+ p.details = err.details;
21
+ return p;
22
+ }
23
+ const e = err;
24
+ const p = { status: 500, code: "internal", message: "Internal Server Error", errorId: opts.errorId };
25
+ if (opts.dev)
26
+ p.detail = e?.stack ?? String(err);
27
+ return p;
28
+ }
29
+ function esc(s) {
30
+ return String(s)
31
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
32
+ }
33
+ export function renderErrorPage(p) {
34
+ const id = p.errorId ? `<p style="color:#888">error id: ${esc(p.errorId)}</p>` : "";
35
+ const detail = p.detail ? `<pre style="background:#1a1a2e;color:#e8e8f0;padding:1rem;overflow:auto;border-radius:6px">${esc(p.detail)}</pre>` : "";
36
+ return `<!doctype html><html lang="vi"><head><meta charset="utf-8"><title>${p.status} ${esc(p.code)}</title></head>` +
37
+ `<body style="font:14px/1.5 ui-sans-serif,system-ui;margin:3rem;max-width:720px">` +
38
+ `<h1>${p.status} — ${esc(p.code)}</h1><p>${esc(p.message)}</p>${id}${detail}</body></html>`;
39
+ }
@@ -0,0 +1,2 @@
1
+ export declare function etagOf(body: string): string;
2
+ export declare function etagMatches(ifNoneMatch: string | undefined, etag: string): boolean;
@@ -0,0 +1,11 @@
1
+ import { createHash } from "node:crypto";
2
+ /* Render cache — ETag/304: hash body, client gửi If-None-Match → 304 nếu không đổi
3
+ * (không gửi lại body). Dùng cho JSON props (SPA nav refetch nhiều). */
4
+ export function etagOf(body) {
5
+ return `"${createHash("sha1").update(body).digest("base64url").slice(0, 27)}"`;
6
+ }
7
+ export function etagMatches(ifNoneMatch, etag) {
8
+ if (!ifNoneMatch)
9
+ return false;
10
+ return ifNoneMatch.split(",").map((s) => s.trim()).includes(etag);
11
+ }
@@ -0,0 +1,27 @@
1
+ export interface Job {
2
+ id: number;
3
+ type: string;
4
+ payload: any;
5
+ status: "pending" | "done" | "dead";
6
+ attempts: number;
7
+ }
8
+ export type JobHandler = (payload: any) => Promise<void>;
9
+ export interface Queue {
10
+ enqueue(type: string, payload: unknown): Job;
11
+ pending(): number;
12
+ dead(): number;
13
+ processNext(handlers: Record<string, JobHandler>, opts?: {
14
+ maxAttempts?: number;
15
+ }): Promise<Job | null>;
16
+ }
17
+ export declare function drain(queue: Queue, handlers: Record<string, JobHandler>, opts?: {
18
+ maxAttempts?: number;
19
+ }): Promise<number>;
20
+ export declare function createQueue(path?: string): {
21
+ enqueue(type: string, payload: unknown): Job;
22
+ pending: () => number;
23
+ dead: () => number;
24
+ processNext(handlers: Record<string, JobHandler>, opts?: {
25
+ maxAttempts?: number;
26
+ }): Promise<Job | null>;
27
+ };
@@ -0,0 +1,54 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ /* Worker: xử lý cạn job pending (retry → dead-letter bao bọc nên luôn kết thúc). */
3
+ export async function drain(queue, handlers, opts) {
4
+ let n = 0;
5
+ while (await queue.processNext(handlers, opts))
6
+ n++;
7
+ return n;
8
+ }
9
+ export function createQueue(path = ":memory:") {
10
+ const db = new DatabaseSync(path);
11
+ db.exec(`CREATE TABLE IF NOT EXISTS jobs (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ type TEXT NOT NULL,
14
+ payload TEXT NOT NULL,
15
+ status TEXT NOT NULL DEFAULT 'pending',
16
+ attempts INTEGER NOT NULL DEFAULT 0,
17
+ last_error TEXT
18
+ )`);
19
+ const toJob = (r) => ({
20
+ id: r.id, type: r.type, payload: JSON.parse(r.payload), status: r.status, attempts: r.attempts,
21
+ });
22
+ const countByStatus = (s) => db.prepare("SELECT COUNT(*) c FROM jobs WHERE status = ?").get(s).c;
23
+ return {
24
+ enqueue(type, payload) {
25
+ const info = db.prepare("INSERT INTO jobs (type, payload) VALUES (?, ?)").run(type, JSON.stringify(payload));
26
+ return toJob(db.prepare("SELECT * FROM jobs WHERE id = ?").get(info.lastInsertRowid));
27
+ },
28
+ pending: () => countByStatus("pending"),
29
+ dead: () => countByStatus("dead"),
30
+ // Claim 1 job pending (id nhỏ nhất), chạy handler; lỗi → attempts++ , dead nếu cạn retry.
31
+ async processNext(handlers, opts = {}) {
32
+ const max = opts.maxAttempts ?? 3;
33
+ const row = db.prepare("SELECT * FROM jobs WHERE status = 'pending' ORDER BY id LIMIT 1").get();
34
+ if (!row)
35
+ return null;
36
+ const job = toJob(row);
37
+ try {
38
+ const handler = handlers[job.type];
39
+ if (!handler)
40
+ throw new Error(`không có handler cho type '${job.type}'`);
41
+ await handler(job.payload);
42
+ db.prepare("UPDATE jobs SET status = 'done' WHERE id = ?").run(job.id);
43
+ return { ...job, status: "done" };
44
+ }
45
+ catch (e) {
46
+ const attempts = job.attempts + 1;
47
+ const dead = attempts >= max;
48
+ db.prepare("UPDATE jobs SET attempts = ?, status = ?, last_error = ? WHERE id = ?")
49
+ .run(attempts, dead ? "dead" : "pending", String(e?.message ?? e), job.id);
50
+ return { ...job, attempts, status: dead ? "dead" : "pending" };
51
+ }
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,5 @@
1
+ export interface LayoutMeta {
2
+ id: string;
3
+ parent?: string;
4
+ }
5
+ export declare function layoutChain(layoutId: string | undefined, layouts: Record<string, LayoutMeta>): string[];
@@ -0,0 +1,17 @@
1
+ /* Nested layouts — phần thuần: giải chuỗi layout từ cell lên root.
2
+ * Trả về thứ tự INNER→OUTER để render bọc dần (outermost cuối cùng bọc ngoài). */
3
+ export function layoutChain(layoutId, layouts) {
4
+ const chain = [];
5
+ const seen = new Set();
6
+ let cur = layoutId;
7
+ while (cur) {
8
+ if (seen.has(cur))
9
+ throw new Error(`layout vòng lặp: ${cur}`);
10
+ if (!layouts[cur])
11
+ throw new Error(`layout không tồn tại: ${cur}`);
12
+ seen.add(cur);
13
+ chain.push(cur); // inner trước
14
+ cur = layouts[cur].parent;
15
+ }
16
+ return chain; // [inner, …, outer]
17
+ }
@@ -0,0 +1,29 @@
1
+ export interface PageProps {
2
+ cell: string;
3
+ data: unknown;
4
+ layout?: string;
5
+ }
6
+ interface ClickLike {
7
+ button: number;
8
+ metaKey: boolean;
9
+ ctrlKey: boolean;
10
+ shiftKey: boolean;
11
+ altKey: boolean;
12
+ defaultPrevented: boolean;
13
+ }
14
+ interface AnchorLike {
15
+ href: string;
16
+ target?: string;
17
+ origin: string;
18
+ download?: boolean;
19
+ }
20
+ export declare function shouldIntercept(e: ClickLike, a: AnchorLike): boolean;
21
+ export interface PrefetchCache {
22
+ has(url: string): boolean;
23
+ peek(url: string): PageProps | undefined;
24
+ load(url: string, fetcher: (u: string) => Promise<PageProps>): Promise<PageProps>;
25
+ clear(url?: string): void;
26
+ size(): number;
27
+ }
28
+ export declare function createPrefetchCache(): PrefetchCache;
29
+ export {};
@@ -0,0 +1,53 @@
1
+ /* Resolved Navigation — phần THUẦN (testable, không DOM/fetch).
2
+ * shouldIntercept: có nên chặn click để SPA-nav không. createPrefetchCache: cache props
3
+ * theo URL + dedup in-flight (prefetch on hover → nav cảm giác tức thì). */
4
+ /* Chỉ chặn để SPA-nav khi: chuột trái, không phím bổ trợ, cùng origin, không _blank/download.
5
+ * Còn lại → để browser điều hướng thường (progressive enhancement: JS tắt vẫn chạy <a>). */
6
+ export function shouldIntercept(e, a) {
7
+ if (e.defaultPrevented)
8
+ return false;
9
+ if (e.button !== 0)
10
+ return false;
11
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
12
+ return false;
13
+ if (a.download)
14
+ return false;
15
+ if (a.target && a.target !== "" && a.target !== "_self")
16
+ return false;
17
+ let u;
18
+ try {
19
+ u = new URL(a.href, a.origin);
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ if (u.origin !== a.origin)
25
+ return false; // external
26
+ return true;
27
+ }
28
+ export function createPrefetchCache() {
29
+ const done = new Map();
30
+ const inflight = new Map();
31
+ return {
32
+ has: (url) => done.has(url),
33
+ peek: (url) => done.get(url),
34
+ load(url, fetcher) {
35
+ const cached = done.get(url);
36
+ if (cached)
37
+ return Promise.resolve(cached);
38
+ let p = inflight.get(url);
39
+ if (!p) {
40
+ p = fetcher(url).then((r) => { done.set(url, r); inflight.delete(url); return r; }, (e) => { inflight.delete(url); throw e; });
41
+ inflight.set(url, p);
42
+ }
43
+ return p;
44
+ },
45
+ clear(url) {
46
+ if (url)
47
+ done.delete(url);
48
+ else
49
+ done.clear();
50
+ },
51
+ size: () => done.size,
52
+ };
53
+ }
@@ -0,0 +1,12 @@
1
+ export interface ReqLog {
2
+ method: string;
3
+ path: string;
4
+ status: number;
5
+ ms: number;
6
+ ts: number;
7
+ }
8
+ export interface Recorder {
9
+ record(e: ReqLog): void;
10
+ recent(n?: number): ReqLog[];
11
+ }
12
+ export declare function createRecorder(max?: number): Recorder;
@@ -0,0 +1,24 @@
1
+ /* Observability — request log Ring Buffer CIRCULAR (#17).
2
+ * Trước: push + shift (shift O(n) mỗi record khi đầy). Giờ: mảng vòng + con trỏ ghi →
3
+ * record O(1) (không dời mảng). recent O(n) chỉ trên số phần tử yêu cầu. */
4
+ export function createRecorder(max = 200) {
5
+ const buf = new Array(max);
6
+ let writeIdx = 0; // vị trí ghi tiếp theo
7
+ let size = 0;
8
+ return {
9
+ record(e) {
10
+ buf[writeIdx] = e;
11
+ writeIdx = (writeIdx + 1) % max;
12
+ if (size < max)
13
+ size++;
14
+ },
15
+ recent(n = 50) {
16
+ const count = Math.min(n, size);
17
+ const out = [];
18
+ for (let i = 0; i < count; i++) {
19
+ out.push(buf[(writeIdx - 1 - i + max * 2) % max]); // lùi từ mới nhất
20
+ }
21
+ return out; // newest-first
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,3 @@
1
+ import type { ResolutionManifest } from "./resolver.ts";
2
+ import type { ReqLog } from "./observe.ts";
3
+ export declare function renderResolutionPanel(m: ResolutionManifest, requests?: ReqLog[]): string;
@@ -0,0 +1,46 @@
1
+ const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2
+ /* Dashboard fluxe (Trục 4j): RCA Resolution (mỗi cell giải trục nào) + Recent requests
3
+ * (observe: timing/status). String builder thuần, testable. */
4
+ export function renderResolutionPanel(m, requests = []) {
5
+ const rows = Object.values(m.cells).map((c) => `
6
+ <tr>
7
+ <td><code>${c.id}</code></td>
8
+ <td><code>${c.route}</code></td>
9
+ <td>${c.render.mode}</td>
10
+ <td>${c.render.shipClientJs ? "✓ JS" : "0 JS"}</td>
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
+ </tr>`).join("");
15
+ return `<!doctype html><html lang="vi"><head><meta charset="utf-8"><title>fluxe — RCA Resolution</title>
16
+ <style>
17
+ body{font:14px/1.5 ui-sans-serif,system-ui;margin:2rem;background:#0f0f1a;color:#e8e8f0}
18
+ h1{font-size:1.3rem;margin:0 0 .2rem}
19
+ .sub{color:#8a8aa0;margin-bottom:1.5rem}
20
+ table{border-collapse:collapse;width:100%;max-width:920px}
21
+ th,td{text-align:left;padding:.5rem .75rem;border-bottom:1px solid #2a2a40}
22
+ th{color:#8a8aa0;font-weight:600;font-size:.78rem;text-transform:uppercase;letter-spacing:.04em}
23
+ code{background:#1a1a2e;padding:.1rem .35rem;border-radius:4px;color:#a0a0ff}
24
+ .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}
26
+ </style></head>
27
+ <body>
28
+ <h1>RCA Resolution</h1>
29
+ <div class="sub">profile <b>${m.profile}</b> · default backend <b>${m.backend.language}</b> (${m.backend.transport})</div>
30
+ <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>
32
+ <tbody>${rows}
33
+ </tbody>
34
+ </table>
35
+ <p class="sub" style="margin-top:1.5rem">Đọc từ <code>.fluxe/resolution.json</code> — mỗi cell được Resolution Plane giải độc lập.</p>
36
+ ${requests.length ? `
37
+ <h1 style="font-size:1.1rem;margin:2rem 0 .2rem">Recent requests</h1>
38
+ <div class="sub">${requests.length} request gần nhất (observe)</div>
39
+ <table>
40
+ <thead><tr><th>method</th><th>path</th><th>status</th><th>ms</th></tr></thead>
41
+ <tbody>${requests.map((r) => `
42
+ <tr><td>${esc(r.method)}</td><td><code>${esc(r.path)}</code></td><td>${r.status}</td><td>${r.ms}ms</td></tr>`).join("")}
43
+ </tbody>
44
+ </table>` : ""}
45
+ </body></html>`;
46
+ }
@@ -0,0 +1,6 @@
1
+ export interface Presence {
2
+ join(topic: string, id: string): () => void;
3
+ members(topic: string): string[];
4
+ count(topic: string): number;
5
+ }
6
+ export declare function createPresence(): Presence;
@@ -0,0 +1,33 @@
1
+ /* Presence — ai đang online per topic (Trục 4g). Refcount theo id để chịu multi-tab/
2
+ * nhiều kết nối cùng user. Bản 1-node; distributed: chia sẻ qua NATS/Redis (gắn 4d). */
3
+ export function createPresence() {
4
+ const topics = new Map(); // topic → (id → số kết nối)
5
+ return {
6
+ join(topic, id) {
7
+ let m = topics.get(topic);
8
+ if (!m) {
9
+ m = new Map();
10
+ topics.set(topic, m);
11
+ }
12
+ m.set(id, (m.get(id) ?? 0) + 1);
13
+ return () => {
14
+ const mm = topics.get(topic);
15
+ if (!mm)
16
+ return;
17
+ const n = (mm.get(id) ?? 0) - 1;
18
+ if (n <= 0)
19
+ mm.delete(id);
20
+ else
21
+ mm.set(id, n);
22
+ if (mm.size === 0)
23
+ topics.delete(topic);
24
+ };
25
+ },
26
+ members(topic) {
27
+ return [...(topics.get(topic)?.keys() ?? [])];
28
+ },
29
+ count(topic) {
30
+ return topics.get(topic)?.size ?? 0;
31
+ },
32
+ };
33
+ }
@@ -0,0 +1,13 @@
1
+ export interface RateLimiter {
2
+ take(key: string): {
3
+ ok: boolean;
4
+ retryAfter: number;
5
+ };
6
+ size(): number;
7
+ }
8
+ export declare function createRateLimiter(opts: {
9
+ capacity: number;
10
+ refillPerSec: number;
11
+ now?: () => number;
12
+ maxKeys?: number;
13
+ }): RateLimiter;
@@ -0,0 +1,32 @@
1
+ /* Rate limiting (6b.F) — token bucket per key (vd IP/user/route). Thuần, clock injected.
2
+ * Bucket bound bằng LRU (maxKeys) → CHỐNG RÒ RAM khi nhiều IP (kẻ tấn công nhiều nguồn).
3
+ * Bản 1-node; distributed: chia sẻ bucket qua Redis (gắn 4d), cùng interface. */
4
+ export function createRateLimiter(opts) {
5
+ const now = opts.now ?? (() => Date.now());
6
+ const max = opts.maxKeys ?? 100_000;
7
+ // Map giữ thứ tự chèn → dùng làm LRU: chạm key = delete+set (đẩy về cuối).
8
+ const buckets = new Map();
9
+ return {
10
+ take(key) {
11
+ const t = now();
12
+ let b = buckets.get(key);
13
+ if (b) {
14
+ buckets.delete(key); // chạm → đưa về cuối (recently used)
15
+ }
16
+ else {
17
+ b = { tokens: opts.capacity, last: t };
18
+ if (buckets.size >= max)
19
+ buckets.delete(buckets.keys().next().value); // evict LRU (đầu)
20
+ }
21
+ buckets.set(key, b);
22
+ b.tokens = Math.min(opts.capacity, b.tokens + ((t - b.last) / 1000) * opts.refillPerSec);
23
+ b.last = t;
24
+ if (b.tokens >= 1) {
25
+ b.tokens -= 1;
26
+ return { ok: true, retryAfter: 0 };
27
+ }
28
+ return { ok: false, retryAfter: Math.ceil((1 - b.tokens) / opts.refillPerSec) };
29
+ },
30
+ size: () => buckets.size,
31
+ };
32
+ }
@@ -0,0 +1,15 @@
1
+ export interface RenderEntry {
2
+ etag: string;
3
+ buf: Buffer;
4
+ }
5
+ export interface RenderCache {
6
+ /** Lấy entry theo key (đánh dấu vừa dùng — LRU). undefined nếu chưa có. */
7
+ get(key: string): RenderEntry | undefined;
8
+ /** Lưu/ghi đè entry; evict key cũ nhất nếu vượt maxKeys. */
9
+ set(key: string, entry: RenderEntry): void;
10
+ /** Số entry hiện giữ (≤ maxKeys) — cho test/quan sát. */
11
+ size(): number;
12
+ }
13
+ export declare function createRenderCache(opts?: {
14
+ maxKeys?: number;
15
+ }): RenderCache;
@@ -0,0 +1,39 @@
1
+ /* ============================================================
2
+ * Render cache — memoize HTML đã render của cell static, key theo route.
3
+ *
4
+ * Tối ưu "zero-copy kiểu Node": render React 1 lần → giữ Buffer → mọi request
5
+ * sau ghi LẠI đúng buffer đó (res.end), bỏ qua renderToPipeableStream (đắt ~1 lõi).
6
+ *
7
+ * Behavior-preserving: mỗi entry gắn `etag` của data sinh ra nó. Caller chỉ dùng
8
+ * buffer khi etag khớp data hiện tại → data đổi ⇒ miss ⇒ render lại (không trả HTML cũ).
9
+ *
10
+ * Bound bộ nhớ bằng LRU (route động như /hello/[name] có không gian key vô hạn —
11
+ * không bound sẽ rò RAM). DSA: Hash Table + danh sách LRU, get/set O(1).
12
+ * ============================================================ */
13
+ export function createRenderCache(opts = {}) {
14
+ const maxKeys = opts.maxKeys ?? 256;
15
+ // Map giữ thứ tự chèn → key đầu tiên = ít-dùng-gần-đây nhất (LRU).
16
+ const m = new Map();
17
+ return {
18
+ get(key) {
19
+ const e = m.get(key);
20
+ if (e === undefined)
21
+ return undefined;
22
+ m.delete(key); // chạm vào → đẩy lên "mới dùng" (cuối Map)
23
+ m.set(key, e);
24
+ return e;
25
+ },
26
+ set(key, entry) {
27
+ if (m.has(key))
28
+ m.delete(key);
29
+ m.set(key, entry);
30
+ while (m.size > maxKeys) {
31
+ const oldest = m.keys().next().value;
32
+ if (oldest === undefined)
33
+ break;
34
+ m.delete(oldest);
35
+ }
36
+ },
37
+ size: () => m.size,
38
+ };
39
+ }
@@ -0,0 +1,37 @@
1
+ export type RenderMode = "static" | "island";
2
+ export type BackendKind = "memory" | "go" | "rust";
3
+ export interface CellDecl {
4
+ id: string;
5
+ route: string;
6
+ hydration?: RenderMode;
7
+ }
8
+ export interface ResolutionProfile {
9
+ name: string;
10
+ backend: BackendKind;
11
+ endpoints?: {
12
+ go?: string;
13
+ rust?: string;
14
+ };
15
+ cellBackends?: Record<string, BackendKind>;
16
+ }
17
+ export interface BackendResolution {
18
+ language: BackendKind;
19
+ transport: "in-process" | "http";
20
+ endpoint?: string;
21
+ }
22
+ export interface CellResolution {
23
+ id: string;
24
+ route: string;
25
+ render: {
26
+ mode: RenderMode;
27
+ shipClientJs: boolean;
28
+ };
29
+ backend: BackendResolution;
30
+ }
31
+ export interface ResolutionManifest {
32
+ version: 1;
33
+ profile: string;
34
+ backend: BackendResolution;
35
+ cells: Record<string, CellResolution>;
36
+ }
37
+ export declare function resolve(cells: CellDecl[], profile: ResolutionProfile): ResolutionManifest;
@@ -0,0 +1,42 @@
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.
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
+ 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 };
15
+ }
16
+ export function resolve(cells, profile) {
17
+ const ids = new Set(cells.map((c) => c.id));
18
+ for (const id of Object.keys(profile.cellBackends ?? {})) {
19
+ if (!ids.has(id)) {
20
+ throw new Error(`profile "${profile.name}": cellBackends trỏ cell không tồn tại: ${id}`);
21
+ }
22
+ }
23
+ const backend = resolveBackend(profile.backend, profile); // default app-level
24
+ const out = {};
25
+ const seenRoutes = new Set();
26
+ for (const c of cells) {
27
+ if (out[c.id])
28
+ throw new Error(`cell id trùng: ${c.id}`);
29
+ if (seenRoutes.has(c.route))
30
+ throw new Error(`route trùng: ${c.route}`);
31
+ seenRoutes.add(c.route);
32
+ const kind = profile.cellBackends?.[c.id] ?? profile.backend;
33
+ const mode = c.hydration ?? "island"; // default island
34
+ out[c.id] = {
35
+ id: c.id,
36
+ route: c.route,
37
+ render: { mode, shipClientJs: mode === "island" },
38
+ backend: resolveBackend(kind, profile),
39
+ };
40
+ }
41
+ return { version: 1, profile: profile.name, backend, cells: out };
42
+ }
@@ -0,0 +1,6 @@
1
+ import type { CellDef } from "./engine";
2
+ export interface RouteMatch {
3
+ cell: CellDef<any, any>;
4
+ params: Record<string, string>;
5
+ }
6
+ export declare function makeRouter(cells: CellDef<any, any>[]): (pathname: string) => RouteMatch | null;
@@ -0,0 +1,41 @@
1
+ const ESC = /[.*+?^${}()|[\]\\]/g;
2
+ function compile(cell) {
3
+ const paramNames = [];
4
+ let isStatic = true;
5
+ const parts = cell.route.split("/").map((seg) => {
6
+ const m = seg.match(/^\[(.+)\]$/);
7
+ if (m) {
8
+ isStatic = false;
9
+ paramNames.push(m[1]);
10
+ return "([^/]+)";
11
+ }
12
+ return seg.replace(ESC, "\\$&");
13
+ });
14
+ return { isStatic, compiled: { cell, regex: new RegExp("^" + parts.join("/") + "$"), paramNames } };
15
+ }
16
+ // Trả về matcher: static exact (Map, O(1)) trước, dynamic param sau (precedence).
17
+ export function makeRouter(cells) {
18
+ const staticMap = new Map();
19
+ const dynamic = [];
20
+ for (const cell of cells) {
21
+ const { isStatic, compiled } = compile(cell);
22
+ if (isStatic)
23
+ staticMap.set(cell.route, cell);
24
+ else
25
+ dynamic.push(compiled);
26
+ }
27
+ return (pathname) => {
28
+ const s = staticMap.get(pathname);
29
+ if (s)
30
+ return { cell: s, params: {} };
31
+ for (const d of dynamic) {
32
+ const m = d.regex.exec(pathname);
33
+ if (m) {
34
+ const params = {};
35
+ d.paramNames.forEach((n, i) => (params[n] = decodeURIComponent(m[i + 1])));
36
+ return { cell: d.cell, params };
37
+ }
38
+ }
39
+ return null;
40
+ };
41
+ }
@@ -0,0 +1,10 @@
1
+ export interface HeadMeta {
2
+ title?: string;
3
+ description?: string;
4
+ canonical?: string;
5
+ og?: Record<string, string>;
6
+ jsonLd?: unknown;
7
+ }
8
+ export declare function renderHead(meta: HeadMeta): string;
9
+ export declare function renderSitemap(routes: string[], baseUrl: string): string;
10
+ export declare function renderRobots(baseUrl: string): string;