titanpl-sdk 2.0.0 → 2.0.2

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.
@@ -51,7 +51,9 @@ async fn dynamic_handler_inner(
51
51
  // ---------------------------
52
52
  let method = req.method().as_str().to_uppercase();
53
53
  let path = req.uri().path().to_string();
54
- let key = format!("{}:{}", method, path);
54
+ let strict_key = format!("{}:{}", method, path);
55
+ // Also try simple path for generic routes
56
+ // Check strict first, then simple path
55
57
 
56
58
  // ---------------------------
57
59
  // TIMER + LOG META
@@ -101,7 +103,8 @@ async fn dynamic_handler_inner(
101
103
  let mut action_name: Option<String> = None;
102
104
 
103
105
  // Exact route
104
- if let Some(route) = state.routes.get(&key) {
106
+ let route = state.routes.get(&strict_key).or_else(|| state.routes.get(&path));
107
+ if let Some(route) = route {
105
108
  route_kind = "exact";
106
109
  if route.r#type == "action" {
107
110
  let name = route.value.as_str().unwrap_or("unknown").to_string();
@@ -182,7 +185,8 @@ async fn dynamic_handler_inner(
182
185
  // This sends a pointer-sized message through the ring buffer, triggering
183
186
  // the V8 thread to wake up and process the request immediately.
184
187
 
185
- let result_json = state
188
+ // Dispatch to the worker pool for V8 execution
189
+ let (mut result_json, timings) = state
186
190
  .runtime
187
191
  .execute(
188
192
  action_name,
@@ -194,88 +198,118 @@ async fn dynamic_handler_inner(
194
198
  query_vec
195
199
  )
196
200
  .await
197
- .unwrap_or_else(|e| serde_json::json!({"error": e}));
201
+ .unwrap_or_else(|e| {
202
+ // Log catastrophic runtime errors
203
+ (serde_json::json!({"error": e}), vec![])
204
+ });
205
+
206
+ // Construct Server-Timing header
207
+ let server_timing = timings.iter().enumerate().map(|(i, (name, duration))| {
208
+ format!("{}_{};dur={:.2}", name, i, duration)
209
+ }).collect::<Vec<_>>().join(", ");
210
+
211
+ // Inject timings into JSON if it's an object
212
+ if let Some(obj) = result_json.as_object_mut() {
213
+ obj.insert("_titanTimings".to_string(), serde_json::json!(timings));
214
+ }
198
215
 
216
+ let prefix = if !timings.is_empty() {
217
+ format!("{} {}", blue("[Titan"), blue("Drift]"))
218
+ } else {
219
+ blue("[Titan]").to_string()
220
+ };
199
221
 
200
222
  // ---------------------------
201
- // FINAL LOG
223
+ // ERROR HANDLING
202
224
  // ---------------------------
203
- let elapsed = start.elapsed();
204
-
205
- // Check for errors in result
206
225
  if let Some(err) = result_json.get("error") {
207
226
  println!(
208
227
  "{} {} {} {}",
209
- blue("[Titan]"),
228
+ prefix,
210
229
  red(&format!("{} {}", method, path)),
211
230
  red("→ error"),
212
- gray(&format!("in {:.2?}", elapsed))
231
+ gray(&format!("in {:.2?}", start.elapsed()))
213
232
  );
214
- println!(
215
- "{} {} {} {}",
216
- blue("[Titan]"),
233
+ println!(
234
+ "{} {} {}",
235
+ prefix,
217
236
  red("Action Error:"),
218
- red(err.as_str().unwrap_or("Unknown")),
219
- gray(&format!("in {:.2?}", elapsed))
237
+ red(err.as_str().unwrap_or("Unknown"))
220
238
  );
221
- return (StatusCode::INTERNAL_SERVER_ERROR, Json(result_json)).into_response();
222
- }
223
-
224
- match route_kind {
225
- "dynamic" => println!(
226
- "{} {} {} {} {} {}",
227
- blue("[Titan]"),
228
- green(&format!("{} {}", method, path)),
229
- white("→"),
230
- green(&route_label),
231
- white("(dynamic)"),
232
- gray(&format!("in {:.2?}", elapsed))
233
- ),
234
- "exact" => println!(
235
- "{} {} {} {} {}",
236
- blue("[Titan]"),
237
- white(&format!("{} {}", method, path)),
238
- white("→"),
239
- yellow(&route_label),
240
- gray(&format!("in {:.2?}", elapsed))
241
- ),
242
- _ => {}
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;
243
244
  }
244
245
 
245
- // --------------------------------------------------------------
246
- // Titan Response Contract: Custom HTTP Handling
247
- // --------------------------------------------------------------
248
- if let Some(is_resp) = result_json.get("_isResponse") {
249
- if is_resp.as_bool().unwrap_or(false) {
250
- let status = result_json
251
- .get("status")
252
- .and_then(|v| v.as_u64())
253
- .unwrap_or(200) as u16;
254
-
255
- let status = StatusCode::from_u16(status).unwrap_or(StatusCode::OK);
256
-
257
- let mut builder = axum::http::Response::builder().status(status);
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;
252
+ let status = StatusCode::from_u16(status_u16).unwrap_or(StatusCode::OK);
253
+ let mut builder = axum::http::Response::builder().status(status);
254
+
255
+ if let Some(hmap) = result_json.get("headers").and_then(|v| v.as_object()) {
256
+ for (k, v) in hmap {
257
+ if let Some(vs) = v.as_str() {
258
+ builder = builder.header(k, vs);
259
+ }
260
+ }
261
+ }
258
262
 
259
- if let Some(hmap) = result_json.get("headers").and_then(|v| v.as_object()) {
260
- for (k, v) in hmap {
261
- if let Some(vs) = v.as_str() {
262
- builder = builder.header(k, vs);
263
+ let mut is_redirect = false;
264
+ if let Some(location) = result_json.get("redirect") {
265
+ if let Some(url) = location.as_str() {
266
+ let mut final_status_u16 = status.as_u16();
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);
269
+ is_redirect = true;
263
270
  }
264
271
  }
272
+
273
+ let body_text = if is_redirect { "".to_string() } else {
274
+ match result_json.get("body") {
275
+ Some(Value::String(s)) => s.clone(),
276
+ Some(v) => v.to_string(),
277
+ None => "".to_string(),
278
+ }
279
+ };
280
+ builder.body(Body::from(body_text)).unwrap()
281
+ } else {
282
+ Json(result_json.clone()).into_response()
265
283
  }
284
+ } else {
285
+ Json(result_json.clone()).into_response()
286
+ };
287
+
288
+ if !server_timing.is_empty() {
289
+ response.headers_mut().insert("Server-Timing", server_timing.parse().unwrap());
290
+ }
266
291
 
267
- // FIX: Safe body conversion
268
- let body_text = match result_json.get("body") {
269
- Some(Value::String(s)) => s.clone(),
270
- Some(v) => v.to_string(),
271
- None => "".to_string(),
272
- };
292
+ // ---------------------------
293
+ // FINAL LOG (SUCCESS)
294
+ // ---------------------------
295
+ let total_elapsed = start.elapsed();
296
+ let total_elapsed_ms = total_elapsed.as_secs_f64() * 1000.0;
297
+ let total_drift_ms: f64 = timings.iter().filter(|(n, _)| n == "drift" || n == "drift_error").map(|(_, d)| d).sum();
298
+ let compute_ms = (total_elapsed_ms - total_drift_ms).max(0.0);
299
+
300
+ let timing_info = if !timings.is_empty() {
301
+ gray(&format!("(active: {:.2}ms, drift: {:.2}ms) in {:.2?}", compute_ms, total_drift_ms, total_elapsed))
302
+ } else {
303
+ gray(&format!("in {:.2?}", total_elapsed))
304
+ };
273
305
 
274
- return builder.body(Body::from(body_text)).unwrap();
306
+ match route_kind {
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),
309
+ _ => {}
275
310
  }
276
- }
277
311
 
278
- Json(result_json).into_response()
312
+ response
279
313
  }
280
314
 
281
315
 
@@ -284,23 +318,28 @@ if let Some(is_resp) = result_json.get("_isResponse") {
284
318
  #[tokio::main]
285
319
  async fn main() -> Result<()> {
286
320
  dotenvy::dotenv().ok();
287
-
321
+
288
322
  // Load routes.json
289
323
  let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
290
324
  let json: Value = serde_json::from_str(&raw).unwrap_or_default();
291
325
 
292
- 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);
293
331
  let thread_count = json["__config"]["threads"].as_u64();
294
332
  let routes_json = json["routes"].clone();
295
333
  let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
296
334
  let dynamic_routes: Vec<DynamicRoute> =
297
335
  serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
298
336
 
299
- // Identify project root (where .ext or node_modules lives)
337
+ // Identify project root
300
338
  let project_root = resolve_project_root();
301
-
302
- // Load extensions (Load definitions globally)
339
+
340
+ // Load extensions and action definitions
303
341
  extensions::load_project_extensions(project_root.clone());
342
+
304
343
 
305
344
  // Initialize Runtime Manager (Worker Pool)
306
345
  let threads = match thread_count {
@@ -308,8 +347,10 @@ async fn main() -> Result<()> {
308
347
  _ => num_cpus::get() * 4, // default
309
348
  };
310
349
 
350
+ let stack_mb = json["__config"]["stack_mb"].as_u64().unwrap_or(8);
351
+ let stack_size = (stack_mb as usize) * 1024 * 1024;
311
352
 
312
- 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));
313
354
 
314
355
  let state = AppState {
315
356
  routes: Arc::new(map),
@@ -326,9 +367,10 @@ async fn main() -> Result<()> {
326
367
 
327
368
 
328
369
  println!(
329
- "\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",
330
371
  port,
331
- threads
372
+ threads,
373
+ stack_mb
332
374
  );
333
375
 
334
376
 
@@ -1,104 +1,139 @@
1
- use std::thread;
1
+ use bytes::Bytes;
2
2
  use crossbeam::channel::{bounded, Sender};
3
+ use std::thread;
4
+ use std::sync::atomic::{AtomicUsize, Ordering};
5
+ use tokio::sync::mpsc;
3
6
  use tokio::sync::oneshot;
4
- use bytes::Bytes;
5
7
  use smallvec::SmallVec;
6
- use crate::extensions;
7
8
 
8
- // ----------------------------------------------------------------------------
9
- // TITANVM: HIGH-PERFORMANCE WORKER POOL
10
- // ----------------------------------------------------------------------------
9
+ use crate::extensions::{self, TitanRuntime, AsyncOpRequest, WorkerAsyncResult};
10
+
11
+ pub struct RuntimeManager {
12
+ request_txs: Vec<Sender<WorkerCommand>>,
13
+ round_robin_counter: AtomicUsize,
14
+ _resume_txs: Vec<Sender<WorkerCommand>>, // Keep alive
15
+ _workers: Vec<thread::JoinHandle<()>>,
16
+ }
11
17
 
12
- /// The command sent from the Async Axum thread to the Sync V8 Worker thread.
13
- ///
14
- /// IMPLEMENTATION NOTE: Zero-Copy Design
15
- /// Instead of passing `String` or `Vec<u8>` which incur heap allocations for every request,
16
- /// we use:
17
- /// 1. `Bytes`: An Arc-counted slice of the original TCP buffer. Cloning this is O(1).
18
- /// 2. `SmallVec`: Stack-allocated vectors for headers/params. 99% of requests fit in standard limits
19
- /// (8 headers, 4 params), avoiding malloc/free overhead entirely.
20
18
  pub enum WorkerCommand {
21
19
  Request(RequestTask),
20
+ Resume {
21
+ drift_id: u32,
22
+ result: WorkerAsyncResult,
23
+ },
22
24
  }
23
25
 
26
+ #[allow(dead_code)]
24
27
  pub struct RequestTask {
25
28
  pub action_name: String,
26
-
27
- // Zero-copy body (Arc-based byte slice)
28
- // This slice points directly into the Hyper/Tokio TCP buffer.
29
- // It is passed to V8 as an ArrayBuffer BackingStore without copying.
30
- pub body: Option<Bytes>,
31
-
32
- // Efficient Metadata (No JSON)
29
+ pub body: Option<Bytes>,
33
30
  pub method: String,
34
31
  pub path: String,
35
-
36
- // SmallVec<[T; N]> stores N items inline on the struct (stack memory).
37
- // Only unnecessary heap allocation occurs if headers > 8.
38
32
  pub headers: SmallVec<[(String, String); 8]>,
39
33
  pub params: SmallVec<[(String, String); 4]>,
40
34
  pub query: SmallVec<[(String, String); 4]>,
41
-
42
- // Response channel
43
- // Used to signal the Async Runtime when the Sync V8 work is done.
44
35
  pub response_tx: oneshot::Sender<WorkerResult>,
45
36
  }
46
37
 
47
-
48
38
  pub struct WorkerResult {
49
39
  pub json: serde_json::Value,
50
- }
51
-
52
- pub struct RuntimeManager {
53
- sender: Sender<WorkerCommand>,
54
- _workers: Vec<thread::JoinHandle<()>>,
40
+ pub timings: Vec<(String, f64)>,
55
41
  }
56
42
 
57
43
  impl RuntimeManager {
58
- pub fn new(project_root: std::path::PathBuf, num_threads: usize) -> Self {
59
- let (tx, rx) = bounded::<WorkerCommand>(num_threads * 2000);
44
+ pub fn new(project_root: std::path::PathBuf, num_threads: usize, stack_size: usize) -> Self {
45
+ let (async_tx, mut async_rx) = mpsc::channel::<AsyncOpRequest>(1000);
60
46
 
47
+ let tokio_handle = tokio::runtime::Handle::current();
48
+
49
+ // Spawn Tokio Async Handler
50
+ tokio_handle.spawn(async move {
51
+ while let Some(req) = async_rx.recv().await {
52
+ let drift_id = req.drift_id;
53
+ let respond_tx = req.respond_tx;
54
+ tokio::spawn(async move {
55
+ let start = std::time::Instant::now();
56
+ let result = extensions::builtin::run_async_operation(req.op).await;
57
+ let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
58
+ let _ = respond_tx.send(WorkerAsyncResult {
59
+ drift_id,
60
+ result,
61
+ duration_ms,
62
+ });
63
+ });
64
+ }
65
+ });
66
+
67
+ let mut worker_txs = Vec::new();
61
68
  let mut workers = Vec::new();
62
-
63
- for i in 0..num_threads {
64
- let rx_clone = rx.clone();
65
- let tx_clone = tx.clone();
66
- let root_clone = project_root.clone();
69
+
70
+ // Pass 1: Create channels
71
+ for _ in 0..num_threads {
72
+ let (tx, rx) = bounded(100);
73
+ worker_txs.push((tx, rx));
74
+ }
75
+
76
+ let mut final_txs = Vec::new();
77
+ for (tx, _) in &worker_txs {
78
+ final_txs.push(tx.clone());
79
+ }
80
+
81
+ // Pass 2: Spawn Workers
82
+ for (i, (tx, rx)) in worker_txs.into_iter().enumerate() {
83
+ let my_tx = tx.clone(); // The worker needs a way to send commands to ITSELF (for resumes)
84
+ let root = project_root.clone();
85
+ let handle = tokio_handle.clone();
86
+ let async_tx = async_tx.clone();
67
87
 
68
88
  let handle = thread::Builder::new()
69
89
  .name(format!("titan-worker-{}", i))
90
+ .stack_size(stack_size)
70
91
  .spawn(move || {
71
- // 1. Thread-Local Event Loop Init
72
- // Initialize independent V8 Isolate for this thread
73
- let mut runtime = extensions::init_runtime_worker(root_clone, tx_clone);
92
+ // Start a thread with a pinned V8 isolate.
93
+ // This thread will handle requests for this isolate exclusively.
94
+ let mut rt = extensions::init_runtime_worker(
95
+ i,
96
+ root,
97
+ my_tx,
98
+ handle,
99
+ async_tx,
100
+ stack_size
101
+ );
74
102
 
75
- // 2. Event Loop
76
- let runtime_ptr = &mut runtime as *mut extensions::TitanRuntime as *mut std::ffi::c_void;
77
- runtime.isolate.set_data(0, runtime_ptr);
103
+ // Bind the runtime instance to the V8 isolate data slot
104
+ // This is CRITICAL because native drift calls use this pointer.
105
+ rt.bind_to_isolate();
78
106
 
79
107
  loop {
80
- // Strictly synchronous blocking receive
81
- // This makes the worker a dedicated processor for requests from the queue.
82
- // No background event loop is running.
83
- if let Ok(cmd) = rx_clone.recv() {
84
- handle_cmd(cmd, &mut runtime);
85
- } else {
86
- break;
108
+ match rx.recv() {
109
+ Ok(cmd) => {
110
+ match cmd {
111
+ WorkerCommand::Request(task) => {
112
+ handle_new_request(task, &mut rt);
113
+ },
114
+ WorkerCommand::Resume { drift_id, result } => {
115
+ handle_resume(drift_id, result, &mut rt);
116
+ }
117
+ }
118
+ }
119
+ Err(_) => break, // Channel closed
87
120
  }
88
121
  }
89
122
  })
90
- .expect("Failed to spawn worker thread");
91
-
123
+ .expect("Failed to spawn worker");
124
+
92
125
  workers.push(handle);
93
126
  }
94
127
 
95
128
  Self {
96
- sender: tx,
129
+ request_txs: final_txs.clone(),
130
+ round_robin_counter: AtomicUsize::new(0),
131
+ _resume_txs: final_txs,
97
132
  _workers: workers,
98
133
  }
99
- }
134
+
135
+ }
100
136
 
101
- // Optimized Execute method (Takes maps/vecs instead of JSON strings)
102
137
  pub async fn execute(
103
138
  &self,
104
139
  action: String,
@@ -108,9 +143,8 @@ impl RuntimeManager {
108
143
  headers: SmallVec<[(String, String); 8]>,
109
144
  params: SmallVec<[(String, String); 4]>,
110
145
  query: SmallVec<[(String, String); 4]>,
111
- ) -> Result<serde_json::Value, String> {
146
+ ) -> Result<(serde_json::Value, Vec<(String, f64)>), String> {
112
147
  let (tx, rx) = oneshot::channel();
113
-
114
148
  let task = RequestTask {
115
149
  action_name: action,
116
150
  body,
@@ -122,37 +156,90 @@ impl RuntimeManager {
122
156
  response_tx: tx,
123
157
  };
124
158
 
125
- // Dispatch to RingBuffer/Channel
126
- self.sender.send(WorkerCommand::Request(task)).map_err(|e| e.to_string())?;
159
+ // Round Robin Distribution
160
+ let idx = self.round_robin_counter.fetch_add(1, Ordering::Relaxed) % self.request_txs.len();
161
+ self.request_txs[idx].send(WorkerCommand::Request(task)).map_err(|e| e.to_string())?;
127
162
 
128
- // Await Result (Async-Sync Bridge)
129
163
  match rx.await {
130
- Ok(res) => Ok(res.json),
164
+ Ok(res) => Ok((res.json, res.timings)),
131
165
  Err(_) => Err("Worker channel closed".to_string()),
132
166
  }
133
167
  }
168
+ }
169
+
170
+ // ----------------------------------------------------------------------------
171
+ // HANDLERS (Simpler - No Mutex/Vec lookup)
172
+ // ----------------------------------------------------------------------------
134
173
 
174
+ fn handle_new_request(task: RequestTask, rt: &mut TitanRuntime) {
175
+ rt.request_counter += 1;
176
+ let request_id = rt.request_counter;
177
+ rt.pending_requests.insert(request_id, task.response_tx);
178
+
179
+ let req_data = extensions::RequestData {
180
+ action_name: task.action_name.clone(),
181
+ body: task.body.clone(),
182
+ method: task.method.clone(),
183
+ path: task.path.clone(),
184
+ headers: task.headers.iter().map(|(k,v)| (k.clone(), v.clone())).collect(),
185
+ params: task.params.iter().map(|(k,v)| (k.clone(), v.clone())).collect(),
186
+ query: task.query.iter().map(|(k,v)| (k.clone(), v.clone())).collect(),
187
+ };
188
+ rt.active_requests.insert(request_id, req_data);
189
+ let drift_count = rt.drift_counter;
190
+ rt.request_start_counters.insert(request_id, drift_count);
191
+
192
+ extensions::execute_action_optimized(
193
+ rt,
194
+ request_id,
195
+ &task.action_name,
196
+ task.body,
197
+ &task.method,
198
+ &task.path,
199
+ &task.headers,
200
+ &task.params,
201
+ &task.query
202
+ );
203
+
204
+ // Cleanup if sync
205
+ if !rt.pending_requests.contains_key(&request_id) {
206
+ rt.active_requests.remove(&request_id);
207
+ rt.request_start_counters.remove(&request_id);
208
+ }
135
209
  }
136
210
 
137
- fn handle_cmd(cmd: WorkerCommand, runtime: &mut extensions::TitanRuntime) {
138
-
139
- match cmd {
140
- WorkerCommand::Request(task) => {
141
- // 3. Execution (Zero-Copy)
142
- let result = extensions::execute_action_optimized(
143
- runtime,
144
- &task.action_name,
145
- task.body,
146
- &task.method,
147
- &task.path,
148
- &task.headers,
149
- &task.params,
150
- &task.query
151
- );
152
-
153
- let _ = task.response_tx.send(WorkerResult {
154
- json: result,
155
- });
156
- }
157
- }
211
+ fn handle_resume(drift_id: u32, result: WorkerAsyncResult, rt: &mut TitanRuntime) {
212
+ // 1. Identify which request this drift belongs to
213
+ let req_id = rt.drift_to_request.get(&drift_id).copied().unwrap_or(0);
214
+
215
+ // 2. Perform Timing
216
+ let timing_type = if result.result.get("error").is_some() { "drift_error" } else { "drift" };
217
+ rt.request_timings.entry(req_id).or_default().push((timing_type.to_string(), result.duration_ms));
218
+
219
+ // 3. Store Result for Replay
220
+ rt.completed_drifts.insert(drift_id, result.result);
221
+
222
+ // 4. Trigger Replay
223
+ if let Some(req_data) = rt.active_requests.get(&req_id).cloned() {
224
+ let start_counter = rt.request_start_counters.get(&req_id).copied().unwrap_or(0);
225
+ rt.drift_counter = start_counter;
226
+
227
+ extensions::execute_action_optimized(
228
+ rt,
229
+ req_id,
230
+ &req_data.action_name,
231
+ req_data.body,
232
+ &req_data.method,
233
+ &req_data.path,
234
+ &req_data.headers,
235
+ &req_data.params,
236
+ &req_data.query
237
+ );
238
+ }
239
+
240
+ // 5. Cleanup
241
+ if req_id != 0 && !rt.pending_requests.contains_key(&req_id) {
242
+ rt.active_requests.remove(&req_id);
243
+ rt.request_start_counters.remove(&req_id);
244
+ }
158
245
  }
@@ -158,7 +158,7 @@ export async function bundleFile(options) {
158
158
  export async function bundle() {
159
159
  const root = process.cwd();
160
160
  const actionsDir = path.join(root, 'app', 'actions');
161
- const bundleDir = path.join(root, 'server', 'actions');
161
+ const bundleDir = path.join(root, 'server', 'src', 'actions');
162
162
 
163
163
  // Ensure bundle directory exists and is clean
164
164
  if (fs.existsSync(bundleDir)) {
@@ -197,7 +197,7 @@ export async function bundle() {
197
197
  minify: false,
198
198
  sourcemap: false,
199
199
  banner: {
200
- js: "const defineAction = (fn) => fn; const Titan = t;"
200
+ js: "var Titan = t;"
201
201
  },
202
202
  footer: {
203
203
  js: `
@@ -261,4 +261,4 @@ export async function bundle() {
261
261
  throw new Error('__TITAN_BUNDLE_FAILED__');
262
262
  }
263
263
  }
264
- }
264
+ }
@@ -72,7 +72,7 @@ const t = {
72
72
  * RULE: Only calls bundle() - does NOT handle esbuild errors
73
73
  * RULE: If bundle throws __TITAN_BUNDLE_FAILED__, stop immediately without printing
74
74
  */
75
- async start(port = 3000, msg = "", threads) {
75
+ async start(port = 3000, msg = "", threads, stack_mb = 8) {
76
76
  try {
77
77
  console.log(cyan("[Titan] Preparing runtime..."));
78
78
 
@@ -91,7 +91,7 @@ const t = {
91
91
  routesPath,
92
92
  JSON.stringify(
93
93
  {
94
- __config: { port, threads },
94
+ __config: { port, threads, stack_mb },
95
95
  routes,
96
96
  __dynamic_routes: Object.values(dynamicRoutes).flat()
97
97
  },