@nmvuong92/fluxe 0.3.0 → 0.5.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/lib/core/cli.js +4 -0
- package/lib/core/config.d.ts +98 -0
- package/lib/core/config.js +66 -0
- package/lib/core/container.d.ts +9 -0
- package/lib/core/container.js +46 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/server_factory.d.ts +2 -1
- package/lib/server_factory.js +17 -8
- package/package.json +1 -1
package/lib/core/cli.js
CHANGED
|
@@ -17,6 +17,10 @@ export const COMMANDS = {
|
|
|
17
17
|
desc: "Auto-discovery: quét app/cells/* → app/app.ts (dev/resolve tự gọi)",
|
|
18
18
|
shell: () => SYNC,
|
|
19
19
|
},
|
|
20
|
+
config: {
|
|
21
|
+
desc: "In config đã giải (default ← ENV FLUXE_* ← override)",
|
|
22
|
+
shell: () => `tsx scripts/config.ts`,
|
|
23
|
+
},
|
|
20
24
|
gen: {
|
|
21
25
|
desc: "Codegen contract → types TS/Go/Rust (.fluxe/gen)",
|
|
22
26
|
shell: () => `tsx scripts/codegen.ts`,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const Schema: z.ZodObject<{
|
|
3
|
+
env: z.ZodEnum<["development", "production", "test"]>;
|
|
4
|
+
secret: z.ZodString;
|
|
5
|
+
port: z.ZodNumber;
|
|
6
|
+
defaultBackend: z.ZodEnum<["memory", "go", "rust"]>;
|
|
7
|
+
rateLimit: z.ZodObject<{
|
|
8
|
+
capacity: z.ZodNumber;
|
|
9
|
+
refillPerSec: z.ZodNumber;
|
|
10
|
+
maxKeys: z.ZodNumber;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
capacity: number;
|
|
13
|
+
refillPerSec: number;
|
|
14
|
+
maxKeys: number;
|
|
15
|
+
}, {
|
|
16
|
+
capacity: number;
|
|
17
|
+
refillPerSec: number;
|
|
18
|
+
maxKeys: number;
|
|
19
|
+
}>;
|
|
20
|
+
renderCache: z.ZodObject<{
|
|
21
|
+
maxKeys: z.ZodNumber;
|
|
22
|
+
}, "strip", z.ZodTypeAny, {
|
|
23
|
+
maxKeys: number;
|
|
24
|
+
}, {
|
|
25
|
+
maxKeys: number;
|
|
26
|
+
}>;
|
|
27
|
+
upload: z.ZodObject<{
|
|
28
|
+
maxBytes: z.ZodNumber;
|
|
29
|
+
}, "strip", z.ZodTypeAny, {
|
|
30
|
+
maxBytes: number;
|
|
31
|
+
}, {
|
|
32
|
+
maxBytes: number;
|
|
33
|
+
}>;
|
|
34
|
+
i18n: z.ZodObject<{
|
|
35
|
+
defaultLocale: z.ZodString;
|
|
36
|
+
}, "strip", z.ZodTypeAny, {
|
|
37
|
+
defaultLocale: string;
|
|
38
|
+
}, {
|
|
39
|
+
defaultLocale: string;
|
|
40
|
+
}>;
|
|
41
|
+
}, "strip", z.ZodTypeAny, {
|
|
42
|
+
env: "development" | "production" | "test";
|
|
43
|
+
secret: string;
|
|
44
|
+
port: number;
|
|
45
|
+
defaultBackend: "memory" | "go" | "rust";
|
|
46
|
+
rateLimit: {
|
|
47
|
+
capacity: number;
|
|
48
|
+
refillPerSec: number;
|
|
49
|
+
maxKeys: number;
|
|
50
|
+
};
|
|
51
|
+
renderCache: {
|
|
52
|
+
maxKeys: number;
|
|
53
|
+
};
|
|
54
|
+
upload: {
|
|
55
|
+
maxBytes: number;
|
|
56
|
+
};
|
|
57
|
+
i18n: {
|
|
58
|
+
defaultLocale: string;
|
|
59
|
+
};
|
|
60
|
+
}, {
|
|
61
|
+
env: "development" | "production" | "test";
|
|
62
|
+
secret: string;
|
|
63
|
+
port: number;
|
|
64
|
+
defaultBackend: "memory" | "go" | "rust";
|
|
65
|
+
rateLimit: {
|
|
66
|
+
capacity: number;
|
|
67
|
+
refillPerSec: number;
|
|
68
|
+
maxKeys: number;
|
|
69
|
+
};
|
|
70
|
+
renderCache: {
|
|
71
|
+
maxKeys: number;
|
|
72
|
+
};
|
|
73
|
+
upload: {
|
|
74
|
+
maxBytes: number;
|
|
75
|
+
};
|
|
76
|
+
i18n: {
|
|
77
|
+
defaultLocale: string;
|
|
78
|
+
};
|
|
79
|
+
}>;
|
|
80
|
+
export type FluxeConfig = z.infer<typeof Schema>;
|
|
81
|
+
type Src = Record<string, string | undefined>;
|
|
82
|
+
export declare const ENV_KEYS: {
|
|
83
|
+
readonly NODE_ENV: "env";
|
|
84
|
+
readonly FLUXE_SECRET: "secret";
|
|
85
|
+
readonly "PORT (ho\u1EB7c FLUXE_PORT)": "port";
|
|
86
|
+
readonly FLUXE_BACKEND: "defaultBackend";
|
|
87
|
+
readonly FLUXE_RATELIMIT_CAPACITY: "rateLimit.capacity";
|
|
88
|
+
readonly FLUXE_RATELIMIT_REFILL: "rateLimit.refillPerSec";
|
|
89
|
+
readonly FLUXE_RATELIMIT_MAX_KEYS: "rateLimit.maxKeys";
|
|
90
|
+
readonly FLUXE_RENDERCACHE_MAX_KEYS: "renderCache.maxKeys";
|
|
91
|
+
readonly FLUXE_UPLOAD_MAX_BYTES: "upload.maxBytes";
|
|
92
|
+
readonly FLUXE_LOCALE_DEFAULT: "i18n.defaultLocale";
|
|
93
|
+
};
|
|
94
|
+
type DeepPartial<T> = {
|
|
95
|
+
[K in keyof T]?: T[K] extends object ? Partial<T[K]> : T[K];
|
|
96
|
+
};
|
|
97
|
+
export declare function loadConfig(source?: Src, overrides?: DeepPartial<FluxeConfig>): FluxeConfig;
|
|
98
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright (c) 2026 nmvuong92
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/* Config core — DEFAULT + override qua ENV (quy ước FLUXE_*), kiểu Laravel config().
|
|
4
|
+
* Một nguồn sự thật cho mọi tham số tinh chỉnh của engine. Thứ tự ưu tiên:
|
|
5
|
+
* default < ENV (FLUXE_*) < override truyền tay.
|
|
6
|
+
* Mọi giá trị quan trọng ĐỀU expose ra ENV + tài liệu (xem docs/reference/configuration). */
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
const Schema = z.object({
|
|
9
|
+
env: z.enum(["development", "production", "test"]),
|
|
10
|
+
secret: z.string().min(8),
|
|
11
|
+
port: z.coerce.number().int().positive(),
|
|
12
|
+
defaultBackend: z.enum(["memory", "go", "rust"]),
|
|
13
|
+
rateLimit: z.object({
|
|
14
|
+
capacity: z.coerce.number().int().positive(),
|
|
15
|
+
refillPerSec: z.coerce.number().positive(),
|
|
16
|
+
maxKeys: z.coerce.number().int().positive(),
|
|
17
|
+
}),
|
|
18
|
+
renderCache: z.object({ maxKeys: z.coerce.number().int().positive() }),
|
|
19
|
+
upload: z.object({ maxBytes: z.coerce.number().int().positive() }),
|
|
20
|
+
i18n: z.object({ defaultLocale: z.string().min(2) }),
|
|
21
|
+
});
|
|
22
|
+
/* Bảng ENV — TÊN biến ↔ field. Dùng cho loadConfig + sinh tài liệu. */
|
|
23
|
+
export const ENV_KEYS = {
|
|
24
|
+
"NODE_ENV": "env",
|
|
25
|
+
"FLUXE_SECRET": "secret",
|
|
26
|
+
"PORT (hoặc FLUXE_PORT)": "port",
|
|
27
|
+
"FLUXE_BACKEND": "defaultBackend",
|
|
28
|
+
"FLUXE_RATELIMIT_CAPACITY": "rateLimit.capacity",
|
|
29
|
+
"FLUXE_RATELIMIT_REFILL": "rateLimit.refillPerSec",
|
|
30
|
+
"FLUXE_RATELIMIT_MAX_KEYS": "rateLimit.maxKeys",
|
|
31
|
+
"FLUXE_RENDERCACHE_MAX_KEYS": "renderCache.maxKeys",
|
|
32
|
+
"FLUXE_UPLOAD_MAX_BYTES": "upload.maxBytes",
|
|
33
|
+
"FLUXE_LOCALE_DEFAULT": "i18n.defaultLocale",
|
|
34
|
+
};
|
|
35
|
+
const num = (s, k, d) => {
|
|
36
|
+
const v = s[k];
|
|
37
|
+
return v == null || v === "" ? d : Number(v);
|
|
38
|
+
};
|
|
39
|
+
function merge(base, o) {
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
...o,
|
|
43
|
+
rateLimit: { ...base.rateLimit, ...o.rateLimit },
|
|
44
|
+
renderCache: { ...base.renderCache, ...o.renderCache },
|
|
45
|
+
upload: { ...base.upload, ...o.upload },
|
|
46
|
+
i18n: { ...base.i18n, ...o.i18n },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/* Giải config: default ← ENV (FLUXE_*) ← override. Validate Zod (sai → ném ngay, fail-fast). */
|
|
50
|
+
export function loadConfig(source = process.env, overrides = {}) {
|
|
51
|
+
const fromEnv = {
|
|
52
|
+
env: source.NODE_ENV || "development",
|
|
53
|
+
secret: source.FLUXE_SECRET || "dev-secret-change-me",
|
|
54
|
+
port: num(source, "PORT", num(source, "FLUXE_PORT", 5180)),
|
|
55
|
+
defaultBackend: source.FLUXE_BACKEND || "memory",
|
|
56
|
+
rateLimit: {
|
|
57
|
+
capacity: num(source, "FLUXE_RATELIMIT_CAPACITY", 30),
|
|
58
|
+
refillPerSec: num(source, "FLUXE_RATELIMIT_REFILL", 10),
|
|
59
|
+
maxKeys: num(source, "FLUXE_RATELIMIT_MAX_KEYS", 5000),
|
|
60
|
+
},
|
|
61
|
+
renderCache: { maxKeys: num(source, "FLUXE_RENDERCACHE_MAX_KEYS", 256) },
|
|
62
|
+
upload: { maxBytes: num(source, "FLUXE_UPLOAD_MAX_BYTES", 10 * 1024 * 1024) },
|
|
63
|
+
i18n: { defaultLocale: source.FLUXE_LOCALE_DEFAULT || "en" },
|
|
64
|
+
};
|
|
65
|
+
return Schema.parse(merge(fromEnv, overrides));
|
|
66
|
+
}
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export * from "./core/resolver.ts";
|
|
|
5
5
|
export * from "./core/wiring.ts";
|
|
6
6
|
export * from "./core/auth.ts";
|
|
7
7
|
export * from "./core/env.ts";
|
|
8
|
+
export * from "./core/config.ts";
|
|
9
|
+
export * from "./core/container.ts";
|
|
8
10
|
export * from "./core/i18n.ts";
|
|
9
11
|
export * from "./core/seo.ts";
|
|
10
12
|
export * from "./core/broker.ts";
|
package/lib/index.js
CHANGED
|
@@ -9,6 +9,8 @@ export * from "./core/resolver.js"; // resolve, ResolutionProfile/Manifest, Cell
|
|
|
9
9
|
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
|
+
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)
|
|
12
14
|
export * from "./core/i18n.js"; // createI18n, resolveLocale, translate, makeT, t(key, vars)
|
|
13
15
|
export * from "./core/seo.js"; // renderHead, renderSitemap, renderRobots, HeadMeta
|
|
14
16
|
export * from "./core/broker.js"; // pub/sub
|
package/lib/server_factory.d.ts
CHANGED
|
@@ -10,9 +10,10 @@ type LayoutEntry = LayoutMeta & {
|
|
|
10
10
|
type LayoutMap = Record<string, LayoutEntry>;
|
|
11
11
|
import { type I18n } from "./core/i18n.ts";
|
|
12
12
|
import { type Storage } from "./storage/types.ts";
|
|
13
|
+
import { type FluxeConfig } from "./core/config.ts";
|
|
13
14
|
export declare function makeServer(manifest: ResolutionManifest, cells: CellDef<any, any>[], layouts?: LayoutMap, opts?: {
|
|
14
15
|
i18n?: I18n;
|
|
15
16
|
storage?: Storage;
|
|
16
|
-
|
|
17
|
+
config?: FluxeConfig;
|
|
17
18
|
}): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
18
19
|
export {};
|
package/lib/server_factory.js
CHANGED
|
@@ -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";
|
|
@@ -23,6 +24,7 @@ import { parseChaos } from "./core/chaos.js";
|
|
|
23
24
|
import { resolveLocale, makeT } from "./core/i18n.js";
|
|
24
25
|
import { parseMultipart, boundaryFromContentType } from "./core/multipart.js";
|
|
25
26
|
import { makeKey } from "./storage/types.js";
|
|
27
|
+
import { loadConfig } from "./core/config.js";
|
|
26
28
|
import { createMemoryBackend } from "./backends/memory.js";
|
|
27
29
|
import { createHttpBackend } from "./backends/http.js";
|
|
28
30
|
// Build backend theo ngôn ngữ (cho live swap trong devtools).
|
|
@@ -92,7 +94,8 @@ function renderBodyToString(node) {
|
|
|
92
94
|
export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
93
95
|
const i18n = opts.i18n;
|
|
94
96
|
const storage = opts.storage;
|
|
95
|
-
const
|
|
97
|
+
const config = opts.config ?? loadConfig(); // default ← ENV (FLUXE_*) ← override
|
|
98
|
+
const MAX_UPLOAD = config.upload.maxBytes;
|
|
96
99
|
const readBodyBuffer = (req) => new Promise((resolve, reject) => {
|
|
97
100
|
const chunks = [];
|
|
98
101
|
let size = 0;
|
|
@@ -114,11 +117,14 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
114
117
|
// Backend GIẢI per-cell từ manifest (Resolution Plane) — cell/frontend giữ nguyên.
|
|
115
118
|
const backends = backendsFromManifest(manifest);
|
|
116
119
|
const backendFor = (id) => backends.byCell.get(id) ?? backends.default;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
// Resolved Container: service realtime đăng ký LƯỜI — chỉ tạo khi thật sự dùng (SSE/action).
|
|
121
|
+
// App không realtime → broker/presence KHÔNG bao giờ bootstrap. resolved() ở /_fluxe/stats.
|
|
122
|
+
const container = createContainer();
|
|
123
|
+
container.register("broker", () => createBroker());
|
|
124
|
+
container.register("presence", () => createPresence());
|
|
125
|
+
const actionLimit = createRateLimiter(config.rateLimit); // per-IP cho action (FLUXE_RATELIMIT_*)
|
|
126
|
+
const recorder = createRecorder(); // request log — chạy mỗi request → eager (luôn dùng)
|
|
127
|
+
const renderCache = createRenderCache({ maxKeys: config.renderCache.maxKeys }); // FLUXE_RENDERCACHE_MAX_KEYS
|
|
122
128
|
let clientJs; // ý A: đọc dist/client.js 1 lần (zero-copy: tái dùng buffer)
|
|
123
129
|
return http.createServer(async (req, res) => {
|
|
124
130
|
const url = new URL(req.url, "http://localhost");
|
|
@@ -158,7 +164,7 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
158
164
|
const m = process.memoryUsage();
|
|
159
165
|
const c = process.cpuUsage();
|
|
160
166
|
res.writeHead(200, { "content-type": "application/json" });
|
|
161
|
-
return res.end(JSON.stringify({ rss: m.rss, heapUsed: m.heapUsed, cpuUser: c.user, cpuSystem: c.system, uptimeMs: Math.round(process.uptime() * 1000) }));
|
|
167
|
+
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() }));
|
|
162
168
|
}
|
|
163
169
|
if (url.pathname === "/_fluxe/requests") {
|
|
164
170
|
// Observability: log request gần đây (timing/status). Prod: gate sau auth.
|
|
@@ -244,6 +250,9 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
244
250
|
const id = url.searchParams.get("id");
|
|
245
251
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
246
252
|
res.write(`event: ready\ndata: {"topic":"${topic}"}\n\n`);
|
|
253
|
+
// Lần đầu có client SSE → broker + presence mới bootstrap (lazy qua container).
|
|
254
|
+
const broker = container.get("broker");
|
|
255
|
+
const presence = container.get("presence");
|
|
247
256
|
const offBroker = broker.subscribe(topic, (data) => res.write(`data: ${JSON.stringify(data)}\n\n`));
|
|
248
257
|
let offPresence = () => { };
|
|
249
258
|
if (id) {
|
|
@@ -299,7 +308,7 @@ export function makeServer(manifest, cells, layouts = {}, opts = {}) {
|
|
|
299
308
|
if (schema)
|
|
300
309
|
input = validateInput(schema, input); // sai → FluxeError 400 (caught)
|
|
301
310
|
const out = await fn({ input, backend, session });
|
|
302
|
-
broker.publish(cellId, { action: name, out }); // realtime: báo client khác
|
|
311
|
+
container.get("broker").publish(cellId, { action: name, out }); // realtime: báo client khác (broker lazy)
|
|
303
312
|
res.writeHead(200, { "content-type": "application/json", "x-fluxe-resolution": resolution, "x-fluxe-server-ms": String(Date.now() - t0) });
|
|
304
313
|
return res.end(JSON.stringify(out));
|
|
305
314
|
}
|