@ezetgalaxy/titan 26.13.1 → 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 (35) hide show
  1. package/index.js +19 -10
  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 +3 -3
  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/dev.js +19 -2
  24. package/templates/ts/titan/titan.d.ts +1 -1
  25. package/templates/ts/titan/titan.js +1 -0
  26. package/titanpl-sdk/package.json +1 -1
  27. package/titanpl-sdk/templates/server/src/action_management.rs +21 -14
  28. package/titanpl-sdk/templates/server/src/extensions/builtin.rs +469 -180
  29. package/titanpl-sdk/templates/server/src/extensions/external.rs +37 -12
  30. package/titanpl-sdk/templates/server/src/extensions/mod.rs +143 -21
  31. package/titanpl-sdk/templates/server/src/extensions/titan_core.js +179 -15
  32. package/titanpl-sdk/templates/server/src/main.rs +113 -71
  33. package/titanpl-sdk/templates/server/src/runtime.rs +172 -85
  34. package/titanpl-sdk/templates/titan/bundle.js +3 -3
  35. package/titanpl-sdk/templates/titan/titan.js +2 -2
@@ -1,178 +1,186 @@
1
-
2
1
  // Titan Core Runtime JS
3
- // This is embedded in the binary for ultra-fast startup.
4
-
5
- globalThis.global = globalThis;
2
+ // Safe Bootstrap runs only once
3
+ if (!globalThis.__TITAN_CORE_LOADED__) {
4
+ globalThis.__TITAN_CORE_LOADED__ = true;
5
+
6
+ globalThis.global = globalThis;
7
+
8
+ // ensure t exists early
9
+ if (!globalThis.t) globalThis.t = {};
10
+
11
+ // -----------------------------
12
+ // defineAction identity helper
13
+ // -----------------------------
14
+ globalThis.defineAction = (fn) => {
15
+ if (fn.__titanWrapped) return fn;
16
+
17
+ const wrapped = function (req) {
18
+ const requestId = req.__titan_request_id;
19
+
20
+ const isSuspend = (err) => {
21
+ const msg = err && (err.message || String(err));
22
+ return msg && (msg.includes("__SUSPEND__") || msg.includes("SUSPEND"));
23
+ };
24
+
25
+ try {
26
+ const result = fn(req);
27
+
28
+ if (result && typeof result.then === 'function') {
29
+ result.then(
30
+ (data) => {
31
+ t._finish_request(requestId, data);
32
+ },
33
+ (err) => {
34
+ if (isSuspend(err)) return;
35
+ t._finish_request(requestId, { error: err.message || String(err) });
36
+ }
37
+ );
38
+ } else {
39
+ t._finish_request(requestId, result);
40
+ }
41
+ } catch (err) {
42
+ if (isSuspend(err)) return;
43
+ t._finish_request(requestId, { error: err.message || String(err) });
44
+ }
45
+ };
6
46
 
7
- // defineAction identity helper
8
- globalThis.defineAction = (fn) => {
9
- if (fn.__titanWrapped) return fn;
10
- const wrapped = function (req) {
11
- const requestId = req.__titan_request_id;
47
+ wrapped.__titanWrapped = true;
48
+ return wrapped;
49
+ };
12
50
 
13
- const isSuspend = (err) => {
14
- const msg = err && (err.message || String(err));
15
- return msg && (msg.includes("__SUSPEND__") || msg.includes("SUSPEND"));
16
- };
17
51
 
18
- try {
19
- const result = fn(req);
20
-
21
- if (result && typeof result.then === 'function') {
22
- // It's a Promise (or thenable)
23
- result.then(
24
- (data) => t._finish_request(requestId, data),
25
- (err) => {
26
- if (isSuspend(err)) return;
27
- t._finish_request(requestId, { error: err.message || String(err) })
28
- }
29
- );
30
- } else {
31
- // Synchronous direct return
32
- t._finish_request(requestId, result);
33
- }
34
- } catch (err) {
35
- if (isSuspend(err)) return;
36
- t._finish_request(requestId, { error: err.message || String(err) });
52
+ // -----------------------------
53
+ // TextDecoder Polyfill
54
+ // -----------------------------
55
+ globalThis.TextDecoder = class TextDecoder {
56
+ decode(buffer) {
57
+ return t.decodeUtf8(buffer);
37
58
  }
38
59
  };
39
- wrapped.__titanWrapped = true;
40
- return wrapped;
41
- };
42
-
43
- // TextDecoder Polyfill using native t.decodeUtf8
44
- globalThis.TextDecoder = class TextDecoder {
45
- decode(buffer) {
46
- return t.decodeUtf8(buffer);
47
- }
48
- };
49
-
50
- // Process environment variables
51
- globalThis.process = {
52
- env: t.loadEnv()
53
- };
54
60
 
55
- // Everything is strictly synchronous and request-driven.
61
+ // -----------------------------
62
+ // process.env
63
+ // -----------------------------
64
+ globalThis.process = {
65
+ env: t.loadEnv ? t.loadEnv() : {}
66
+ };
56
67
 
57
- function createAsyncOp(op) {
58
- return new Proxy(op, {
59
- get(target, prop) {
60
- // Internal properties accessed by drift()
61
- if (prop === "__titanAsync" || prop === "type" || prop === "data" || typeof prop === 'symbol') {
62
- return target[prop];
68
+ // -----------------------------
69
+ // Async Proxy Creator
70
+ // -----------------------------
71
+ function createAsyncOp(op) {
72
+ return new Proxy(op, {
73
+ get(target, prop) {
74
+ if (
75
+ prop === "__titanAsync" ||
76
+ prop === "type" ||
77
+ prop === "data" ||
78
+ typeof prop === 'symbol'
79
+ ) {
80
+ return target[prop];
81
+ }
82
+
83
+ throw new Error(
84
+ `[Titan Error] Accessed '${String(prop)}' without drift(). ` +
85
+ `Fix: const res = drift(t.fetch(...));`
86
+ );
63
87
  }
64
- // If they access anything else (body, status, ok, etc.), it's a mistake
65
- throw new Error(`[Titan Error] Attempted to access response property '${String(prop)}' without using drift(). \n` +
66
- `Fix: const result = drift(t.fetch(...));`);
67
- }
68
- });
69
- }
70
-
71
- // --- Response API ---
72
- const titanResponse = {
73
- json(data, status = 200, extraHeaders = {}) {
74
- return {
75
- _isResponse: true,
76
- status,
77
- headers: { "Content-Type": "application/json", ...extraHeaders },
78
- body: JSON.stringify(data)
79
- };
80
- },
81
- text(data, status = 200, extraHeaders = {}) {
82
- return {
83
- _isResponse: true,
84
- status,
85
- headers: { "Content-Type": "text/plain", ...extraHeaders },
86
- body: String(data)
87
- };
88
- },
89
- html(data, status = 200, extraHeaders = {}) {
90
- return {
91
- _isResponse: true,
92
- status,
93
- headers: { "Content-Type": "text/html", ...extraHeaders },
94
- body: String(data)
95
- };
96
- },
97
- redirect(url, status = 302, extraHeaders = {}) {
98
- return {
99
- _isResponse: true,
100
- status,
101
- headers: { "Location": url, ...extraHeaders },
102
- redirect: url
103
- };
88
+ });
104
89
  }
105
- };
106
-
107
- if (globalThis.t) {
108
- globalThis.t.response = titanResponse;
109
- } else {
110
- globalThis.t = { response: titanResponse };
111
- }
112
90
 
113
- // --- Drift Support ---
91
+ // -----------------------------
92
+ // Response API
93
+ // -----------------------------
94
+ const titanResponse = {
95
+ json(data, status = 200, extraHeaders = {}) {
96
+ return {
97
+ _isResponse: true,
98
+ status,
99
+ headers: { "Content-Type": "application/json", ...extraHeaders },
100
+ body: JSON.stringify(data)
101
+ };
102
+ },
103
+ text(data, status = 200, extraHeaders = {}) {
104
+ return {
105
+ _isResponse: true,
106
+ status,
107
+ headers: { "Content-Type": "text/plain", ...extraHeaders },
108
+ body: String(data)
109
+ };
110
+ },
111
+ html(data, status = 200, extraHeaders = {}) {
112
+ return {
113
+ _isResponse: true,
114
+ status,
115
+ headers: { "Content-Type": "text/html", ...extraHeaders },
116
+ body: String(data)
117
+ };
118
+ },
119
+ redirect(url, status = 302, extraHeaders = {}) {
120
+ return {
121
+ _isResponse: true,
122
+ status,
123
+ headers: { "Location": url, ...extraHeaders },
124
+ redirect: url
125
+ };
126
+ }
127
+ };
114
128
 
115
- globalThis.drift = function (value) {
116
- if (Array.isArray(value)) {
117
- for (const item of value) {
118
- if (!item || !item.__titanAsync) {
119
- throw new Error("drift() array must contain t.fetch/t.db.query/t.read async ops only.");
129
+ t.response = titanResponse;
130
+
131
+ // -----------------------------
132
+ // Drift Support
133
+ // -----------------------------
134
+ globalThis.drift = function (value) {
135
+ if (Array.isArray(value)) {
136
+ for (const item of value) {
137
+ if (!item || !item.__titanAsync) {
138
+ throw new Error("drift() array must contain async ops only.");
139
+ }
120
140
  }
141
+ } else if (!value || !value.__titanAsync) {
142
+ throw new Error("drift() must wrap async ops.");
121
143
  }
122
- } else if (!value || !value.__titanAsync) {
123
- throw new Error("drift() must wrap t.fetch/t.db.query/t.read async ops only.");
124
- }
125
- return t._drift_call(value);
126
- };
127
-
128
- // Wrap native fetch
129
- if (t.fetch && !t.fetch.__titanWrapped) {
130
- const nativeFetch = t.fetch;
131
- t.fetch = function (...args) {
132
- return createAsyncOp(nativeFetch(...args));
133
- };
134
- t.fetch.__titanWrapped = true;
135
- }
136
144
 
137
- // Wrap t.read (it's now async metadata)
138
- if (t.read && !t.read.__titanWrapped) {
139
- const nativeRead = t.read;
140
- t.read = function (path) {
141
- return createAsyncOp(nativeRead(path));
145
+ return t._drift_call(value);
142
146
  };
143
- t.read.__titanWrapped = true;
144
- }
145
147
 
146
- // Fix t.core.fs.read mapping
147
- if (t.core && t.core.fs) {
148
- if (t.core.fs.read && !t.core.fs.read.__titanWrapped) {
149
- const nativeFsRead = t.core.fs.read;
150
- t.core.fs.read = function (path) {
151
- return createAsyncOp(nativeFsRead(path));
148
+ // -----------------------------
149
+ // Safe Wrappers
150
+ // -----------------------------
151
+
152
+ // fetch
153
+ if (t.fetch && !t.fetch.__titanWrapped) {
154
+ const nativeFetch = t.fetch;
155
+ t.fetch = function (...args) {
156
+ return createAsyncOp(nativeFetch(...args));
152
157
  };
153
- t.core.fs.read.__titanWrapped = true;
154
- // Alias
155
- t.core.fs.readFile = t.core.fs.read;
158
+ t.fetch.__titanWrapped = true;
156
159
  }
157
- }
158
160
 
161
+ // db.connect
162
+ if (t.db && !t.db.__titanWrapped) {
163
+ const nativeDbConnect = t.db.connect;
164
+
165
+ t.db.connect = function (connString) {
166
+ const conn = nativeDbConnect(connString);
167
+
168
+ if (!conn.query.__titanWrapped) {
169
+ const nativeQuery = conn.query;
170
+ conn.query = (sql) => {
171
+ return createAsyncOp({
172
+ __titanAsync: true,
173
+ type: "db_query",
174
+ data: { conn: connString, query: sql }
175
+ });
176
+ };
177
+ conn.query.__titanWrapped = true;
178
+ }
159
179
 
180
+ return conn;
181
+ };
160
182
 
161
- // Wrap t.db.connect
162
- const nativeDbConnect = t.db.connect;
163
- t.db.connect = function (connString) {
164
- const conn = nativeDbConnect(connString);
165
- const nativeQuery = conn.query;
166
- conn.query = (sql) => {
167
- return createAsyncOp({
168
- __titanAsync: true,
169
- type: "db_query",
170
- data: {
171
- conn: connString,
172
- query: sql
173
- }
174
- });
175
- };
176
- return conn;
177
- };
183
+ t.db.__titanWrapped = true;
184
+ }
178
185
 
186
+ }
@@ -185,7 +185,7 @@ async fn dynamic_handler_inner(
185
185
  // This sends a pointer-sized message through the ring buffer, triggering
186
186
  // the V8 thread to wake up and process the request immediately.
187
187
 
188
- // Dispatch to the optimized RuntimeManager
188
+ // Dispatch to the worker pool for V8 execution
189
189
  let (mut result_json, timings) = state
190
190
  .runtime
191
191
  .execute(
@@ -198,7 +198,10 @@ async fn dynamic_handler_inner(
198
198
  query_vec
199
199
  )
200
200
  .await
201
- .unwrap_or_else(|e| (serde_json::json!({"error": e}), vec![]));
201
+ .unwrap_or_else(|e| {
202
+ // Log catastrophic runtime errors
203
+ (serde_json::json!({"error": e}), vec![])
204
+ });
202
205
 
203
206
  // Construct Server-Timing header
204
207
  let server_timing = timings.iter().enumerate().map(|(i, (name, duration))| {
@@ -210,14 +213,16 @@ async fn dynamic_handler_inner(
210
213
  obj.insert("_titanTimings".to_string(), serde_json::json!(timings));
211
214
  }
212
215
 
213
- // Prepare response
214
- let mut response = if let Some(err) = result_json.get("error") {
215
- let prefix = if !timings.is_empty() {
216
- format!("{} {}", blue("[Titan"), blue("Drift]"))
217
- } else {
218
- blue("[Titan]").to_string()
219
- };
216
+ let prefix = if !timings.is_empty() {
217
+ format!("{} {}", blue("[Titan"), blue("Drift]"))
218
+ } else {
219
+ blue("[Titan]").to_string()
220
+ };
220
221
 
222
+ // ---------------------------
223
+ // ERROR HANDLING
224
+ // ---------------------------
225
+ if let Some(err) = result_json.get("error") {
221
226
  println!(
222
227
  "{} {} {} {}",
223
228
  prefix,
@@ -225,29 +230,25 @@ async fn dynamic_handler_inner(
225
230
  red("→ error"),
226
231
  gray(&format!("in {:.2?}", start.elapsed()))
227
232
  );
228
- println!(
229
- "{} {} {} {}",
233
+ println!(
234
+ "{} {} {}",
230
235
  prefix,
231
236
  red("Action Error:"),
232
- red(err.as_str().unwrap_or("Unknown")),
233
- gray(&format!("in {:.2?}", start.elapsed()))
237
+ red(err.as_str().unwrap_or("Unknown"))
234
238
  );
235
- (StatusCode::INTERNAL_SERVER_ERROR, Json(result_json.clone())).into_response()
236
- } else if let Some(is_resp) = result_json.get("_isResponse") {
237
- if is_resp.as_bool().unwrap_or(false) {
238
- let status_u16 = match result_json.get("status") {
239
- Some(Value::Number(n)) => {
240
- if let Some(u) = n.as_u64() {
241
- u as u16
242
- } else if let Some(f) = n.as_f64() {
243
- f as u16
244
- } else {
245
- 200
246
- }
247
- }
248
- _ => 200,
249
- };
239
+ let mut response = (StatusCode::INTERNAL_SERVER_ERROR, Json(result_json.clone())).into_response();
240
+ if !server_timing.is_empty() {
241
+ response.headers_mut().insert("Server-Timing", server_timing.parse().unwrap());
242
+ }
243
+ return response;
244
+ }
250
245
 
246
+ // ---------------------------
247
+ // RESPONSE CONSTRUCTION
248
+ // ---------------------------
249
+ let mut response = if let Some(is_resp) = result_json.get("_isResponse") {
250
+ if is_resp.as_bool().unwrap_or(false) {
251
+ let status_u16 = result_json.get("status").and_then(|v| v.as_u64()).unwrap_or(200) as u16;
251
252
  let status = StatusCode::from_u16(status_u16).unwrap_or(StatusCode::OK);
252
253
  let mut builder = axum::http::Response::builder().status(status);
253
254
 
@@ -260,31 +261,22 @@ async fn dynamic_handler_inner(
260
261
  }
261
262
 
262
263
  let mut is_redirect = false;
263
-
264
264
  if let Some(location) = result_json.get("redirect") {
265
265
  if let Some(url) = location.as_str() {
266
266
  let mut final_status_u16 = status.as_u16();
267
- // If it's a redirect call, ensure we use a 3xx status
268
- if final_status_u16 < 300 || final_status_u16 > 399 {
269
- final_status_u16 = 302; // Default to 302 Found
270
- }
271
-
272
- builder = builder.status(StatusCode::from_u16(final_status_u16).unwrap_or(StatusCode::FOUND))
273
- .header("Location", url);
267
+ if final_status_u16 < 300 || final_status_u16 > 399 { final_status_u16 = 302; }
268
+ builder = builder.status(StatusCode::from_u16(final_status_u16).unwrap_or(StatusCode::FOUND)).header("Location", url);
274
269
  is_redirect = true;
275
270
  }
276
271
  }
277
272
 
278
- let body_text = if is_redirect {
279
- "".to_string()
280
- } else {
273
+ let body_text = if is_redirect { "".to_string() } else {
281
274
  match result_json.get("body") {
282
275
  Some(Value::String(s)) => s.clone(),
283
276
  Some(v) => v.to_string(),
284
277
  None => "".to_string(),
285
278
  }
286
279
  };
287
-
288
280
  builder.body(Body::from(body_text)).unwrap()
289
281
  } else {
290
282
  Json(result_json.clone()).into_response()
@@ -293,7 +285,6 @@ async fn dynamic_handler_inner(
293
285
  Json(result_json.clone()).into_response()
294
286
  };
295
287
 
296
- // Add Server-Timing Header
297
288
  if !server_timing.is_empty() {
298
289
  response.headers_mut().insert("Server-Timing", server_timing.parse().unwrap());
299
290
  }
@@ -303,20 +294,9 @@ async fn dynamic_handler_inner(
303
294
  // ---------------------------
304
295
  let total_elapsed = start.elapsed();
305
296
  let total_elapsed_ms = total_elapsed.as_secs_f64() * 1000.0;
306
-
307
- let total_drift_ms: f64 = timings.iter()
308
- .filter(|(n, _)| n == "drift" || n == "drift_error")
309
- .map(|(_, d)| d)
310
- .sum();
311
-
297
+ let total_drift_ms: f64 = timings.iter().filter(|(n, _)| n == "drift" || n == "drift_error").map(|(_, d)| d).sum();
312
298
  let compute_ms = (total_elapsed_ms - total_drift_ms).max(0.0);
313
299
 
314
- let prefix = if !timings.is_empty() {
315
- format!("{} {}", blue("[Titan"), blue("Drift]"))
316
- } else {
317
- blue("[Titan]").to_string()
318
- };
319
-
320
300
  let timing_info = if !timings.is_empty() {
321
301
  gray(&format!("(active: {:.2}ms, drift: {:.2}ms) in {:.2?}", compute_ms, total_drift_ms, total_elapsed))
322
302
  } else {
@@ -324,23 +304,8 @@ async fn dynamic_handler_inner(
324
304
  };
325
305
 
326
306
  match route_kind {
327
- "dynamic" => println!(
328
- "{} {} {} {} {} {}",
329
- prefix,
330
- green(&format!("{} {}", method, path)),
331
- white("→"),
332
- green(&route_label),
333
- white("(dynamic)"),
334
- timing_info
335
- ),
336
- "exact" => println!(
337
- "{} {} {} {} {}",
338
- prefix,
339
- white(&format!("{} {}", method, path)),
340
- white("→"),
341
- yellow(&route_label),
342
- timing_info
343
- ),
307
+ "dynamic" => println!("{} {} {} {} {} {}", prefix, green(&format!("{} {}", method, path)), white("→"), green(&route_label), white("(dynamic)"), timing_info),
308
+ "exact" => println!("{} {} {} {} {}", prefix, white(&format!("{} {}", method, path)), white("→"), yellow(&route_label), timing_info),
344
309
  _ => {}
345
310
  }
346
311
 
@@ -358,18 +323,23 @@ async fn main() -> Result<()> {
358
323
  let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
359
324
  let json: Value = serde_json::from_str(&raw).unwrap_or_default();
360
325
 
361
- let port = json["__config"]["port"].as_u64().unwrap_or(3000);
326
+ let port = std::env::var("PORT")
327
+ .ok()
328
+ .and_then(|p| p.parse::<u64>().ok())
329
+ .or_else(|| json["__config"]["port"].as_u64())
330
+ .unwrap_or(3000);
362
331
  let thread_count = json["__config"]["threads"].as_u64();
363
332
  let routes_json = json["routes"].clone();
364
333
  let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
365
334
  let dynamic_routes: Vec<DynamicRoute> =
366
335
  serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
367
336
 
368
- // Identify project root (where .ext or node_modules lives)
337
+ // Identify project root
369
338
  let project_root = resolve_project_root();
370
-
371
- // Load extensions (Load definitions globally)
339
+
340
+ // Load extensions and action definitions
372
341
  extensions::load_project_extensions(project_root.clone());
342
+
373
343
 
374
344
  // Initialize Runtime Manager (Worker Pool)
375
345
  let threads = match thread_count {
@@ -377,8 +347,10 @@ async fn main() -> Result<()> {
377
347
  _ => num_cpus::get() * 4, // default
378
348
  };
379
349
 
350
+ let stack_mb = json["__config"]["stack_mb"].as_u64().unwrap_or(8);
351
+ let stack_size = (stack_mb as usize) * 1024 * 1024;
380
352
 
381
- let runtime_manager = Arc::new(RuntimeManager::new(project_root.clone(), threads));
353
+ let runtime_manager = Arc::new(RuntimeManager::new(project_root.clone(), threads, stack_size));
382
354
 
383
355
  let state = AppState {
384
356
  routes: Arc::new(map),
@@ -395,9 +367,10 @@ async fn main() -> Result<()> {
395
367
 
396
368
 
397
369
  println!(
398
- "\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{} \x1b[90m(Threads: {})\x1b[0m",
370
+ "\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{} \x1b[90m(Threads: {}, Stack: {}MB)\x1b[0m",
399
371
  port,
400
- threads
372
+ threads,
373
+ stack_mb
401
374
  );
402
375
 
403
376