@ezetgalaxy/titan 26.13.0 → 26.13.3
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/index.js +44 -12
- package/package.json +5 -1
- package/templates/common/Dockerfile +60 -37
- package/templates/common/_dockerignore +36 -0
- package/templates/common/app/titan.d.ts +1 -1
- package/templates/js/server/src/action_management.rs +20 -14
- package/templates/js/server/src/extensions/builtin.rs +54 -5
- package/templates/js/server/src/extensions/external.rs +37 -12
- package/templates/js/server/src/extensions/mod.rs +28 -10
- package/templates/js/server/src/extensions/titan_core.js +163 -155
- package/templates/js/server/src/main.rs +50 -77
- package/templates/js/server/src/runtime.rs +141 -210
- package/templates/js/titan/bundle.js +4 -7
- package/templates/js/titan/dev.js +19 -2
- package/templates/js/titan/titan.js +2 -2
- package/templates/ts/server/src/action_management.rs +20 -14
- package/templates/ts/server/src/extensions/builtin.rs +54 -5
- package/templates/ts/server/src/extensions/external.rs +37 -12
- package/templates/ts/server/src/extensions/mod.rs +28 -10
- package/templates/ts/server/src/extensions/titan_core.js +163 -155
- package/templates/ts/server/src/main.rs +50 -77
- package/templates/ts/server/src/runtime.rs +141 -210
- package/templates/ts/titan/bundle.js +11 -14
- package/templates/ts/titan/dev.js +20 -2
- package/templates/ts/titan/titan.d.ts +1 -1
- package/templates/ts/titan/titan.js +1 -0
- package/titanpl-sdk/package.json +1 -1
- package/titanpl-sdk/templates/index.d.ts +249 -0
- package/titanpl-sdk/templates/server/src/action_management.rs +21 -14
- package/titanpl-sdk/templates/server/src/extensions/builtin.rs +469 -180
- package/titanpl-sdk/templates/server/src/extensions/external.rs +37 -12
- package/titanpl-sdk/templates/server/src/extensions/mod.rs +143 -21
- package/titanpl-sdk/templates/server/src/extensions/titan_core.js +179 -15
- package/titanpl-sdk/templates/server/src/main.rs +113 -71
- package/titanpl-sdk/templates/server/src/runtime.rs +172 -85
- package/titanpl-sdk/templates/titan/bundle.js +4 -7
- package/titanpl-sdk/templates/titan/titan.js +2 -2
- package/titanpl-sdk/index.d.ts +0 -50
package/index.js
CHANGED
|
@@ -107,13 +107,13 @@ export function help() {
|
|
|
107
107
|
console.log(`
|
|
108
108
|
${bold(cyan("Titan Planet"))} v${TITAN_VERSION}
|
|
109
109
|
|
|
110
|
-
${green("titan init <project> [-t <template>]")} Create new
|
|
111
|
-
${green("titan create ext <name>")} Create new
|
|
112
|
-
${green("titan dev")}
|
|
110
|
+
${green("titan init <project> [-t <template>]")} Create new TitanPl project
|
|
111
|
+
${green("titan create ext <name>")} Create new TitanPl extension
|
|
112
|
+
${green("titan dev [-c]")} Dev mode (hot reload) [-c to backward clean]
|
|
113
113
|
${green("titan build")} Build production Rust server
|
|
114
114
|
${green("titan start")} Start production binary
|
|
115
|
-
${green("titan update")} Update
|
|
116
|
-
${green("titan --version")} Show
|
|
115
|
+
${green("titan update")} Update TitanPl Framework
|
|
116
|
+
${green("titan --version")} Show TitanPl CLI version
|
|
117
117
|
|
|
118
118
|
${yellow("Note: `tit` is supported as a legacy alias.")}
|
|
119
119
|
`);
|
|
@@ -282,8 +282,31 @@ export async function initProject(name, templateName) {
|
|
|
282
282
|
/* -------------------------------------------------------
|
|
283
283
|
* DEV SERVER
|
|
284
284
|
* ----------------------------------------------------- */
|
|
285
|
-
export async function devServer() {
|
|
285
|
+
export async function devServer(args = []) {
|
|
286
286
|
const root = process.cwd();
|
|
287
|
+
|
|
288
|
+
// Check for clean cache flag
|
|
289
|
+
if (args.includes("-c") || args.includes("--clean") || args.includes("--clean-cache")) {
|
|
290
|
+
console.log(cyan("TitanPl: Clearing cache..."));
|
|
291
|
+
|
|
292
|
+
const pathsToClean = [
|
|
293
|
+
path.join(root, ".titan"),
|
|
294
|
+
path.join(root, "server", "actions"),
|
|
295
|
+
path.join(root, "server", "target")
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
for (const p of pathsToClean) {
|
|
299
|
+
if (fs.existsSync(p)) {
|
|
300
|
+
try {
|
|
301
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
302
|
+
console.log(gray(` ✔ Deleted ${path.relative(root, p)}`));
|
|
303
|
+
} catch (e) {
|
|
304
|
+
console.log(yellow(` ⚠ Could not delete ${path.relative(root, p)}: ${e.message}`));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
console.log(green("✔ Cache cleared."));
|
|
309
|
+
}
|
|
287
310
|
const devScript = path.join(root, "titan", "dev.js");
|
|
288
311
|
|
|
289
312
|
if (!fs.existsSync(devScript)) {
|
|
@@ -461,6 +484,8 @@ export function updateTitan() {
|
|
|
461
484
|
}
|
|
462
485
|
|
|
463
486
|
const templatesRoot = path.join(__dirname, "templates", templateType);
|
|
487
|
+
const commonRoot = path.join(__dirname, "templates", "common");
|
|
488
|
+
|
|
464
489
|
const templateTitan = path.join(templatesRoot, "titan");
|
|
465
490
|
const templateServer = path.join(templatesRoot, "server");
|
|
466
491
|
|
|
@@ -530,11 +555,17 @@ export function updateTitan() {
|
|
|
530
555
|
"_gitignore": ".gitignore",
|
|
531
556
|
"_dockerignore": ".dockerignore",
|
|
532
557
|
"Dockerfile": "Dockerfile",
|
|
533
|
-
"jsconfig.json": "jsconfig.json"
|
|
558
|
+
"jsconfig.json": "jsconfig.json",
|
|
559
|
+
"tsconfig.json": "tsconfig.json",
|
|
560
|
+
"eslint.config.js": "eslint.config.js"
|
|
534
561
|
};
|
|
535
562
|
|
|
536
563
|
for (const [srcName, destName] of Object.entries(rootFiles)) {
|
|
537
|
-
|
|
564
|
+
let src = path.join(templatesRoot, srcName);
|
|
565
|
+
if (!fs.existsSync(src)) {
|
|
566
|
+
src = path.join(commonRoot, srcName);
|
|
567
|
+
}
|
|
568
|
+
|
|
538
569
|
const dest = path.join(root, destName);
|
|
539
570
|
|
|
540
571
|
if (fs.existsSync(src)) {
|
|
@@ -545,9 +576,10 @@ export function updateTitan() {
|
|
|
545
576
|
|
|
546
577
|
// app/titan.d.ts (JS typing contract)
|
|
547
578
|
const appDir = path.join(root, "app");
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
579
|
+
const templatesDts = path.join(templatesRoot, "app", "titan.d.ts");
|
|
580
|
+
const commonDts = path.join(commonRoot, "app", "titan.d.ts");
|
|
581
|
+
|
|
582
|
+
const finalDtsSrc = fs.existsSync(templatesDts) ? templatesDts : (fs.existsSync(commonDts) ? commonDts : null);
|
|
551
583
|
const destDts = path.join(appDir, "titan.d.ts");
|
|
552
584
|
|
|
553
585
|
if (finalDtsSrc) {
|
|
@@ -686,7 +718,7 @@ if (isMainModule) {
|
|
|
686
718
|
await initProject(projName, tpl);
|
|
687
719
|
break;
|
|
688
720
|
}
|
|
689
|
-
case "dev": devServer(); break;
|
|
721
|
+
case "dev": devServer(process.argv.slice(3)); break;
|
|
690
722
|
case "build": await buildProd(); break;
|
|
691
723
|
case "start": startProd(); break;
|
|
692
724
|
case "update": updateTitan(); break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ezetgalaxy/titan",
|
|
3
|
-
"version": "26.13.
|
|
3
|
+
"version": "26.13.3",
|
|
4
4
|
"description": "Titan Planet is a JavaScript-first backend framework that embeds JS actions into a Rust + Axum server and ships as a single native binary. Routes are compiled to static metadata; only actions run in the embedded JS runtime. No Node.js. No event loop in production.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "ezetgalaxy",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
],
|
|
19
19
|
"keywords": [
|
|
20
20
|
"titan",
|
|
21
|
+
"titanpl",
|
|
22
|
+
"t8n",
|
|
23
|
+
"tpl",
|
|
24
|
+
"tgrv",
|
|
21
25
|
"titanjs",
|
|
22
26
|
"ezetgalaxy",
|
|
23
27
|
"framework",
|
|
@@ -1,66 +1,89 @@
|
|
|
1
1
|
# ================================================================
|
|
2
|
-
# STAGE 1 — Build
|
|
2
|
+
# STAGE 1 — Build TitanPl
|
|
3
3
|
# ================================================================
|
|
4
|
-
FROM
|
|
4
|
+
FROM node:20.20.0-slim AS builder
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
&&
|
|
6
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
7
|
+
curl ca-certificates build-essential pkg-config libssl-dev git bash \
|
|
8
|
+
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
|
|
9
|
+
sh -s -- -y --default-toolchain stable --profile minimal \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
ENV PATH="/root/.cargo/bin:${PATH}"
|
|
13
|
+
ENV NODE_ENV=production
|
|
14
|
+
ENV RUSTFLAGS="-C target-cpu=native -C strip=symbols"
|
|
15
|
+
|
|
16
|
+
WORKDIR /app
|
|
12
17
|
|
|
18
|
+
# ---------- Rust Cache ----------
|
|
19
|
+
RUN mkdir -p server/src
|
|
20
|
+
COPY server/Cargo.toml server/Cargo.lock* server/
|
|
21
|
+
RUN echo "fn main(){}" > server/src/main.rs
|
|
22
|
+
WORKDIR /app/server
|
|
23
|
+
RUN cargo build --release
|
|
24
|
+
RUN rm src/main.rs
|
|
13
25
|
WORKDIR /app
|
|
14
26
|
|
|
15
|
-
#
|
|
27
|
+
# ---------- Node Cache ----------
|
|
28
|
+
COPY package.json package-lock.json* ./
|
|
29
|
+
RUN if [ -f package-lock.json ]; then npm ci --omit=dev; else npm install --omit=dev; fi
|
|
30
|
+
|
|
31
|
+
RUN npm install -g @ezetgalaxy/titan@latest
|
|
32
|
+
|
|
33
|
+
# ---------- Copy Project ----------
|
|
16
34
|
COPY . .
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
RUN npm install
|
|
36
|
+
RUN node app/app.js --build
|
|
20
37
|
|
|
38
|
+
# ---------- Extensions ----------
|
|
21
39
|
SHELL ["/bin/bash", "-c"]
|
|
22
|
-
|
|
23
|
-
# Extract Titan extensions into .ext
|
|
24
40
|
RUN mkdir -p /app/.ext && \
|
|
25
|
-
find /app/node_modules -
|
|
41
|
+
find /app/node_modules -type f -name "titan.json" -print0 | \
|
|
26
42
|
while IFS= read -r -d '' file; do \
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
done
|
|
32
|
-
|
|
33
|
-
ls -R /app/.ext
|
|
34
|
-
|
|
35
|
-
# Build Titan metadata + bundle JS actions
|
|
36
|
-
RUN titan build
|
|
37
|
-
|
|
38
|
-
# Build Rust binary
|
|
43
|
+
pkg_dir="$(dirname "$file")"; \
|
|
44
|
+
pkg_name="$(basename "$pkg_dir")"; \
|
|
45
|
+
cp -r "$pkg_dir" "/app/.ext/$pkg_name"; \
|
|
46
|
+
rm -rf "/app/.ext/$pkg_name/node_modules"; \
|
|
47
|
+
done
|
|
48
|
+
|
|
39
49
|
RUN cd server && cargo build --release
|
|
40
50
|
|
|
41
51
|
|
|
42
52
|
|
|
43
53
|
# ================================================================
|
|
44
|
-
# STAGE 2 — Runtime
|
|
54
|
+
# STAGE 2 — Runtime (Render Safe)
|
|
45
55
|
# ================================================================
|
|
46
|
-
FROM debian:
|
|
56
|
+
FROM debian:bookworm-slim
|
|
57
|
+
|
|
58
|
+
RUN apt-get update && \
|
|
59
|
+
apt-get install -y --no-install-recommends ca-certificates curl && \
|
|
60
|
+
rm -rf /var/lib/apt/lists/*
|
|
47
61
|
|
|
48
62
|
WORKDIR /app
|
|
49
63
|
|
|
50
|
-
# Copy
|
|
64
|
+
# ---- Copy Files as root ----
|
|
51
65
|
COPY --from=builder /app/server/target/release/titan-server ./titan-server
|
|
66
|
+
COPY --from=builder /app/server/routes.json .
|
|
67
|
+
COPY --from=builder /app/server/action_map.json .
|
|
68
|
+
COPY --from=builder /app/server/src/actions ./actions
|
|
69
|
+
COPY --from=builder /app/app/static ./static
|
|
70
|
+
COPY --from=builder /app/.ext ./.ext
|
|
52
71
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
COPY --from=builder /app/server/action_map.json ./action_map.json
|
|
72
|
+
# ---- Ensure Executable ----
|
|
73
|
+
RUN chmod +x ./titan-server
|
|
56
74
|
|
|
57
|
-
#
|
|
58
|
-
RUN
|
|
59
|
-
|
|
75
|
+
# ---- Create User After Copy ----
|
|
76
|
+
RUN useradd -m titan && chown -R titan:titan /app
|
|
77
|
+
USER titan
|
|
60
78
|
|
|
61
|
-
#
|
|
62
|
-
|
|
79
|
+
# ---- Platform Defaults ----
|
|
80
|
+
ENV HOST=0.0.0.0
|
|
81
|
+
ENV PORT=5100
|
|
82
|
+
|
|
83
|
+
# ---- Verify Node Not Present ----
|
|
84
|
+
RUN which node || echo "NodeJS not present ✔"
|
|
63
85
|
|
|
64
86
|
EXPOSE 5100
|
|
65
87
|
|
|
66
|
-
|
|
88
|
+
# ---- Force Foreground Process ----
|
|
89
|
+
ENTRYPOINT ["./titan-server"]
|
|
@@ -1,3 +1,39 @@
|
|
|
1
1
|
node_modules
|
|
2
2
|
npm-debug.log
|
|
3
3
|
.git
|
|
4
|
+
.gitignore
|
|
5
|
+
|
|
6
|
+
package-lock.json
|
|
7
|
+
yarn.lock
|
|
8
|
+
|
|
9
|
+
# Titan Runtime (Auto-generated - DO NOT COMMIT)
|
|
10
|
+
titan/server-bin*
|
|
11
|
+
.titan/
|
|
12
|
+
server/routes.json
|
|
13
|
+
server/action_map.json
|
|
14
|
+
server/actions/
|
|
15
|
+
server/titan/
|
|
16
|
+
server/src/actions_rust/
|
|
17
|
+
deploy/
|
|
18
|
+
|
|
19
|
+
# Rust Build Artifacts
|
|
20
|
+
server/target/
|
|
21
|
+
Cargo.lock
|
|
22
|
+
|
|
23
|
+
# OS Files
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
*.tmp
|
|
27
|
+
*.bak
|
|
28
|
+
|
|
29
|
+
# Environment & Secrets
|
|
30
|
+
.env
|
|
31
|
+
.env.local
|
|
32
|
+
.env.*.local
|
|
33
|
+
|
|
34
|
+
# IDEs
|
|
35
|
+
.vscode/
|
|
36
|
+
.idea/
|
|
37
|
+
*.swp
|
|
38
|
+
*.swo
|
|
39
|
+
|
|
@@ -10,7 +10,7 @@ export interface TitanBuilder {
|
|
|
10
10
|
get(route: string): RouteHandler;
|
|
11
11
|
post(route: string): RouteHandler;
|
|
12
12
|
log(module: string, msg: string): void;
|
|
13
|
-
start(port?: number, msg?: string): Promise<void>;
|
|
13
|
+
start(port?: number, msg?: string, threads?: number): Promise<void>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
declare const builder: TitanBuilder;
|
|
@@ -34,30 +34,36 @@ pub fn resolve_actions_dir() -> PathBuf {
|
|
|
34
34
|
return PathBuf::from("/app/actions");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Try to walk up from the executing binary to discover `<...>/server/actions`
|
|
37
|
+
// Try to walk up from the executing binary to discover `<...>/server/src/actions`
|
|
38
38
|
if let Ok(exe) = std::env::current_exe() {
|
|
39
39
|
if let Some(parent) = exe.parent() {
|
|
40
40
|
if let Some(target_dir) = parent.parent() {
|
|
41
41
|
if let Some(server_dir) = target_dir.parent() {
|
|
42
|
-
let candidate = server_dir.join("actions");
|
|
42
|
+
let candidate = server_dir.join("src").join("actions");
|
|
43
43
|
if candidate.exists() {
|
|
44
44
|
return candidate;
|
|
45
45
|
}
|
|
46
|
+
let candidate2 = server_dir.join("actions");
|
|
47
|
+
if candidate2.exists() {
|
|
48
|
+
return candidate2;
|
|
49
|
+
}
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
// Fall back to local ./actions
|
|
52
|
-
PathBuf::from("./actions")
|
|
55
|
+
// Fall back to local ./src/actions
|
|
56
|
+
PathBuf::from("./src/actions")
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/// Try to find the directory that contains compiled action bundles.
|
|
56
60
|
pub fn find_actions_dir(project_root: &PathBuf) -> Option<PathBuf> {
|
|
57
61
|
let candidates = [
|
|
62
|
+
project_root.join("server").join("src").join("actions"),
|
|
63
|
+
project_root.join("server").join("actions"),
|
|
58
64
|
project_root.join("app").join("actions"),
|
|
59
65
|
project_root.join("actions"),
|
|
60
|
-
|
|
66
|
+
|
|
61
67
|
project_root.join("..").join("server").join("actions"),
|
|
62
68
|
PathBuf::from("/app").join("actions"),
|
|
63
69
|
PathBuf::from("actions"),
|
|
@@ -139,17 +145,16 @@ pub fn match_dynamic_route(
|
|
|
139
145
|
pub fn scan_actions(root: &PathBuf) -> HashMap<String, PathBuf> {
|
|
140
146
|
let mut map = HashMap::new();
|
|
141
147
|
|
|
142
|
-
// Locate actions dir
|
|
143
|
-
let
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Some(d) => d,
|
|
149
|
-
None => return map,
|
|
148
|
+
// Locate actions dir - Priority: project root relative paths
|
|
149
|
+
let dir = match find_actions_dir(root) {
|
|
150
|
+
Some(d) => d,
|
|
151
|
+
None => {
|
|
152
|
+
let ad = resolve_actions_dir();
|
|
153
|
+
if ad.exists() { ad } else { return map; }
|
|
150
154
|
}
|
|
151
155
|
};
|
|
152
156
|
|
|
157
|
+
// Scanning actions
|
|
153
158
|
if let Ok(entries) = std::fs::read_dir(dir) {
|
|
154
159
|
for entry in entries.flatten() {
|
|
155
160
|
let path = entry.path();
|
|
@@ -163,9 +168,10 @@ pub fn scan_actions(root: &PathBuf) -> HashMap<String, PathBuf> {
|
|
|
163
168
|
let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
|
164
169
|
if file_stem.is_empty() { continue; }
|
|
165
170
|
|
|
171
|
+
// Found action
|
|
166
172
|
map.insert(file_stem.to_string(), path);
|
|
167
173
|
}
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
map
|
|
171
|
-
}
|
|
177
|
+
}
|
|
@@ -158,12 +158,64 @@ fn setup_native_utils(scope: &mut v8::HandleScope, t_obj: v8::Local<v8::Object>)
|
|
|
158
158
|
let fs_read_fn = v8::Function::new(scope, native_read).unwrap();
|
|
159
159
|
let read_key = v8_str(scope, "read");
|
|
160
160
|
fs_obj.set(scope, read_key.into(), fs_read_fn.into());
|
|
161
|
+
|
|
162
|
+
let fs_read_sync_fn = v8::Function::new(scope, native_read_sync).unwrap();
|
|
163
|
+
let read_sync_key = v8_str(scope, "readFile");
|
|
164
|
+
fs_obj.set(scope, read_sync_key.into(), fs_read_sync_fn.into());
|
|
165
|
+
|
|
166
|
+
// Also Expose as t.readSync
|
|
167
|
+
let t_read_sync_fn = v8::Function::new(scope, native_read_sync).unwrap();
|
|
168
|
+
let t_read_sync_key = v8_str(scope, "readSync");
|
|
169
|
+
t_obj.set(scope, t_read_sync_key.into(), t_read_sync_fn.into());
|
|
161
170
|
|
|
162
171
|
let fs_key = v8_str(scope, "fs");
|
|
163
172
|
core_obj.set(scope, fs_key.into(), fs_obj.into());
|
|
164
173
|
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn native_read_sync(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
178
|
+
let path_val = args.get(0);
|
|
179
|
+
if !path_val.is_string() {
|
|
180
|
+
throw(scope, "readSync/readFile: path is required");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let path_str = v8_to_string(scope, path_val);
|
|
184
|
+
|
|
185
|
+
let root = super::PROJECT_ROOT.get().cloned().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
|
186
|
+
let joined = root.join(&path_str);
|
|
187
|
+
|
|
188
|
+
// Security Check
|
|
189
|
+
if let Ok(target) = joined.canonicalize() {
|
|
190
|
+
// In Docker, /app/static/index.html vs /app
|
|
191
|
+
// Canonical paths might resolve symlinks.
|
|
192
|
+
// We just ensure it's within root or a subdirectory.
|
|
193
|
+
// For simplicity in this fix, we trust canonicalize logic if it exists, otherwise strict join.
|
|
194
|
+
if target.starts_with(&root.canonicalize().unwrap_or(root.clone())) {
|
|
195
|
+
match std::fs::read_to_string(&target) {
|
|
196
|
+
Ok(content) => {
|
|
197
|
+
let v8_content = v8_str(scope, &content);
|
|
198
|
+
retval.set(v8_content.into());
|
|
199
|
+
},
|
|
200
|
+
Err(e) => {
|
|
201
|
+
// Return null or throw? Node's readFile throws. Titan types say return string.
|
|
202
|
+
// The user's code: fs.readFile(...) || "Default"
|
|
203
|
+
// This implies it might return undefined/null on failure?
|
|
204
|
+
// Or maybe they expect it to succeed.
|
|
205
|
+
// Let's throw to be safe for debugging, or return null if not found?
|
|
206
|
+
// "||" handles null/undefined usually.
|
|
207
|
+
// But usually readFile throws if file not found.
|
|
208
|
+
// Let's print error and return null to avoid crashing entire worker init if optional.
|
|
209
|
+
retval.set(v8::null(scope).into());
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
retval.set(v8::null(scope).into());
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// File doesn't exist usually
|
|
217
|
+
retval.set(v8::null(scope).into());
|
|
218
|
+
}
|
|
167
219
|
}
|
|
168
220
|
|
|
169
221
|
fn native_read(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
@@ -684,13 +736,11 @@ fn native_drift_call(scope: &mut v8::HandleScope, mut args: v8::FunctionCallback
|
|
|
684
736
|
// Trigger Tokio task completion handling in a separate bridge
|
|
685
737
|
let tokio_handle = runtime.tokio_handle.clone();
|
|
686
738
|
let worker_tx = runtime.worker_tx.clone();
|
|
687
|
-
let isolate_id = runtime.id;
|
|
688
739
|
|
|
689
740
|
tokio_handle.spawn(async move {
|
|
690
741
|
if let Ok(res) = rx.await {
|
|
691
742
|
// Signal the pool to RESUME (REPLAY) this specific isolate
|
|
692
743
|
let _ = worker_tx.send(crate::runtime::WorkerCommand::Resume {
|
|
693
|
-
isolate_id,
|
|
694
744
|
drift_id,
|
|
695
745
|
result: res,
|
|
696
746
|
});
|
|
@@ -703,7 +753,6 @@ fn native_drift_call(scope: &mut v8::HandleScope, mut args: v8::FunctionCallback
|
|
|
703
753
|
fn native_finish_request(scope: &mut v8::HandleScope, mut args: v8::FunctionCallbackArguments, _retval: v8::ReturnValue) {
|
|
704
754
|
let request_id = args.get(0).uint32_value(scope).unwrap_or(0);
|
|
705
755
|
let result_val = args.get(1);
|
|
706
|
-
|
|
707
756
|
let json = super::v8_to_json(scope, result_val);
|
|
708
757
|
|
|
709
758
|
let runtime_ptr = unsafe { args.get_isolate() }.get_data(0) as *mut super::TitanRuntime;
|
|
@@ -104,8 +104,10 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
// Generic scanner helper
|
|
108
|
+
let scan_dir = |path: PathBuf, modules: &mut Vec<ModuleDef>, libs: &mut Vec<Library>, all_natives: &mut Vec<NativeFnEntry>| {
|
|
109
|
+
if !path.exists() { return; }
|
|
110
|
+
for entry in WalkDir::new(&path).follow_links(true).min_depth(1).max_depth(4) {
|
|
109
111
|
let entry = match entry { Ok(e) => e, Err(_) => continue };
|
|
110
112
|
if entry.file_type().is_file() && entry.file_name() == "titan.json" {
|
|
111
113
|
let dir = entry.path().parent().unwrap();
|
|
@@ -118,17 +120,28 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
118
120
|
if let Some(native_conf) = config.native {
|
|
119
121
|
let lib_path = dir.join(&native_conf.path);
|
|
120
122
|
unsafe {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
// Try loading library
|
|
124
|
+
let lib_load = Library::new(&lib_path);
|
|
125
|
+
// If failed, try resolving relative to current dir or LD_LIBRARY_PATH implicit
|
|
126
|
+
// But usually absolute path from `dir` works.
|
|
127
|
+
match lib_load {
|
|
128
|
+
Ok(lib) => {
|
|
129
|
+
for (fn_name, fn_conf) in native_conf.functions {
|
|
130
|
+
let params = fn_conf.parameters.iter().map(|p| parse_type(&p.to_lowercase())).collect();
|
|
131
|
+
let ret = parse_return(&fn_conf.result.to_lowercase());
|
|
132
|
+
if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes()) {
|
|
133
|
+
let idx = all_natives.len();
|
|
134
|
+
all_natives.push(NativeFnEntry { symbol_ptr: *symbol as usize, sig: Signature { params, ret } });
|
|
135
|
+
mod_natives_map.insert(fn_name, idx);
|
|
136
|
+
} else {
|
|
137
|
+
println!("{} {} {} -> {}", blue("[Titan]"), red("Symbol not found:"), fn_conf.symbol, config.name);
|
|
138
|
+
}
|
|
129
139
|
}
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
libs.push(lib);
|
|
141
|
+
},
|
|
142
|
+
Err(e) => {
|
|
143
|
+
println!("{} {} {} -> {:?}", blue("[Titan]"), red("Failed to load native lib:"), config.name, e);
|
|
144
|
+
}
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
}
|
|
@@ -137,7 +150,19 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
137
150
|
println!("{} {} {}", blue("[Titan]"), green("Extension loaded:"), config.name);
|
|
138
151
|
}
|
|
139
152
|
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Scan node_modules
|
|
156
|
+
if node_modules.exists() {
|
|
157
|
+
scan_dir(node_modules, &mut modules, &mut libs, &mut all_natives);
|
|
140
158
|
}
|
|
159
|
+
|
|
160
|
+
// Scan .ext (Production / Docker)
|
|
161
|
+
let ext_dir = root.join(".ext");
|
|
162
|
+
if ext_dir.exists() {
|
|
163
|
+
scan_dir(ext_dir, &mut modules, &mut libs, &mut all_natives);
|
|
164
|
+
}
|
|
165
|
+
|
|
141
166
|
*REGISTRY.lock().unwrap() = Some(Registry { _libs: libs, modules, natives: all_natives });
|
|
142
167
|
}
|
|
143
168
|
|
|
@@ -119,6 +119,13 @@ pub struct RequestData {
|
|
|
119
119
|
unsafe impl Send for TitanRuntime {}
|
|
120
120
|
unsafe impl Sync for TitanRuntime {}
|
|
121
121
|
|
|
122
|
+
impl TitanRuntime {
|
|
123
|
+
pub fn bind_to_isolate(&mut self) {
|
|
124
|
+
let ptr = self as *mut TitanRuntime as *mut std::ffi::c_void;
|
|
125
|
+
self.isolate.set_data(0, ptr);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
122
129
|
static V8_INIT: Once = Once::new();
|
|
123
130
|
|
|
124
131
|
pub fn init_v8() {
|
|
@@ -135,17 +142,14 @@ pub fn init_runtime_worker(
|
|
|
135
142
|
worker_tx: crossbeam::channel::Sender<crate::runtime::WorkerCommand>,
|
|
136
143
|
tokio_handle: tokio::runtime::Handle,
|
|
137
144
|
global_async_tx: tokio::sync::mpsc::Sender<AsyncOpRequest>,
|
|
145
|
+
stack_size: usize,
|
|
138
146
|
) -> TitanRuntime {
|
|
139
147
|
init_v8();
|
|
140
148
|
|
|
141
|
-
// Memory optimization strategy
|
|
142
|
-
// - V8 snapshots reduce memory footprint by sharing compiled code
|
|
143
|
-
// - Each isolate still has its own heap, but the snapshot reduces base overhead
|
|
144
|
-
// - For explicit heap limits, use V8 flags: --max-old-space-size=128
|
|
145
|
-
|
|
149
|
+
// Memory optimization strategy
|
|
146
150
|
let params = v8::CreateParams::default();
|
|
147
151
|
let mut isolate = v8::Isolate::new(params);
|
|
148
|
-
|
|
152
|
+
|
|
149
153
|
let (global_context, actions_map) = {
|
|
150
154
|
let handle_scope = &mut v8::HandleScope::new(&mut isolate);
|
|
151
155
|
let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
|
|
@@ -165,6 +169,7 @@ pub fn init_runtime_worker(
|
|
|
165
169
|
let action_files = scan_actions(&root);
|
|
166
170
|
for (name, path) in action_files {
|
|
167
171
|
if let Ok(code) = fs::read_to_string(&path) {
|
|
172
|
+
// Wrap action in an IIFE to capture its exports and register it globally
|
|
168
173
|
let wrapped_source =
|
|
169
174
|
format!("(function() {{ {} }})(); globalThis[\"{}\"];", code, name);
|
|
170
175
|
let source_str = v8_str(scope, &wrapped_source);
|
|
@@ -174,8 +179,22 @@ pub fn init_runtime_worker(
|
|
|
174
179
|
if val.is_function() {
|
|
175
180
|
let func = v8::Local::<v8::Function>::try_from(val).unwrap();
|
|
176
181
|
map.insert(name.clone(), v8::Global::new(try_catch, func));
|
|
182
|
+
} else if id == 0 {
|
|
183
|
+
println!("[V8] Action '{}' did not evaluate to a function: {:?}", name, val.to_rust_string_lossy(try_catch));
|
|
177
184
|
}
|
|
185
|
+
} else if id == 0 {
|
|
186
|
+
let msg = try_catch
|
|
187
|
+
.message()
|
|
188
|
+
.map(|m| m.get(try_catch).to_rust_string_lossy(try_catch))
|
|
189
|
+
.unwrap_or("Unknown run error".to_string());
|
|
190
|
+
println!("[V8] Failed to run action '{}': {}", name, msg);
|
|
178
191
|
}
|
|
192
|
+
} else if id == 0 {
|
|
193
|
+
let msg = try_catch
|
|
194
|
+
.message()
|
|
195
|
+
.map(|m| m.get(try_catch).to_rust_string_lossy(try_catch))
|
|
196
|
+
.unwrap_or("Unknown compile error".to_string());
|
|
197
|
+
println!("[V8] Failed to compile action '{}': {}", name, msg);
|
|
179
198
|
}
|
|
180
199
|
}
|
|
181
200
|
}
|
|
@@ -312,6 +331,7 @@ pub fn execute_action_optimized(
|
|
|
312
331
|
params: &[(String, String)],
|
|
313
332
|
query: &[(String, String)],
|
|
314
333
|
) {
|
|
334
|
+
// Execute action in V8
|
|
315
335
|
let context_global = runtime.context.clone();
|
|
316
336
|
let actions_map = runtime.actions.clone(); // Clone the map of globals (cheap)
|
|
317
337
|
let isolate = &mut runtime.isolate;
|
|
@@ -384,21 +404,19 @@ pub fn execute_action_optimized(
|
|
|
384
404
|
let try_catch = &mut v8::TryCatch::new(scope);
|
|
385
405
|
|
|
386
406
|
if let Some(_) = action_fn.call(try_catch, global.into(), &[req_obj.into()]) {
|
|
387
|
-
// JS side is responsible for calling t._finish_request(requestId, result)
|
|
388
|
-
// Even if the action is NOT async, our JS wrapper in titan_core.js will handle it.
|
|
389
407
|
return;
|
|
390
408
|
}
|
|
391
|
-
|
|
409
|
+
|
|
392
410
|
let msg = try_catch
|
|
393
411
|
.message()
|
|
394
412
|
.map(|m| m.get(try_catch).to_rust_string_lossy(try_catch))
|
|
395
413
|
.unwrap_or("Unknown error".to_string());
|
|
396
414
|
|
|
397
|
-
// Check for suspension
|
|
398
415
|
if msg.contains("SUSPEND") {
|
|
399
416
|
return;
|
|
400
417
|
}
|
|
401
418
|
|
|
419
|
+
println!("[Isolate {}] Action Error: {}", runtime.id, msg);
|
|
402
420
|
if let Some(tx) = runtime.pending_requests.remove(&request_id) {
|
|
403
421
|
let _ = tx.send(crate::runtime::WorkerResult {
|
|
404
422
|
json: serde_json::json!({"error": msg}),
|