titanpl-sdk 2.0.3 → 2.0.4
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/README.md +2 -0
- package/package.json +38 -34
- package/templates/app/app.js +1 -4
- package/templates/server/Cargo.toml +22 -2
- package/templates/server/src/action_management.rs +8 -10
- package/templates/server/src/errors.rs +3 -1
- package/templates/server/src/extensions/builtin.rs +1038 -877
- package/templates/server/src/extensions/external.rs +338 -404
- package/templates/server/src/extensions/mod.rs +580 -448
- package/templates/server/src/extensions/titan_core.js +249 -186
- package/templates/server/src/fast_path.rs +719 -0
- package/templates/server/src/main.rs +370 -169
- package/templates/server/src/runtime.rs +284 -245
- package/templates/server/src/utils.rs +2 -2
- package/templates/titan/bundle.js +259 -264
- package/templates/titan/dev.js +46 -6
- package/templates/titan/error-box.js +277 -268
- package/templates/app/titan.d.ts +0 -87
- package/templates/index.d.ts +0 -249
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
//! Titan HTTP Server (Performance Optimized)
|
|
2
|
+
//!
|
|
3
|
+
//! Key Features:
|
|
4
|
+
//! 1. Fast-path integration: static actions bypass V8 entirely.
|
|
5
|
+
//! 2. Pre-computed route responses: reply routes serve cached bytes.
|
|
6
|
+
//! 3. Benchmark mode: `TITAN_BENCHMARK=1` disables per-request logging & timings.
|
|
7
|
+
//! 4. Early fast-path check BEFORE body/header parsing.
|
|
8
|
+
//! 5. Mimalloc global allocator for faster allocations.
|
|
9
|
+
//! 6. Optimized response construction.
|
|
10
|
+
|
|
1
11
|
use anyhow::Result;
|
|
2
12
|
use axum::{
|
|
3
13
|
Router,
|
|
@@ -8,63 +18,181 @@ use axum::{
|
|
|
8
18
|
routing::any,
|
|
9
19
|
};
|
|
10
20
|
use serde_json::Value;
|
|
21
|
+
use smallvec::SmallVec;
|
|
11
22
|
use std::time::Instant;
|
|
12
23
|
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
|
|
13
24
|
use tokio::net::TcpListener;
|
|
14
|
-
use smallvec::SmallVec;
|
|
15
|
-
|
|
16
|
-
mod utils;
|
|
17
25
|
|
|
18
26
|
mod action_management;
|
|
19
27
|
mod extensions;
|
|
28
|
+
mod fast_path;
|
|
20
29
|
mod runtime;
|
|
30
|
+
mod utils;
|
|
21
31
|
|
|
22
|
-
use action_management::{
|
|
23
|
-
|
|
24
|
-
};
|
|
32
|
+
use action_management::{DynamicRoute, RouteVal, match_dynamic_route};
|
|
33
|
+
use fast_path::{FastPathRegistry, PrecomputedRoute};
|
|
25
34
|
use runtime::RuntimeManager;
|
|
26
35
|
use utils::{blue, gray, green, red, white, yellow};
|
|
27
36
|
|
|
37
|
+
/// Global allocator: mimalloc for ~5-15% better allocation throughput.
|
|
38
|
+
#[global_allocator]
|
|
39
|
+
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
40
|
+
|
|
28
41
|
#[derive(Clone)]
|
|
29
42
|
struct AppState {
|
|
30
43
|
routes: Arc<HashMap<String, RouteVal>>,
|
|
31
44
|
dynamic_routes: Arc<Vec<DynamicRoute>>,
|
|
32
45
|
runtime: Arc<RuntimeManager>,
|
|
46
|
+
/// Pre-computed responses for static actions (bypass V8)
|
|
47
|
+
fast_paths: Arc<FastPathRegistry>,
|
|
48
|
+
/// Pre-serialized responses for reply routes (no re-serialization per request)
|
|
49
|
+
precomputed: Arc<HashMap<String, PrecomputedRoute>>,
|
|
50
|
+
/// When true: disable per-request logging and timings injection
|
|
51
|
+
production_mode: bool,
|
|
33
52
|
}
|
|
34
53
|
|
|
35
|
-
// Root/dynamic handlers -----------------------------------------------------
|
|
36
|
-
|
|
37
54
|
async fn root_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
|
|
38
|
-
|
|
55
|
+
handler(state, req).await
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
async fn dynamic_route(state: State<AppState>, req: Request<Body>) -> impl IntoResponse {
|
|
42
|
-
|
|
59
|
+
handler(state, req).await
|
|
43
60
|
}
|
|
44
61
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
req: Request<Body>,
|
|
48
|
-
) -> impl IntoResponse {
|
|
49
|
-
// ---------------------------
|
|
50
|
-
// BASIC REQUEST INFO
|
|
51
|
-
// ---------------------------
|
|
62
|
+
/// Main request handler — optimized with early fast-path bailout.
|
|
63
|
+
async fn handler(State(state): State<AppState>, req: Request<Body>) -> impl IntoResponse {
|
|
52
64
|
let method = req.method().as_str().to_uppercase();
|
|
53
65
|
let path = req.uri().path().to_string();
|
|
54
66
|
let strict_key = format!("{}:{}", method, path);
|
|
55
|
-
// Also try simple path for generic routes
|
|
56
|
-
// Check strict first, then simple path
|
|
57
67
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
68
|
+
// Phase 1: Fast-Path Check (before ANY body/header parsing)
|
|
69
|
+
// This is the critical optimization. For static actions and reply routes,
|
|
70
|
+
// we return pre-computed bytes without touching the request body, headers,
|
|
71
|
+
// or V8 runtime. This path costs ~2-5µs vs ~50-100µs for the V8 path.
|
|
72
|
+
|
|
61
73
|
let start = Instant::now();
|
|
62
|
-
let
|
|
63
|
-
|
|
74
|
+
let log_enabled = !state.production_mode;
|
|
75
|
+
|
|
76
|
+
if let Some(route) = state
|
|
77
|
+
.routes
|
|
78
|
+
.get(&strict_key)
|
|
79
|
+
.or_else(|| state.routes.get(&path))
|
|
80
|
+
{
|
|
81
|
+
match route.r#type.as_str() {
|
|
82
|
+
|
|
83
|
+
// Precomputed reply routes
|
|
84
|
+
"json" | "text" => {
|
|
85
|
+
if let Some(precomputed) = state.precomputed.get(&strict_key) {
|
|
86
|
+
|
|
87
|
+
if state.production_mode {
|
|
88
|
+
// Benchmark mode → zero overhead
|
|
89
|
+
return precomputed.to_axum_response();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let mut response = precomputed.to_axum_response();
|
|
93
|
+
let elapsed = start.elapsed();
|
|
94
|
+
|
|
95
|
+
response.headers_mut().insert(
|
|
96
|
+
"Server-Timing",
|
|
97
|
+
format!("reply;dur={:.2}", elapsed.as_secs_f64() * 1000.0)
|
|
98
|
+
.parse()
|
|
99
|
+
.unwrap(),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if log_enabled {
|
|
103
|
+
println!(
|
|
104
|
+
"{} {} {} {}",
|
|
105
|
+
blue("[Titan]"),
|
|
106
|
+
green(&format!("{} {}", method, path)),
|
|
107
|
+
white("→ reply"),
|
|
108
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fallback (should never happen)
|
|
116
|
+
if route.r#type == "json" {
|
|
117
|
+
return Json(route.value.clone()).into_response();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if let Some(s) = route.value.as_str() {
|
|
121
|
+
return s.to_string().into_response();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Action routes (Fast path check)
|
|
126
|
+
"action" => {
|
|
127
|
+
let action_name = route.value.as_str().unwrap_or("");
|
|
128
|
+
|
|
129
|
+
if let Some(static_resp) = state.fast_paths.get(action_name) {
|
|
130
|
+
|
|
131
|
+
if state.production_mode {
|
|
132
|
+
// Benchmark mode → zero overhead
|
|
133
|
+
return static_resp.to_axum_response();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let mut response = static_resp.to_axum_response();
|
|
137
|
+
let elapsed = start.elapsed();
|
|
138
|
+
|
|
139
|
+
response.headers_mut().insert(
|
|
140
|
+
"Server-Timing",
|
|
141
|
+
format!("fastpath;dur={:.2}", elapsed.as_secs_f64() * 1000.0)
|
|
142
|
+
.parse()
|
|
143
|
+
.unwrap(),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if log_enabled {
|
|
147
|
+
println!(
|
|
148
|
+
"{} {} {} {}",
|
|
149
|
+
blue("[Titan]"),
|
|
150
|
+
green(&format!("{} {}", method, path)),
|
|
151
|
+
white("→ fastpath"),
|
|
152
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return response;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Not static → continue to dynamic execution
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// String reply routes
|
|
163
|
+
_ => {
|
|
164
|
+
if let Some(s) = route.value.as_str() {
|
|
165
|
+
|
|
166
|
+
if state.production_mode {
|
|
167
|
+
return s.to_string().into_response();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let elapsed = start.elapsed();
|
|
171
|
+
|
|
172
|
+
if log_enabled {
|
|
173
|
+
println!(
|
|
174
|
+
"{} {} {} {}",
|
|
175
|
+
blue("[Titan]"),
|
|
176
|
+
green(&format!("{} {}", method, path)),
|
|
177
|
+
white("→ reply"),
|
|
178
|
+
gray(&format!("in {:.2?}", elapsed))
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return s.to_string().into_response();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
64
187
|
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
188
|
+
|
|
189
|
+
// Phase 2: Dynamic Route Handling (requires body/header parsing)
|
|
190
|
+
// Only reached for actions that actually need V8 execution.
|
|
191
|
+
|
|
192
|
+
let start = Instant::now(); // restart timing for dynamic path
|
|
193
|
+
let log_enabled = !state.production_mode;
|
|
194
|
+
|
|
195
|
+
// Query parsing
|
|
68
196
|
let query_pairs: Vec<(String, String)> = req
|
|
69
197
|
.uri()
|
|
70
198
|
.query()
|
|
@@ -77,14 +205,10 @@ async fn dynamic_handler_inner(
|
|
|
77
205
|
.collect()
|
|
78
206
|
})
|
|
79
207
|
.unwrap_or_default();
|
|
80
|
-
|
|
81
208
|
let query_map: HashMap<String, String> = query_pairs.into_iter().collect();
|
|
82
209
|
|
|
83
|
-
//
|
|
84
|
-
// HEADERS & BODY
|
|
85
|
-
// ---------------------------
|
|
210
|
+
// Headers & Body
|
|
86
211
|
let (parts, body) = req.into_parts();
|
|
87
|
-
|
|
88
212
|
let headers_map: HashMap<String, String> = parts
|
|
89
213
|
.headers
|
|
90
214
|
.iter()
|
|
@@ -96,14 +220,17 @@ async fn dynamic_handler_inner(
|
|
|
96
220
|
Err(_) => return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response(),
|
|
97
221
|
};
|
|
98
222
|
|
|
99
|
-
//
|
|
100
|
-
// ROUTE RESOLUTION
|
|
101
|
-
// ---------------------------
|
|
223
|
+
// Route resolution
|
|
102
224
|
let mut params: HashMap<String, String> = HashMap::new();
|
|
103
225
|
let mut action_name: Option<String> = None;
|
|
226
|
+
let mut route_kind = "none";
|
|
227
|
+
let mut route_label = String::from("not_found");
|
|
104
228
|
|
|
105
|
-
// Exact route
|
|
106
|
-
let route = state
|
|
229
|
+
// Exact route lookup (may find action routes not caught in fast-path phase)
|
|
230
|
+
let route = state
|
|
231
|
+
.routes
|
|
232
|
+
.get(&strict_key)
|
|
233
|
+
.or_else(|| state.routes.get(&path));
|
|
107
234
|
if let Some(route) = route {
|
|
108
235
|
route_kind = "exact";
|
|
109
236
|
if route.r#type == "action" {
|
|
@@ -111,29 +238,32 @@ async fn dynamic_handler_inner(
|
|
|
111
238
|
route_label = name.clone();
|
|
112
239
|
action_name = Some(name);
|
|
113
240
|
} else if route.r#type == "json" {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
241
|
+
// This path shouldn't be reached (handled in Phase 1), but keep as safety
|
|
242
|
+
if log_enabled {
|
|
243
|
+
println!(
|
|
244
|
+
"{} {} {} {}",
|
|
245
|
+
blue("[Titan]"),
|
|
246
|
+
white(&format!("{} {}", method, path)),
|
|
247
|
+
white("→ json"),
|
|
248
|
+
gray(&format!("in {:.2?}", start.elapsed()))
|
|
249
|
+
);
|
|
250
|
+
}
|
|
122
251
|
return Json(route.value.clone()).into_response();
|
|
123
252
|
} else if let Some(s) = route.value.as_str() {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
253
|
+
if log_enabled {
|
|
254
|
+
println!(
|
|
255
|
+
"{} {} {} {}",
|
|
256
|
+
blue("[Titan]"),
|
|
257
|
+
white(&format!("{} {}", method, path)),
|
|
258
|
+
white("→ reply"),
|
|
259
|
+
gray(&format!("in {:.2?}", start.elapsed()))
|
|
260
|
+
);
|
|
261
|
+
}
|
|
132
262
|
return s.to_string().into_response();
|
|
133
263
|
}
|
|
134
264
|
}
|
|
135
265
|
|
|
136
|
-
// Dynamic route
|
|
266
|
+
// Dynamic route matching
|
|
137
267
|
if action_name.is_none() {
|
|
138
268
|
if let Some((action, p)) =
|
|
139
269
|
match_dynamic_route(&method, &path, state.dynamic_routes.as_slice())
|
|
@@ -148,107 +278,85 @@ async fn dynamic_handler_inner(
|
|
|
148
278
|
let action_name = match action_name {
|
|
149
279
|
Some(a) => a,
|
|
150
280
|
None => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
281
|
+
if log_enabled {
|
|
282
|
+
println!(
|
|
283
|
+
"{} {} {} {}",
|
|
284
|
+
blue("[Titan]"),
|
|
285
|
+
white(&format!("{} {}", method, path)),
|
|
286
|
+
white("→ 404"),
|
|
287
|
+
gray(&format!("in {:.2?}", start.elapsed()))
|
|
288
|
+
);
|
|
289
|
+
}
|
|
159
290
|
return (StatusCode::NOT_FOUND, "Not Found").into_response();
|
|
160
291
|
}
|
|
161
292
|
};
|
|
162
293
|
|
|
294
|
+
// Phase 3: V8 Execution (dispatch to worker pool)
|
|
163
295
|
|
|
164
|
-
// ---------------------------
|
|
165
|
-
// EXECUTE IN V8 (WORKER POOL)
|
|
166
|
-
// ---------------------------
|
|
167
|
-
|
|
168
|
-
// OPTIMIZATION: Zero-Copy & Stack Allocation
|
|
169
|
-
// 1. Headers/Params are collected into `SmallVec` (stack allocated if small).
|
|
170
|
-
// 2. Body is passed as `Bytes` (ref-counted pointer), not copied.
|
|
171
|
-
// 3. No JSON serialization happens here anymore. This saves ~60% CPU vs previous version.
|
|
172
|
-
|
|
173
296
|
let headers_vec: SmallVec<[(String, String); 8]> = headers_map.into_iter().collect();
|
|
174
297
|
let params_vec: SmallVec<[(String, String); 4]> = params.into_iter().collect();
|
|
175
298
|
let query_vec: SmallVec<[(String, String); 4]> = query_map.into_iter().collect();
|
|
176
|
-
|
|
177
|
-
// Pass raw bytes to worker if not empty
|
|
299
|
+
|
|
178
300
|
let body_arg = if !body_bytes.is_empty() {
|
|
179
301
|
Some(body_bytes)
|
|
180
302
|
} else {
|
|
181
303
|
None
|
|
182
304
|
};
|
|
183
305
|
|
|
184
|
-
|
|
185
|
-
// This sends a pointer-sized message through the ring buffer, triggering
|
|
186
|
-
// the V8 thread to wake up and process the request immediately.
|
|
187
|
-
|
|
188
|
-
// Dispatch to the worker pool for V8 execution
|
|
189
|
-
let (mut result_json, timings) = state
|
|
306
|
+
let (result_json, timings) = state
|
|
190
307
|
.runtime
|
|
191
308
|
.execute(
|
|
192
|
-
action_name,
|
|
309
|
+
action_name.clone(),
|
|
193
310
|
method.clone(),
|
|
194
311
|
path.clone(),
|
|
195
312
|
body_arg,
|
|
196
313
|
headers_vec,
|
|
197
314
|
params_vec,
|
|
198
|
-
query_vec
|
|
315
|
+
query_vec,
|
|
199
316
|
)
|
|
200
317
|
.await
|
|
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
|
-
}
|
|
318
|
+
.unwrap_or_else(|e| (serde_json::json!({"error": e}), vec![]));
|
|
215
319
|
|
|
216
|
-
|
|
217
|
-
format!("{} {}", blue("[Titan"), blue("Drift]"))
|
|
218
|
-
} else {
|
|
219
|
-
blue("[Titan]").to_string()
|
|
220
|
-
};
|
|
320
|
+
// Phase 4: Response Construction
|
|
221
321
|
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
322
|
+
// NOTE: We intentionally do NOT inject _titanTimings into the JSON body.
|
|
323
|
+
// This was corrupting benchmark responses (e.g., adding extra fields to
|
|
324
|
+
// {"message":"Hello, World!"} which fails TechEmpower validation).
|
|
325
|
+
// Timing info is available via the Server-Timing HTTP header instead.
|
|
326
|
+
|
|
327
|
+
// Error handling
|
|
225
328
|
if let Some(err) = result_json.get("error") {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
329
|
+
if log_enabled {
|
|
330
|
+
let prefix = if !timings.is_empty() {
|
|
331
|
+
format!("{} {}", blue("[Titan"), blue("Drift]"))
|
|
332
|
+
} else {
|
|
333
|
+
blue("[Titan]").to_string()
|
|
334
|
+
};
|
|
335
|
+
println!(
|
|
336
|
+
"{} {} {} {}",
|
|
337
|
+
prefix,
|
|
338
|
+
red(&format!("{} {}", method, path)),
|
|
339
|
+
red("→ error"),
|
|
340
|
+
gray(&format!("in {:.2?}", start.elapsed()))
|
|
341
|
+
);
|
|
342
|
+
println!(
|
|
343
|
+
"{} {} {}",
|
|
344
|
+
prefix,
|
|
345
|
+
red("Action Error:"),
|
|
346
|
+
red(err.as_str().unwrap_or("Unknown"))
|
|
347
|
+
);
|
|
242
348
|
}
|
|
349
|
+
let response = (StatusCode::INTERNAL_SERVER_ERROR, Json(result_json)).into_response();
|
|
243
350
|
return response;
|
|
244
351
|
}
|
|
245
352
|
|
|
246
|
-
//
|
|
247
|
-
// RESPONSE CONSTRUCTION
|
|
248
|
-
// ---------------------------
|
|
353
|
+
// Response object construction
|
|
249
354
|
let mut response = if let Some(is_resp) = result_json.get("_isResponse") {
|
|
250
355
|
if is_resp.as_bool().unwrap_or(false) {
|
|
251
|
-
let status_u16 = result_json
|
|
356
|
+
let status_u16 = result_json
|
|
357
|
+
.get("status")
|
|
358
|
+
.and_then(|v| v.as_u64())
|
|
359
|
+
.unwrap_or(200) as u16;
|
|
252
360
|
let status = StatusCode::from_u16(status_u16).unwrap_or(StatusCode::OK);
|
|
253
361
|
let mut builder = axum::http::Response::builder().status(status);
|
|
254
362
|
|
|
@@ -264,13 +372,19 @@ async fn dynamic_handler_inner(
|
|
|
264
372
|
if let Some(location) = result_json.get("redirect") {
|
|
265
373
|
if let Some(url) = location.as_str() {
|
|
266
374
|
let mut final_status_u16 = status.as_u16();
|
|
267
|
-
if
|
|
268
|
-
|
|
375
|
+
if !(300..400).contains(&final_status_u16) {
|
|
376
|
+
final_status_u16 = 302;
|
|
377
|
+
}
|
|
378
|
+
builder = builder
|
|
379
|
+
.status(StatusCode::from_u16(final_status_u16).unwrap_or(StatusCode::FOUND))
|
|
380
|
+
.header("Location", url);
|
|
269
381
|
is_redirect = true;
|
|
270
382
|
}
|
|
271
383
|
}
|
|
272
384
|
|
|
273
|
-
let body_text = if is_redirect {
|
|
385
|
+
let body_text = if is_redirect {
|
|
386
|
+
"".to_string()
|
|
387
|
+
} else {
|
|
274
388
|
match result_json.get("body") {
|
|
275
389
|
Some(Value::String(s)) => s.clone(),
|
|
276
390
|
Some(v) => v.to_string(),
|
|
@@ -279,47 +393,82 @@ async fn dynamic_handler_inner(
|
|
|
279
393
|
};
|
|
280
394
|
builder.body(Body::from(body_text)).unwrap()
|
|
281
395
|
} else {
|
|
282
|
-
Json(result_json
|
|
396
|
+
Json(result_json).into_response()
|
|
283
397
|
}
|
|
284
398
|
} else {
|
|
285
|
-
Json(result_json
|
|
399
|
+
Json(result_json).into_response()
|
|
286
400
|
};
|
|
287
401
|
|
|
288
|
-
|
|
289
|
-
|
|
402
|
+
// Server-Timing header (only outside benchmark mode)
|
|
403
|
+
if !state.production_mode && !timings.is_empty() {
|
|
404
|
+
let server_timing = timings
|
|
405
|
+
.iter()
|
|
406
|
+
.enumerate()
|
|
407
|
+
.map(|(i, (name, duration))| format!("{}_{};dur={:.2}", name, i, duration))
|
|
408
|
+
.collect::<Vec<_>>()
|
|
409
|
+
.join(", ");
|
|
410
|
+
response
|
|
411
|
+
.headers_mut()
|
|
412
|
+
.insert("Server-Timing", server_timing.parse().unwrap());
|
|
290
413
|
}
|
|
291
414
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
415
|
+
// Logging
|
|
416
|
+
if log_enabled {
|
|
417
|
+
let total_elapsed = start.elapsed();
|
|
418
|
+
let total_elapsed_ms = total_elapsed.as_secs_f64() * 1000.0;
|
|
419
|
+
let total_drift_ms: f64 = timings
|
|
420
|
+
.iter()
|
|
421
|
+
.filter(|(n, _)| n == "drift" || n == "drift_error")
|
|
422
|
+
.map(|(_, d)| d)
|
|
423
|
+
.sum();
|
|
424
|
+
let compute_ms = (total_elapsed_ms - total_drift_ms).max(0.0);
|
|
425
|
+
|
|
426
|
+
let prefix = if !timings.is_empty() {
|
|
427
|
+
format!("{} {}", blue("[Titan"), blue("Drift]"))
|
|
428
|
+
} else {
|
|
429
|
+
blue("[Titan]").to_string()
|
|
430
|
+
};
|
|
431
|
+
let timing_info = if !timings.is_empty() {
|
|
432
|
+
gray(&format!(
|
|
433
|
+
"(active: {:.2}ms, drift: {:.2}ms) in {:.2?}",
|
|
434
|
+
compute_ms, total_drift_ms, total_elapsed
|
|
435
|
+
))
|
|
436
|
+
} else {
|
|
437
|
+
gray(&format!("in {:.2?}", total_elapsed))
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
match route_kind {
|
|
441
|
+
"dynamic" => println!(
|
|
442
|
+
"{} {} {} {} {} {}",
|
|
443
|
+
prefix,
|
|
444
|
+
green(&format!("{} {}", method, path)),
|
|
445
|
+
white("→"),
|
|
446
|
+
green(&route_label),
|
|
447
|
+
white("(dynamic)"),
|
|
448
|
+
timing_info
|
|
449
|
+
),
|
|
450
|
+
"exact" => println!(
|
|
451
|
+
"{} {} {} {} {}",
|
|
452
|
+
prefix,
|
|
453
|
+
white(&format!("{} {}", method, path)),
|
|
454
|
+
white("→"),
|
|
455
|
+
yellow(&route_label),
|
|
456
|
+
timing_info
|
|
457
|
+
),
|
|
458
|
+
_ => {}
|
|
459
|
+
}
|
|
310
460
|
}
|
|
311
461
|
|
|
312
462
|
response
|
|
313
463
|
}
|
|
314
464
|
|
|
315
|
-
|
|
316
|
-
// Entrypoint ---------------------------------------------------------------
|
|
317
|
-
|
|
318
465
|
#[tokio::main]
|
|
319
466
|
async fn main() -> Result<()> {
|
|
320
467
|
dotenvy::dotenv().ok();
|
|
321
|
-
|
|
322
|
-
//
|
|
468
|
+
|
|
469
|
+
// Configuration
|
|
470
|
+
let production_mode = std::env::var("TITAN_DEV").unwrap_or_default() != "1";
|
|
471
|
+
|
|
323
472
|
let raw = fs::read_to_string("./routes.json").unwrap_or_else(|_| "{}".to_string());
|
|
324
473
|
let json: Value = serde_json::from_str(&raw).unwrap_or_default();
|
|
325
474
|
|
|
@@ -328,36 +477,75 @@ async fn main() -> Result<()> {
|
|
|
328
477
|
.and_then(|p| p.parse::<u64>().ok())
|
|
329
478
|
.or_else(|| json["__config"]["port"].as_u64())
|
|
330
479
|
.unwrap_or(3000);
|
|
480
|
+
|
|
331
481
|
let thread_count = json["__config"]["threads"].as_u64();
|
|
332
482
|
let routes_json = json["routes"].clone();
|
|
333
483
|
let map: HashMap<String, RouteVal> = serde_json::from_value(routes_json).unwrap_or_default();
|
|
334
484
|
let dynamic_routes: Vec<DynamicRoute> =
|
|
335
485
|
serde_json::from_value(json["__dynamic_routes"].clone()).unwrap_or_default();
|
|
336
486
|
|
|
337
|
-
// Identify project root
|
|
338
487
|
let project_root = resolve_project_root();
|
|
339
|
-
|
|
340
|
-
// Load extensions
|
|
488
|
+
|
|
489
|
+
// Load extensions
|
|
341
490
|
extensions::load_project_extensions(project_root.clone());
|
|
342
491
|
|
|
343
|
-
|
|
344
|
-
|
|
492
|
+
// Build pre-computed route responses
|
|
493
|
+
let mut precomputed = HashMap::new();
|
|
494
|
+
for (key, route) in &map {
|
|
495
|
+
match route.r#type.as_str() {
|
|
496
|
+
"json" => {
|
|
497
|
+
precomputed.insert(key.clone(), PrecomputedRoute::from_json(&route.value));
|
|
498
|
+
}
|
|
499
|
+
"text" => {
|
|
500
|
+
if let Some(s) = route.value.as_str() {
|
|
501
|
+
precomputed.insert(key.clone(), PrecomputedRoute::from_text(s));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
_ => {}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if !precomputed.is_empty() {
|
|
508
|
+
println!(
|
|
509
|
+
"{} {} reply route(s) pre-computed",
|
|
510
|
+
blue("[Titan]"),
|
|
511
|
+
precomputed.len()
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Build fast-path registry (scan action files for static patterns)
|
|
516
|
+
let actions_dir = find_actions_dir(&project_root);
|
|
517
|
+
let fast_paths = FastPathRegistry::build(&actions_dir);
|
|
518
|
+
|
|
519
|
+
// Initialize Runtime Manager (V8 Worker Pool)
|
|
345
520
|
let threads = match thread_count {
|
|
346
521
|
Some(t) if t > 0 => t as usize,
|
|
347
|
-
_ =>
|
|
522
|
+
_ => {
|
|
523
|
+
let cpus = num_cpus::get();
|
|
524
|
+
// Optimal for CPU-bound V8 work: 2x cores
|
|
525
|
+
cpus * 2
|
|
526
|
+
}
|
|
348
527
|
};
|
|
349
528
|
|
|
350
529
|
let stack_mb = json["__config"]["stack_mb"].as_u64().unwrap_or(8);
|
|
351
530
|
let stack_size = (stack_mb as usize) * 1024 * 1024;
|
|
352
|
-
|
|
353
|
-
let runtime_manager = Arc::new(RuntimeManager::new(project_root.clone(), threads, stack_size));
|
|
354
531
|
|
|
532
|
+
let runtime_manager = Arc::new(RuntimeManager::new(
|
|
533
|
+
project_root.clone(),
|
|
534
|
+
threads,
|
|
535
|
+
stack_size,
|
|
536
|
+
));
|
|
537
|
+
|
|
538
|
+
// Build AppState
|
|
355
539
|
let state = AppState {
|
|
356
540
|
routes: Arc::new(map),
|
|
357
541
|
dynamic_routes: Arc::new(dynamic_routes),
|
|
358
542
|
runtime: runtime_manager,
|
|
543
|
+
fast_paths: Arc::new(fast_paths),
|
|
544
|
+
precomputed: Arc::new(precomputed),
|
|
545
|
+
production_mode,
|
|
359
546
|
};
|
|
360
547
|
|
|
548
|
+
// Router
|
|
361
549
|
let app = Router::new()
|
|
362
550
|
.route("/", any(root_route))
|
|
363
551
|
.fallback(any(dynamic_route))
|
|
@@ -365,21 +553,19 @@ async fn main() -> Result<()> {
|
|
|
365
553
|
|
|
366
554
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
|
|
367
555
|
|
|
368
|
-
|
|
369
556
|
println!(
|
|
370
|
-
"\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{} \x1b[90m(Threads: {}, Stack: {}MB)\x1b[0m",
|
|
557
|
+
"\x1b[38;5;39mTitan server running at:\x1b[0m http://localhost:{} \x1b[90m(Threads: {}, Stack: {}MB{})\x1b[0m",
|
|
371
558
|
port,
|
|
372
559
|
threads,
|
|
373
|
-
stack_mb
|
|
560
|
+
stack_mb,
|
|
561
|
+
if production_mode { "" } else { ", Dev Mode" }
|
|
374
562
|
);
|
|
375
|
-
|
|
376
563
|
|
|
377
564
|
axum::serve(listener, app).await?;
|
|
378
565
|
Ok(())
|
|
379
566
|
}
|
|
380
567
|
|
|
381
568
|
fn resolve_project_root() -> PathBuf {
|
|
382
|
-
// 1. Check CWD (preferred for local dev/tooling)
|
|
383
569
|
if let Ok(cwd) = std::env::current_dir() {
|
|
384
570
|
if cwd.join("node_modules").exists()
|
|
385
571
|
|| cwd.join("package.json").exists()
|
|
@@ -389,8 +575,6 @@ fn resolve_project_root() -> PathBuf {
|
|
|
389
575
|
}
|
|
390
576
|
}
|
|
391
577
|
|
|
392
|
-
// 2. Check executable persistence (Docker / Production)
|
|
393
|
-
// Walk up from the executable to find .ext or node_modules
|
|
394
578
|
if let Ok(exe) = std::env::current_exe() {
|
|
395
579
|
let mut current = exe.parent();
|
|
396
580
|
while let Some(dir) = current {
|
|
@@ -401,6 +585,23 @@ fn resolve_project_root() -> PathBuf {
|
|
|
401
585
|
}
|
|
402
586
|
}
|
|
403
587
|
|
|
404
|
-
// 3. Fallback to CWD
|
|
405
588
|
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
|
406
589
|
}
|
|
590
|
+
|
|
591
|
+
/// Find the actions directory for fast-path scanning.
|
|
592
|
+
fn find_actions_dir(root: &PathBuf) -> PathBuf {
|
|
593
|
+
let candidates = [
|
|
594
|
+
root.join("server").join("src").join("actions"),
|
|
595
|
+
root.join("server").join("actions"),
|
|
596
|
+
root.join("actions"),
|
|
597
|
+
PathBuf::from("/app").join("actions"),
|
|
598
|
+
];
|
|
599
|
+
|
|
600
|
+
for p in &candidates {
|
|
601
|
+
if p.exists() && p.is_dir() {
|
|
602
|
+
return p.clone();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
root.join("server").join("src").join("actions")
|
|
607
|
+
}
|