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.
- package/bin/run.js +16 -16
- package/package.json +1 -1
- package/templates/server/src/action_management.rs +21 -14
- package/templates/server/src/extensions/builtin.rs +469 -180
- package/templates/server/src/extensions/external.rs +112 -17
- package/templates/server/src/extensions/mod.rs +143 -21
- package/templates/server/src/extensions/titan_core.js +179 -15
- package/templates/server/src/main.rs +113 -71
- package/templates/server/src/runtime.rs +172 -85
- package/templates/titan/bundle.js +3 -3
- package/templates/titan/titan.js +2 -2
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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|
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
"{} {} {}
|
|
216
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
//
|
|
247
|
-
//
|
|
248
|
-
if let Some(is_resp) = result_json.get("_isResponse") {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
if let Some(
|
|
262
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
337
|
+
// Identify project root
|
|
300
338
|
let project_root = resolve_project_root();
|
|
301
|
-
|
|
302
|
-
// Load extensions
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
let
|
|
66
|
-
|
|
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
|
-
//
|
|
72
|
-
//
|
|
73
|
-
let mut
|
|
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
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
91
|
-
|
|
123
|
+
.expect("Failed to spawn worker");
|
|
124
|
+
|
|
92
125
|
workers.push(handle);
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
Self {
|
|
96
|
-
|
|
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
|
-
//
|
|
126
|
-
self.
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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: "
|
|
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
|
+
}
|
package/templates/titan/titan.js
CHANGED
|
@@ -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
|
},
|