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
|
@@ -16,15 +16,19 @@ pub struct Registry {
|
|
|
16
16
|
pub _libs: Vec<Library>,
|
|
17
17
|
pub modules: Vec<ModuleDef>,
|
|
18
18
|
pub natives: Vec<NativeFnEntry>,
|
|
19
|
+
pub v8_natives: Vec<usize>,
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
|
|
21
23
|
#[derive(Clone)]
|
|
22
24
|
pub struct ModuleDef {
|
|
23
25
|
pub name: String,
|
|
24
26
|
pub js: String,
|
|
25
27
|
pub native_indices: HashMap<String, usize>,
|
|
28
|
+
pub v8_native_indices: HashMap<String, usize>,
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
|
|
28
32
|
#[derive(Clone, Debug, PartialEq)]
|
|
29
33
|
pub enum ParamType {
|
|
30
34
|
String, F64, Bool, Json, Buffer,
|
|
@@ -56,9 +60,18 @@ struct TitanConfig {
|
|
|
56
60
|
#[derive(serde::Deserialize)]
|
|
57
61
|
struct TitanNativeConfig {
|
|
58
62
|
path: String,
|
|
63
|
+
#[serde(default)]
|
|
59
64
|
functions: HashMap<String, TitanNativeFunc>,
|
|
65
|
+
#[serde(default)]
|
|
66
|
+
v8_functions: HashMap<String, TitanV8Func>,
|
|
60
67
|
}
|
|
61
68
|
|
|
69
|
+
#[derive(serde::Deserialize)]
|
|
70
|
+
struct TitanV8Func {
|
|
71
|
+
symbol: String,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
62
75
|
#[derive(serde::Deserialize)]
|
|
63
76
|
struct TitanNativeFunc {
|
|
64
77
|
symbol: String,
|
|
@@ -95,6 +108,8 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
95
108
|
let mut modules = Vec::new();
|
|
96
109
|
let mut libs = Vec::new();
|
|
97
110
|
let mut all_natives = Vec::new();
|
|
111
|
+
let mut all_v8_natives = Vec::new();
|
|
112
|
+
|
|
98
113
|
|
|
99
114
|
let mut node_modules = root.join("node_modules");
|
|
100
115
|
if !node_modules.exists() {
|
|
@@ -104,8 +119,11 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
104
119
|
}
|
|
105
120
|
}
|
|
106
121
|
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
// Generic scanner helper
|
|
123
|
+
let scan_dir = |path: PathBuf, modules: &mut Vec<ModuleDef>, libs: &mut Vec<Library>, all_natives: &mut Vec<NativeFnEntry>, all_v8_natives: &mut Vec<usize>| {
|
|
124
|
+
|
|
125
|
+
if !path.exists() { return; }
|
|
126
|
+
for entry in WalkDir::new(&path).follow_links(true).min_depth(1).max_depth(4) {
|
|
109
127
|
let entry = match entry { Ok(e) => e, Err(_) => continue };
|
|
110
128
|
if entry.file_type().is_file() && entry.file_name() == "titan.json" {
|
|
111
129
|
let dir = entry.path().parent().unwrap();
|
|
@@ -115,30 +133,74 @@ pub fn load_project_extensions(root: PathBuf) {
|
|
|
115
133
|
Err(_) => continue,
|
|
116
134
|
};
|
|
117
135
|
let mut mod_natives_map = HashMap::new();
|
|
136
|
+
let mut mod_v8_natives_map = HashMap::new();
|
|
118
137
|
if let Some(native_conf) = config.native {
|
|
138
|
+
|
|
119
139
|
let lib_path = dir.join(&native_conf.path);
|
|
120
140
|
unsafe {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
// Try loading library
|
|
142
|
+
let lib_load = Library::new(&lib_path);
|
|
143
|
+
// If failed, try resolving relative to current dir or LD_LIBRARY_PATH implicit
|
|
144
|
+
// But usually absolute path from `dir` works.
|
|
145
|
+
match lib_load {
|
|
146
|
+
Ok(lib) => {
|
|
147
|
+
for (fn_name, fn_conf) in &native_conf.functions {
|
|
148
|
+
let params = fn_conf.parameters.iter().map(|p| parse_type(&p.to_lowercase())).collect();
|
|
149
|
+
let ret = parse_return(&fn_conf.result.to_lowercase());
|
|
150
|
+
if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes()) {
|
|
151
|
+
let idx = all_natives.len();
|
|
152
|
+
all_natives.push(NativeFnEntry { symbol_ptr: *symbol as usize, sig: Signature { params, ret } });
|
|
153
|
+
mod_natives_map.insert(fn_name.clone(), idx);
|
|
154
|
+
} else {
|
|
155
|
+
println!("{} {} {} -> {}", blue("[Titan]"), red("Symbol not found:"), fn_conf.symbol, config.name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (fn_name, fn_conf) in &native_conf.v8_functions {
|
|
160
|
+
if let Ok(symbol) = lib.get::<*const ()>(fn_conf.symbol.as_bytes()) {
|
|
161
|
+
let idx = all_v8_natives.len();
|
|
162
|
+
all_v8_natives.push(*symbol as usize);
|
|
163
|
+
mod_v8_natives_map.insert(fn_name.clone(), idx);
|
|
164
|
+
} else {
|
|
165
|
+
println!("{} {} {} -> {}", blue("[Titan]"), red("V8 Symbol not found:"), fn_conf.symbol, config.name);
|
|
166
|
+
}
|
|
129
167
|
}
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
|
|
169
|
+
libs.push(lib);
|
|
170
|
+
},
|
|
171
|
+
Err(e) => {
|
|
172
|
+
println!("{} {} {} -> {:?}", blue("[Titan]"), red("Failed to load native lib:"), config.name, e);
|
|
173
|
+
}
|
|
132
174
|
}
|
|
133
175
|
}
|
|
134
176
|
}
|
|
135
177
|
let js_path = dir.join(&config.main);
|
|
136
|
-
modules.push(ModuleDef {
|
|
178
|
+
modules.push(ModuleDef {
|
|
179
|
+
name: config.name.clone(),
|
|
180
|
+
js: fs::read_to_string(js_path).unwrap_or_default(),
|
|
181
|
+
native_indices: mod_natives_map,
|
|
182
|
+
v8_native_indices: mod_v8_natives_map
|
|
183
|
+
});
|
|
184
|
+
|
|
137
185
|
println!("{} {} {}", blue("[Titan]"), green("Extension loaded:"), config.name);
|
|
138
186
|
}
|
|
139
187
|
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Scan node_modules
|
|
191
|
+
if node_modules.exists() {
|
|
192
|
+
scan_dir(node_modules, &mut modules, &mut libs, &mut all_natives, &mut all_v8_natives);
|
|
140
193
|
}
|
|
141
|
-
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// Scan .ext (Production / Docker)
|
|
197
|
+
let ext_dir = root.join(".ext");
|
|
198
|
+
if ext_dir.exists() {
|
|
199
|
+
scan_dir(ext_dir, &mut modules, &mut libs, &mut all_natives, &mut all_v8_natives);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
*REGISTRY.lock().unwrap() = Some(Registry { _libs: libs, modules, natives: all_natives, v8_natives: all_v8_natives });
|
|
203
|
+
|
|
142
204
|
}
|
|
143
205
|
|
|
144
206
|
pub fn inject_external_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>, t_obj: v8::Local<v8::Object>) {
|
|
@@ -146,9 +208,10 @@ pub fn inject_external_extensions(scope: &mut v8::HandleScope, global: v8::Local
|
|
|
146
208
|
let invoke_key = v8_str(scope, "__titan_invoke_native");
|
|
147
209
|
global.set(scope, invoke_key.into(), invoke_fn.into());
|
|
148
210
|
|
|
149
|
-
let modules = if let Ok(guard) = REGISTRY.lock() {
|
|
150
|
-
guard.as_ref().map(|r| r.modules.clone()).unwrap_or_default()
|
|
151
|
-
} else { vec![] };
|
|
211
|
+
let (modules, v8_native_ptrs) = if let Ok(guard) = REGISTRY.lock() {
|
|
212
|
+
(guard.as_ref().map(|r| r.modules.clone()).unwrap_or_default(), guard.as_ref().map(|r| r.v8_natives.clone()).unwrap_or_default())
|
|
213
|
+
} else { (vec![], vec![]) };
|
|
214
|
+
|
|
152
215
|
|
|
153
216
|
for module in modules {
|
|
154
217
|
let mod_obj = v8::Object::new(scope);
|
|
@@ -162,6 +225,24 @@ pub fn inject_external_extensions(scope: &mut v8::HandleScope, global: v8::Local
|
|
|
162
225
|
}
|
|
163
226
|
}
|
|
164
227
|
}
|
|
228
|
+
|
|
229
|
+
for (fn_name, &idx) in &module.v8_native_indices {
|
|
230
|
+
if let Some(&ptr) = v8_native_ptrs.get(idx) {
|
|
231
|
+
if ptr != 0 {
|
|
232
|
+
unsafe {
|
|
233
|
+
let ext = v8::External::new(scope, ptr as *mut std::ffi::c_void);
|
|
234
|
+
let templ = v8::FunctionTemplate::builder(native_invoke_v8_proxy)
|
|
235
|
+
.data(ext.into())
|
|
236
|
+
.build(scope);
|
|
237
|
+
|
|
238
|
+
if let Some(func) = templ.get_function(scope) {
|
|
239
|
+
let key = v8_str(scope, fn_name);
|
|
240
|
+
mod_obj.set(scope, key.into(), func.into());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
165
246
|
let mod_key = v8_str(scope, &module.name);
|
|
166
247
|
t_obj.set(scope, mod_key.into(), mod_obj.into());
|
|
167
248
|
|
|
@@ -249,6 +330,20 @@ fn native_invoke_extension(scope: &mut v8::HandleScope, args: v8::FunctionCallba
|
|
|
249
330
|
}
|
|
250
331
|
}
|
|
251
332
|
|
|
333
|
+
fn native_invoke_v8_proxy(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, retval: v8::ReturnValue) {
|
|
334
|
+
let val = args.data();
|
|
335
|
+
if let Ok(ext) = v8::Local::<v8::External>::try_from(val) {
|
|
336
|
+
let ptr = ext.value() as *mut std::ffi::c_void;
|
|
337
|
+
if !ptr.is_null() {
|
|
338
|
+
unsafe {
|
|
339
|
+
type TitanV8Handler = extern "C" fn(&mut v8::HandleScope, v8::FunctionCallbackArguments, v8::ReturnValue);
|
|
340
|
+
let handler: TitanV8Handler = std::mem::transmute(ptr);
|
|
341
|
+
handler(scope, args, retval);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
252
347
|
fn arg_from_v8(scope: &mut v8::HandleScope, val: v8::Local<v8::Value>, ty: &ParamType) -> serde_json::Value {
|
|
253
348
|
match ty {
|
|
254
349
|
ParamType::String => serde_json::Value::String(val.to_rust_string_lossy(scope)),
|
|
@@ -21,6 +21,7 @@ use v8;
|
|
|
21
21
|
// ----------------------------------------------------------------------------
|
|
22
22
|
|
|
23
23
|
pub static SHARE_CONTEXT: OnceLock<ShareContextStore> = OnceLock::new();
|
|
24
|
+
pub static PROJECT_ROOT: OnceLock<PathBuf> = OnceLock::new();
|
|
24
25
|
|
|
25
26
|
pub struct ShareContextStore {
|
|
26
27
|
pub kv: DashMap<String, serde_json::Value>,
|
|
@@ -40,22 +41,91 @@ impl ShareContextStore {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// Re-exports for easier access
|
|
43
|
-
pub
|
|
44
|
+
pub fn load_project_extensions(root: PathBuf) {
|
|
45
|
+
PROJECT_ROOT.get_or_init(|| root.clone());
|
|
46
|
+
external::load_project_extensions(root);
|
|
47
|
+
}
|
|
44
48
|
|
|
45
49
|
// ----------------------------------------------------------------------------
|
|
46
50
|
// TITAN RUNTIME
|
|
47
51
|
// ----------------------------------------------------------------------------
|
|
48
52
|
|
|
53
|
+
pub enum TitanAsyncOp {
|
|
54
|
+
Fetch {
|
|
55
|
+
url: String,
|
|
56
|
+
method: String,
|
|
57
|
+
body: Option<String>,
|
|
58
|
+
headers: Vec<(String, String)>,
|
|
59
|
+
},
|
|
60
|
+
DbQuery {
|
|
61
|
+
conn: String,
|
|
62
|
+
query: String,
|
|
63
|
+
},
|
|
64
|
+
FsRead {
|
|
65
|
+
path: String,
|
|
66
|
+
},
|
|
67
|
+
Batch(Vec<TitanAsyncOp>),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pub struct WorkerAsyncResult {
|
|
71
|
+
pub drift_id: u32,
|
|
72
|
+
pub result: serde_json::Value,
|
|
73
|
+
pub duration_ms: f64,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub struct AsyncOpRequest {
|
|
77
|
+
pub op: TitanAsyncOp,
|
|
78
|
+
pub drift_id: u32,
|
|
79
|
+
pub request_id: u32,
|
|
80
|
+
pub op_type: String,
|
|
81
|
+
pub respond_tx: tokio::sync::oneshot::Sender<WorkerAsyncResult>,
|
|
82
|
+
}
|
|
83
|
+
|
|
49
84
|
pub struct TitanRuntime {
|
|
85
|
+
pub id: usize,
|
|
50
86
|
pub isolate: v8::OwnedIsolate,
|
|
51
87
|
pub context: v8::Global<v8::Context>,
|
|
52
88
|
pub actions: HashMap<String, v8::Global<v8::Function>>,
|
|
53
89
|
pub worker_tx: crossbeam::channel::Sender<crate::runtime::WorkerCommand>,
|
|
90
|
+
|
|
91
|
+
// Async State
|
|
92
|
+
pub async_rx: crossbeam::channel::Receiver<WorkerAsyncResult>,
|
|
93
|
+
pub async_tx: crossbeam::channel::Sender<WorkerAsyncResult>,
|
|
94
|
+
pub pending_drifts: HashMap<u32, v8::Global<v8::PromiseResolver>>,
|
|
95
|
+
pub pending_requests: HashMap<u32, tokio::sync::oneshot::Sender<crate::runtime::WorkerResult>>,
|
|
96
|
+
pub drift_counter: u32,
|
|
97
|
+
pub request_counter: u32,
|
|
98
|
+
|
|
99
|
+
pub tokio_handle: tokio::runtime::Handle,
|
|
100
|
+
pub global_async_tx: tokio::sync::mpsc::Sender<AsyncOpRequest>,
|
|
101
|
+
pub request_timings: HashMap<u32, Vec<(String, f64)>>,
|
|
102
|
+
pub drift_to_request: HashMap<u32, u32>,
|
|
103
|
+
pub completed_drifts: HashMap<u32, serde_json::Value>,
|
|
104
|
+
pub active_requests: HashMap<u32, RequestData>,
|
|
105
|
+
pub request_start_counters: HashMap<u32, u32>,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[derive(Clone)]
|
|
109
|
+
pub struct RequestData {
|
|
110
|
+
pub action_name: String,
|
|
111
|
+
pub body: Option<Bytes>,
|
|
112
|
+
pub method: String,
|
|
113
|
+
pub path: String,
|
|
114
|
+
pub headers: Vec<(String, String)>,
|
|
115
|
+
pub params: Vec<(String, String)>,
|
|
116
|
+
pub query: Vec<(String, String)>,
|
|
54
117
|
}
|
|
55
118
|
|
|
56
119
|
unsafe impl Send for TitanRuntime {}
|
|
57
120
|
unsafe impl Sync for TitanRuntime {}
|
|
58
121
|
|
|
122
|
+
impl TitanRuntime {
|
|
123
|
+
pub fn bind_to_isolate(&mut self) {
|
|
124
|
+
let ptr = self as *mut TitanRuntime as *mut std::ffi::c_void;
|
|
125
|
+
self.isolate.set_data(0, ptr);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
59
129
|
static V8_INIT: Once = Once::new();
|
|
60
130
|
|
|
61
131
|
pub fn init_v8() {
|
|
@@ -67,19 +137,19 @@ pub fn init_v8() {
|
|
|
67
137
|
}
|
|
68
138
|
|
|
69
139
|
pub fn init_runtime_worker(
|
|
140
|
+
id: usize,
|
|
70
141
|
root: PathBuf,
|
|
71
142
|
worker_tx: crossbeam::channel::Sender<crate::runtime::WorkerCommand>,
|
|
143
|
+
tokio_handle: tokio::runtime::Handle,
|
|
144
|
+
global_async_tx: tokio::sync::mpsc::Sender<AsyncOpRequest>,
|
|
145
|
+
stack_size: usize,
|
|
72
146
|
) -> TitanRuntime {
|
|
73
147
|
init_v8();
|
|
74
148
|
|
|
75
|
-
// Memory optimization strategy
|
|
76
|
-
// - V8 snapshots reduce memory footprint by sharing compiled code
|
|
77
|
-
// - Each isolate still has its own heap, but the snapshot reduces base overhead
|
|
78
|
-
// - For explicit heap limits, use V8 flags: --max-old-space-size=128
|
|
79
|
-
|
|
149
|
+
// Memory optimization strategy
|
|
80
150
|
let params = v8::CreateParams::default();
|
|
81
151
|
let mut isolate = v8::Isolate::new(params);
|
|
82
|
-
|
|
152
|
+
|
|
83
153
|
let (global_context, actions_map) = {
|
|
84
154
|
let handle_scope = &mut v8::HandleScope::new(&mut isolate);
|
|
85
155
|
let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
|
|
@@ -99,6 +169,7 @@ pub fn init_runtime_worker(
|
|
|
99
169
|
let action_files = scan_actions(&root);
|
|
100
170
|
for (name, path) in action_files {
|
|
101
171
|
if let Ok(code) = fs::read_to_string(&path) {
|
|
172
|
+
// Wrap action in an IIFE to capture its exports and register it globally
|
|
102
173
|
let wrapped_source =
|
|
103
174
|
format!("(function() {{ {} }})(); globalThis[\"{}\"];", code, name);
|
|
104
175
|
let source_str = v8_str(scope, &wrapped_source);
|
|
@@ -108,19 +179,49 @@ pub fn init_runtime_worker(
|
|
|
108
179
|
if val.is_function() {
|
|
109
180
|
let func = v8::Local::<v8::Function>::try_from(val).unwrap();
|
|
110
181
|
map.insert(name.clone(), v8::Global::new(try_catch, func));
|
|
182
|
+
} else if id == 0 {
|
|
183
|
+
println!("[V8] Action '{}' did not evaluate to a function: {:?}", name, val.to_rust_string_lossy(try_catch));
|
|
111
184
|
}
|
|
185
|
+
} else if id == 0 {
|
|
186
|
+
let msg = try_catch
|
|
187
|
+
.message()
|
|
188
|
+
.map(|m| m.get(try_catch).to_rust_string_lossy(try_catch))
|
|
189
|
+
.unwrap_or("Unknown run error".to_string());
|
|
190
|
+
println!("[V8] Failed to run action '{}': {}", name, msg);
|
|
112
191
|
}
|
|
192
|
+
} else if id == 0 {
|
|
193
|
+
let msg = try_catch
|
|
194
|
+
.message()
|
|
195
|
+
.map(|m| m.get(try_catch).to_rust_string_lossy(try_catch))
|
|
196
|
+
.unwrap_or("Unknown compile error".to_string());
|
|
197
|
+
println!("[V8] Failed to compile action '{}': {}", name, msg);
|
|
113
198
|
}
|
|
114
199
|
}
|
|
115
200
|
}
|
|
116
201
|
(v8::Global::new(scope, context), map)
|
|
117
202
|
};
|
|
118
203
|
|
|
204
|
+
let (async_tx, async_rx) = crossbeam::channel::unbounded();
|
|
205
|
+
|
|
119
206
|
TitanRuntime {
|
|
207
|
+
id,
|
|
120
208
|
isolate,
|
|
121
209
|
context: global_context,
|
|
122
210
|
actions: actions_map,
|
|
123
211
|
worker_tx,
|
|
212
|
+
async_rx,
|
|
213
|
+
async_tx,
|
|
214
|
+
pending_drifts: HashMap::new(),
|
|
215
|
+
pending_requests: HashMap::new(),
|
|
216
|
+
drift_counter: 0,
|
|
217
|
+
request_counter: 0,
|
|
218
|
+
tokio_handle,
|
|
219
|
+
global_async_tx,
|
|
220
|
+
request_timings: HashMap::new(),
|
|
221
|
+
drift_to_request: HashMap::new(),
|
|
222
|
+
completed_drifts: HashMap::new(),
|
|
223
|
+
active_requests: HashMap::new(),
|
|
224
|
+
request_start_counters: HashMap::new(),
|
|
124
225
|
}
|
|
125
226
|
}
|
|
126
227
|
|
|
@@ -142,7 +243,7 @@ pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Obje
|
|
|
142
243
|
global.set(scope, t_key.into(), t_obj.into());
|
|
143
244
|
}
|
|
144
245
|
|
|
145
|
-
fn v8_to_json<'s>(
|
|
246
|
+
pub fn v8_to_json<'s>(
|
|
146
247
|
scope: &mut v8::HandleScope<'s>,
|
|
147
248
|
value: v8::Local<v8::Value>,
|
|
148
249
|
) -> serde_json::Value {
|
|
@@ -221,6 +322,7 @@ fn v8_to_json<'s>(
|
|
|
221
322
|
|
|
222
323
|
pub fn execute_action_optimized(
|
|
223
324
|
runtime: &mut TitanRuntime,
|
|
325
|
+
request_id: u32,
|
|
224
326
|
action_name: &str,
|
|
225
327
|
req_body: Option<bytes::Bytes>,
|
|
226
328
|
req_method: &str,
|
|
@@ -228,19 +330,22 @@ pub fn execute_action_optimized(
|
|
|
228
330
|
headers: &[(String, String)],
|
|
229
331
|
params: &[(String, String)],
|
|
230
332
|
query: &[(String, String)],
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
} = runtime;
|
|
333
|
+
) {
|
|
334
|
+
// Execute action in V8
|
|
335
|
+
let context_global = runtime.context.clone();
|
|
336
|
+
let actions_map = runtime.actions.clone(); // Clone the map of globals (cheap)
|
|
337
|
+
let isolate = &mut runtime.isolate;
|
|
338
|
+
|
|
238
339
|
let handle_scope = &mut v8::HandleScope::new(isolate);
|
|
239
|
-
let context = v8::Local::new(handle_scope,
|
|
340
|
+
let context = v8::Local::new(handle_scope, context_global);
|
|
240
341
|
let scope = &mut v8::ContextScope::new(handle_scope, context);
|
|
241
342
|
|
|
242
343
|
let req_obj = v8::Object::new(scope);
|
|
243
344
|
|
|
345
|
+
let req_id_key = v8_str(scope, "__titan_request_id");
|
|
346
|
+
let req_id_val = v8::Integer::new(scope, request_id as i32);
|
|
347
|
+
req_obj.set(scope, req_id_key.into(), req_id_val.into());
|
|
348
|
+
|
|
244
349
|
let m_key = v8_str(scope, "method");
|
|
245
350
|
let m_val = v8_str(scope, req_method);
|
|
246
351
|
req_obj.set(scope, m_key.into(), m_val.into());
|
|
@@ -298,17 +403,34 @@ pub fn execute_action_optimized(
|
|
|
298
403
|
global.set(scope, tr_act_key.into(), tr_act_val.into());
|
|
299
404
|
let try_catch = &mut v8::TryCatch::new(scope);
|
|
300
405
|
|
|
301
|
-
if let Some(
|
|
302
|
-
return
|
|
406
|
+
if let Some(_) = action_fn.call(try_catch, global.into(), &[req_obj.into()]) {
|
|
407
|
+
return;
|
|
303
408
|
}
|
|
304
|
-
|
|
409
|
+
|
|
305
410
|
let msg = try_catch
|
|
306
411
|
.message()
|
|
307
412
|
.map(|m| m.get(try_catch).to_rust_string_lossy(try_catch))
|
|
308
413
|
.unwrap_or("Unknown error".to_string());
|
|
309
|
-
|
|
414
|
+
|
|
415
|
+
if msg.contains("SUSPEND") {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
println!("[Isolate {}] Action Error: {}", runtime.id, msg);
|
|
420
|
+
if let Some(tx) = runtime.pending_requests.remove(&request_id) {
|
|
421
|
+
let _ = tx.send(crate::runtime::WorkerResult {
|
|
422
|
+
json: serde_json::json!({"error": msg}),
|
|
423
|
+
timings: vec![]
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
if let Some(tx) = runtime.pending_requests.remove(&request_id) {
|
|
428
|
+
let _ = tx.send(crate::runtime::WorkerResult {
|
|
429
|
+
json: serde_json::json!({"error": format!("Action '{}' not found", action_name)}),
|
|
430
|
+
timings: vec![]
|
|
431
|
+
});
|
|
432
|
+
}
|
|
310
433
|
}
|
|
311
|
-
serde_json::json!({"error": format!("Action '{}' not found", action_name)})
|
|
312
434
|
}
|
|
313
435
|
|
|
314
436
|
pub fn v8_str<'s>(scope: &mut v8::HandleScope<'s>, s: &str) -> v8::Local<'s, v8::String> {
|
|
@@ -1,22 +1,186 @@
|
|
|
1
|
-
|
|
2
1
|
// Titan Core Runtime JS
|
|
3
|
-
//
|
|
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
|
+
};
|
|
4
24
|
|
|
5
|
-
|
|
25
|
+
try {
|
|
26
|
+
const result = fn(req);
|
|
6
27
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
};
|
|
9
46
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
47
|
+
wrapped.__titanWrapped = true;
|
|
48
|
+
return wrapped;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// -----------------------------
|
|
53
|
+
// TextDecoder Polyfill
|
|
54
|
+
// -----------------------------
|
|
55
|
+
globalThis.TextDecoder = class TextDecoder {
|
|
56
|
+
decode(buffer) {
|
|
57
|
+
return t.decodeUtf8(buffer);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// -----------------------------
|
|
62
|
+
// process.env
|
|
63
|
+
// -----------------------------
|
|
64
|
+
globalThis.process = {
|
|
65
|
+
env: t.loadEnv ? t.loadEnv() : {}
|
|
66
|
+
};
|
|
67
|
+
|
|
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
|
+
);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
14
89
|
}
|
|
15
|
-
};
|
|
16
90
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
};
|
|
128
|
+
|
|
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
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else if (!value || !value.__titanAsync) {
|
|
142
|
+
throw new Error("drift() must wrap async ops.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return t._drift_call(value);
|
|
146
|
+
};
|
|
147
|
+
|
|
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));
|
|
157
|
+
};
|
|
158
|
+
t.fetch.__titanWrapped = true;
|
|
159
|
+
}
|
|
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
|
+
}
|
|
179
|
+
|
|
180
|
+
return conn;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
t.db.__titanWrapped = true;
|
|
184
|
+
}
|
|
21
185
|
|
|
22
|
-
|
|
186
|
+
}
|