@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.
Files changed (38) hide show
  1. package/index.js +44 -12
  2. package/package.json +5 -1
  3. package/templates/common/Dockerfile +60 -37
  4. package/templates/common/_dockerignore +36 -0
  5. package/templates/common/app/titan.d.ts +1 -1
  6. package/templates/js/server/src/action_management.rs +20 -14
  7. package/templates/js/server/src/extensions/builtin.rs +54 -5
  8. package/templates/js/server/src/extensions/external.rs +37 -12
  9. package/templates/js/server/src/extensions/mod.rs +28 -10
  10. package/templates/js/server/src/extensions/titan_core.js +163 -155
  11. package/templates/js/server/src/main.rs +50 -77
  12. package/templates/js/server/src/runtime.rs +141 -210
  13. package/templates/js/titan/bundle.js +4 -7
  14. package/templates/js/titan/dev.js +19 -2
  15. package/templates/js/titan/titan.js +2 -2
  16. package/templates/ts/server/src/action_management.rs +20 -14
  17. package/templates/ts/server/src/extensions/builtin.rs +54 -5
  18. package/templates/ts/server/src/extensions/external.rs +37 -12
  19. package/templates/ts/server/src/extensions/mod.rs +28 -10
  20. package/templates/ts/server/src/extensions/titan_core.js +163 -155
  21. package/templates/ts/server/src/main.rs +50 -77
  22. package/templates/ts/server/src/runtime.rs +141 -210
  23. package/templates/ts/titan/bundle.js +11 -14
  24. package/templates/ts/titan/dev.js +20 -2
  25. package/templates/ts/titan/titan.d.ts +1 -1
  26. package/templates/ts/titan/titan.js +1 -0
  27. package/titanpl-sdk/package.json +1 -1
  28. package/titanpl-sdk/templates/index.d.ts +249 -0
  29. package/titanpl-sdk/templates/server/src/action_management.rs +21 -14
  30. package/titanpl-sdk/templates/server/src/extensions/builtin.rs +469 -180
  31. package/titanpl-sdk/templates/server/src/extensions/external.rs +37 -12
  32. package/titanpl-sdk/templates/server/src/extensions/mod.rs +143 -21
  33. package/titanpl-sdk/templates/server/src/extensions/titan_core.js +179 -15
  34. package/titanpl-sdk/templates/server/src/main.rs +113 -71
  35. package/titanpl-sdk/templates/server/src/runtime.rs +172 -85
  36. package/titanpl-sdk/templates/titan/bundle.js +4 -7
  37. package/titanpl-sdk/templates/titan/titan.js +2 -2
  38. 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 Titan project
111
- ${green("titan create ext <name>")} Create new Titan extension
112
- ${green("titan dev")} Dev mode (hot reload)
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 Titan engine
116
- ${green("titan --version")} Show Titan CLI version
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
- const src = path.join(templatesRoot, srcName);
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 srcDts = path.join(templateServer, "../app/titan.d.ts");
549
- const fallbackDts = path.join(templatesRoot, "app", "titan.d.ts");
550
- const finalDtsSrc = fs.existsSync(srcDts) ? srcDts : (fs.existsSync(fallbackDts) ? fallbackDts : null);
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.0",
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 Titan (JS → Rust)
2
+ # STAGE 1 — Build TitanPl
3
3
  # ================================================================
4
- FROM rust:1.91.1 AS builder
4
+ FROM node:20.20.0-slim AS builder
5
5
 
6
- # Install Node for Titan CLI + bundler
7
- RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
8
- && apt-get install -y nodejs
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
- # Install Titan CLI (latest)
11
- RUN npm install -g @ezetgalaxy/titan@latest
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
- # Copy project files
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
- # Install JS dependencies (needed for Titan DSL + bundler)
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 -maxdepth 5 -type f -name "titan.json" -print0 | \
41
+ find /app/node_modules -type f -name "titan.json" -print0 | \
26
42
  while IFS= read -r -d '' file; do \
27
- pkg_dir="$(dirname "$file")"; \
28
- pkg_name="$(basename "$pkg_dir")"; \
29
- echo "Copying Titan extension: $pkg_name from $pkg_dir"; \
30
- cp -r "$pkg_dir" "/app/.ext/$pkg_name"; \
31
- done && \
32
- echo "Extensions in .ext:" && \
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 Image (Lightweight)
54
+ # STAGE 2 — Runtime (Render Safe)
45
55
  # ================================================================
46
- FROM debian:stable-slim
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 Rust binary from builder stage
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
- # Copy Titan routing metadata
54
- COPY --from=builder /app/server/routes.json ./routes.json
55
- COPY --from=builder /app/server/action_map.json ./action_map.json
72
+ # ---- Ensure Executable ----
73
+ RUN chmod +x ./titan-server
56
74
 
57
- # Copy Titan JS bundles
58
- RUN mkdir -p /app/actions
59
- COPY --from=builder /app/server/actions /app/actions
75
+ # ---- Create User After Copy ----
76
+ RUN useradd -m titan && chown -R titan:titan /app
77
+ USER titan
60
78
 
61
- # Copy only Titan extensions
62
- COPY --from=builder /app/.ext ./.ext
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
- CMD ["./titan-server"]
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
- project_root.join("server").join("actions"),
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 actions_dir = resolve_actions_dir();
144
- let dir = if actions_dir.exists() {
145
- actions_dir
146
- } else {
147
- match find_actions_dir(root) {
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
- let core_key = v8_str(scope, "core");
166
- t_obj.set(scope, core_key.into(), core_obj.into());
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
- if node_modules.exists() {
108
- for entry in WalkDir::new(&node_modules).follow_links(true).min_depth(1).max_depth(4) {
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
- if let Ok(lib) = Library::new(&lib_path) {
122
- for (fn_name, fn_conf) in native_conf.functions {
123
- let params = fn_conf.parameters.iter().map(|p| parse_type(&p.to_lowercase())).collect();
124
- let ret = parse_return(&fn_conf.result.to_lowercase());
125
- if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes()) {
126
- let idx = all_natives.len();
127
- all_natives.push(NativeFnEntry { symbol_ptr: *symbol as usize, sig: Signature { params, ret } });
128
- mod_natives_map.insert(fn_name, idx);
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
- libs.push(lib);
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 (v8 0.106.0 limitations):
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}),