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
|
@@ -9,16 +9,28 @@ use serde_json::Value;
|
|
|
9
9
|
use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
|
|
10
10
|
use bcrypt::{hash, verify, DEFAULT_COST};
|
|
11
11
|
use postgres::{Client as PgClient, NoTls};
|
|
12
|
-
use std::sync::Mutex;
|
|
13
|
-
use std::collections::HashMap;
|
|
12
|
+
use std::sync::{Mutex, OnceLock};
|
|
13
|
+
use std::collections::{HashMap, BTreeMap};
|
|
14
14
|
|
|
15
|
-
use crate::utils::{blue, gray, parse_expires_in};
|
|
15
|
+
use crate::utils::{blue, gray, red, parse_expires_in};
|
|
16
16
|
use super::{TitanRuntime, v8_str, v8_to_string, throw, ShareContextStore};
|
|
17
17
|
|
|
18
18
|
const TITAN_CORE_JS: &str = include_str!("titan_core.js");
|
|
19
19
|
|
|
20
20
|
// Database connection pool
|
|
21
21
|
static DB_POOL: Mutex<Option<HashMap<String, PgClient>>> = Mutex::new(None);
|
|
22
|
+
static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
|
23
|
+
|
|
24
|
+
fn get_http_client() -> &'static reqwest::Client {
|
|
25
|
+
HTTP_CLIENT.get_or_init(|| {
|
|
26
|
+
reqwest::Client::builder()
|
|
27
|
+
.use_rustls_tls()
|
|
28
|
+
.tcp_nodelay(true)
|
|
29
|
+
.user_agent("TitanPL/1.0")
|
|
30
|
+
.build()
|
|
31
|
+
.unwrap_or_else(|_| reqwest::Client::new())
|
|
32
|
+
})
|
|
33
|
+
}
|
|
22
34
|
|
|
23
35
|
|
|
24
36
|
pub fn inject_builtin_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>, t_obj: v8::Local<v8::Object>) {
|
|
@@ -45,23 +57,39 @@ pub fn inject_builtin_extensions(scope: &mut v8::HandleScope, global: v8::Local<
|
|
|
45
57
|
let log_key = v8_str(scope, "log");
|
|
46
58
|
t_obj.set(scope, log_key.into(), log_fn.into());
|
|
47
59
|
|
|
48
|
-
// t.fetch
|
|
49
|
-
let fetch_fn = v8::Function::new(scope,
|
|
60
|
+
// t.fetch (Metadata version for drift)
|
|
61
|
+
let fetch_fn = v8::Function::new(scope, native_fetch_meta).unwrap();
|
|
50
62
|
let fetch_key = v8_str(scope, "fetch");
|
|
51
63
|
t_obj.set(scope, fetch_key.into(), fetch_fn.into());
|
|
52
64
|
|
|
65
|
+
// t._drift_call
|
|
66
|
+
let drift_fn = v8::Function::new(scope, native_drift_call).unwrap();
|
|
67
|
+
let drift_key = v8_str(scope, "_drift_call");
|
|
68
|
+
t_obj.set(scope, drift_key.into(), drift_fn.into());
|
|
69
|
+
|
|
70
|
+
// t._finish_request
|
|
71
|
+
let finish_fn = v8::Function::new(scope, native_finish_request).unwrap();
|
|
72
|
+
let finish_key = v8_str(scope, "_finish_request");
|
|
73
|
+
t_obj.set(scope, finish_key.into(), finish_fn.into());
|
|
74
|
+
|
|
53
75
|
// t.loadEnv
|
|
54
76
|
let env_fn = v8::Function::new(scope, native_load_env).unwrap();
|
|
55
77
|
let env_key = v8_str(scope, "loadEnv");
|
|
56
78
|
t_obj.set(scope, env_key.into(), env_fn.into());
|
|
57
79
|
|
|
58
|
-
// auth, jwt, password ... (
|
|
80
|
+
// auth, jwt, password, db, core ... (setup native objects BEFORE JS injection)
|
|
59
81
|
setup_native_utils(scope, t_obj);
|
|
60
82
|
|
|
61
83
|
// 2. JS Side Injection (Embedded)
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
let tc = &mut v8::TryCatch::new(scope);
|
|
85
|
+
let source = v8_str(tc, TITAN_CORE_JS);
|
|
86
|
+
if let Some(script) = v8::Script::compile(tc, source, None) {
|
|
87
|
+
if script.run(tc).is_none() {
|
|
88
|
+
let msg = tc.message().map(|m| m.get(tc).to_rust_string_lossy(tc)).unwrap_or("Unknown".to_string());
|
|
89
|
+
println!("{} {} {}", blue("[Titan]"), red("Core JS Init Failed:"), msg);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
println!("{} {}", blue("[Titan]"), red("Core JS Compilation Failed"));
|
|
65
93
|
}
|
|
66
94
|
}
|
|
67
95
|
|
|
@@ -116,7 +144,6 @@ fn setup_native_utils(scope: &mut v8::HandleScope, t_obj: v8::Local<v8::Object>)
|
|
|
116
144
|
t_obj.set(scope, sc_key.into(), sc_val);
|
|
117
145
|
|
|
118
146
|
// t.db (Database operations)
|
|
119
|
-
// println!("[DEBUG] Setting up t.db...");
|
|
120
147
|
let db_obj = v8::Object::new(scope);
|
|
121
148
|
let db_connect_fn = v8::Function::new(scope, native_db_connect).unwrap();
|
|
122
149
|
let connect_key = v8_str(scope, "connect");
|
|
@@ -124,59 +151,100 @@ fn setup_native_utils(scope: &mut v8::HandleScope, t_obj: v8::Local<v8::Object>)
|
|
|
124
151
|
|
|
125
152
|
let db_key = v8_str(scope, "db");
|
|
126
153
|
t_obj.set(scope, db_key.into(), db_obj.into());
|
|
127
|
-
|
|
154
|
+
|
|
155
|
+
// t.core (System operations)
|
|
156
|
+
let core_obj = v8::Object::new(scope);
|
|
157
|
+
let fs_obj = v8::Object::new(scope);
|
|
158
|
+
let fs_read_fn = v8::Function::new(scope, native_read).unwrap();
|
|
159
|
+
let read_key = v8_str(scope, "read");
|
|
160
|
+
fs_obj.set(scope, read_key.into(), fs_read_fn.into());
|
|
161
|
+
|
|
162
|
+
let fs_read_sync_fn = v8::Function::new(scope, native_read_sync).unwrap();
|
|
163
|
+
let read_sync_key = v8_str(scope, "readFile");
|
|
164
|
+
fs_obj.set(scope, read_sync_key.into(), fs_read_sync_fn.into());
|
|
165
|
+
|
|
166
|
+
// Also Expose as t.readSync
|
|
167
|
+
let t_read_sync_fn = v8::Function::new(scope, native_read_sync).unwrap();
|
|
168
|
+
let t_read_sync_key = v8_str(scope, "readSync");
|
|
169
|
+
t_obj.set(scope, t_read_sync_key.into(), t_read_sync_fn.into());
|
|
170
|
+
|
|
171
|
+
let fs_key = v8_str(scope, "fs");
|
|
172
|
+
core_obj.set(scope, fs_key.into(), fs_obj.into());
|
|
173
|
+
|
|
174
|
+
|
|
128
175
|
}
|
|
129
176
|
|
|
130
|
-
fn
|
|
177
|
+
fn native_read_sync(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
131
178
|
let path_val = args.get(0);
|
|
132
179
|
if !path_val.is_string() {
|
|
133
|
-
throw(scope, "
|
|
180
|
+
throw(scope, "readSync/readFile: path is required");
|
|
134
181
|
return;
|
|
135
182
|
}
|
|
136
183
|
let path_str = v8_to_string(scope, path_val);
|
|
137
184
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let context = scope.get_current_context();
|
|
144
|
-
let global = context.global(scope);
|
|
145
|
-
let root_key = v8_str(scope, "__titan_root");
|
|
146
|
-
let root_val = global.get(scope, root_key.into()).unwrap();
|
|
185
|
+
let root = super::PROJECT_ROOT.get().cloned().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
|
186
|
+
let joined = root.join(&path_str);
|
|
147
187
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
188
|
+
// Security Check
|
|
189
|
+
if let Ok(target) = joined.canonicalize() {
|
|
190
|
+
// In Docker, /app/static/index.html vs /app
|
|
191
|
+
// Canonical paths might resolve symlinks.
|
|
192
|
+
// We just ensure it's within root or a subdirectory.
|
|
193
|
+
// For simplicity in this fix, we trust canonicalize logic if it exists, otherwise strict join.
|
|
194
|
+
if target.starts_with(&root.canonicalize().unwrap_or(root.clone())) {
|
|
195
|
+
match std::fs::read_to_string(&target) {
|
|
196
|
+
Ok(content) => {
|
|
197
|
+
let v8_content = v8_str(scope, &content);
|
|
198
|
+
retval.set(v8_content.into());
|
|
199
|
+
},
|
|
200
|
+
Err(e) => {
|
|
201
|
+
// Return null or throw? Node's readFile throws. Titan types say return string.
|
|
202
|
+
// The user's code: fs.readFile(...) || "Default"
|
|
203
|
+
// This implies it might return undefined/null on failure?
|
|
204
|
+
// Or maybe they expect it to succeed.
|
|
205
|
+
// Let's throw to be safe for debugging, or return null if not found?
|
|
206
|
+
// "||" handles null/undefined usually.
|
|
207
|
+
// But usually readFile throws if file not found.
|
|
208
|
+
// Let's print error and return null to avoid crashing entire worker init if optional.
|
|
209
|
+
retval.set(v8::null(scope).into());
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
retval.set(v8::null(scope).into());
|
|
164
214
|
}
|
|
165
|
-
}
|
|
215
|
+
} else {
|
|
216
|
+
// File doesn't exist usually
|
|
217
|
+
retval.set(v8::null(scope).into());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
166
220
|
|
|
167
|
-
|
|
168
|
-
|
|
221
|
+
fn native_read(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
222
|
+
let path_val = args.get(0);
|
|
223
|
+
if !path_val.is_string() {
|
|
224
|
+
throw(scope, "t.read(path): path is required");
|
|
169
225
|
return;
|
|
170
226
|
}
|
|
227
|
+
let path_str = v8_to_string(scope, path_val);
|
|
171
228
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
229
|
+
// Return Metadata (Non-blocking for drift)
|
|
230
|
+
let obj = v8::Object::new(scope);
|
|
231
|
+
let op_key = v8_str(scope, "__titanAsync");
|
|
232
|
+
let op_val = v8::Boolean::new(scope, true);
|
|
233
|
+
obj.set(scope, op_key.into(), op_val.into());
|
|
234
|
+
|
|
235
|
+
let type_key = v8_str(scope, "type");
|
|
236
|
+
let type_val = v8_str(scope, "fs_read");
|
|
237
|
+
obj.set(scope, type_key.into(), type_val.into());
|
|
238
|
+
|
|
239
|
+
let data_obj = v8::Object::new(scope);
|
|
240
|
+
let path_k = v8_str(scope, "path");
|
|
241
|
+
let path_v = v8_str(scope, &path_str);
|
|
242
|
+
data_obj.set(scope, path_k.into(), path_v.into());
|
|
243
|
+
|
|
244
|
+
let data_key = v8_str(scope, "data");
|
|
245
|
+
obj.set(scope, data_key.into(), data_obj.into());
|
|
246
|
+
|
|
247
|
+
retval.set(obj.into());
|
|
180
248
|
}
|
|
181
249
|
|
|
182
250
|
fn native_decode_utf8(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
@@ -293,94 +361,7 @@ fn native_log(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments,
|
|
|
293
361
|
);
|
|
294
362
|
}
|
|
295
363
|
|
|
296
|
-
fn native_fetch(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
297
|
-
let url = v8_to_string(scope, args.get(0));
|
|
298
|
-
let mut method = "GET".to_string();
|
|
299
|
-
let mut body_str = None;
|
|
300
|
-
let mut headers_vec = Vec::new();
|
|
301
|
-
|
|
302
|
-
let opts_val = args.get(1);
|
|
303
|
-
if opts_val.is_object() {
|
|
304
|
-
let opts_obj = opts_val.to_object(scope).unwrap();
|
|
305
|
-
|
|
306
|
-
let m_key = v8_str(scope, "method");
|
|
307
|
-
if let Some(m_val) = opts_obj.get(scope, m_key.into()) {
|
|
308
|
-
if m_val.is_string() {
|
|
309
|
-
method = v8_to_string(scope, m_val);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
let b_key = v8_str(scope, "body");
|
|
314
|
-
if let Some(b_val) = opts_obj.get(scope, b_key.into()) {
|
|
315
|
-
if b_val.is_string() {
|
|
316
|
-
body_str = Some(v8_to_string(scope, b_val));
|
|
317
|
-
} else if b_val.is_object() {
|
|
318
|
-
let json_obj = v8::json::stringify(scope, b_val).unwrap();
|
|
319
|
-
body_str = Some(json_obj.to_rust_string_lossy(scope));
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
let h_key = v8_str(scope, "headers");
|
|
324
|
-
if let Some(h_val) = opts_obj.get(scope, h_key.into()) {
|
|
325
|
-
if h_val.is_object() {
|
|
326
|
-
let h_obj = h_val.to_object(scope).unwrap();
|
|
327
|
-
if let Some(keys) = h_obj.get_own_property_names(scope, Default::default()) {
|
|
328
|
-
for i in 0..keys.length() {
|
|
329
|
-
let key = keys.get_index(scope, i).unwrap();
|
|
330
|
-
let val = h_obj.get(scope, key).unwrap();
|
|
331
|
-
headers_vec.push((v8_to_string(scope, key), v8_to_string(scope, val)));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
364
|
|
|
338
|
-
let client = Client::builder().use_rustls_tls().tcp_nodelay(true).build().unwrap_or(Client::new());
|
|
339
|
-
let mut req = client.request(method.parse().unwrap_or(reqwest::Method::GET), &url);
|
|
340
|
-
|
|
341
|
-
for (k, v) in headers_vec {
|
|
342
|
-
if let (Ok(name), Ok(val)) = (HeaderName::from_bytes(k.as_bytes()), HeaderValue::from_str(&v)) {
|
|
343
|
-
let mut map = HeaderMap::new();
|
|
344
|
-
map.insert(name, val);
|
|
345
|
-
req = req.headers(map);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if let Some(b) = body_str {
|
|
350
|
-
req = req.body(b);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
let res = req.send();
|
|
354
|
-
let obj = v8::Object::new(scope);
|
|
355
|
-
match res {
|
|
356
|
-
Ok(r) => {
|
|
357
|
-
let status = r.status().as_u16();
|
|
358
|
-
let text = r.text().unwrap_or_default();
|
|
359
|
-
|
|
360
|
-
let status_key = v8_str(scope, "status");
|
|
361
|
-
let status_val = v8::Number::new(scope, status as f64);
|
|
362
|
-
obj.set(scope, status_key.into(), status_val.into());
|
|
363
|
-
|
|
364
|
-
let body_key = v8_str(scope, "body");
|
|
365
|
-
let body_val = v8_str(scope, &text);
|
|
366
|
-
obj.set(scope, body_key.into(), body_val.into());
|
|
367
|
-
|
|
368
|
-
let ok_key = v8_str(scope, "ok");
|
|
369
|
-
let ok_val = v8::Boolean::new(scope, true);
|
|
370
|
-
obj.set(scope, ok_key.into(), ok_val.into());
|
|
371
|
-
},
|
|
372
|
-
Err(e) => {
|
|
373
|
-
let ok_key = v8_str(scope, "ok");
|
|
374
|
-
let ok_val = v8::Boolean::new(scope, false);
|
|
375
|
-
obj.set(scope, ok_key.into(), ok_val.into());
|
|
376
|
-
|
|
377
|
-
let err_key = v8_str(scope, "error");
|
|
378
|
-
let err_val = v8_str(scope, &e.to_string());
|
|
379
|
-
obj.set(scope, err_key.into(), err_val.into());
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
retval.set(obj.into());
|
|
383
|
-
}
|
|
384
365
|
|
|
385
366
|
fn native_jwt_sign(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
386
367
|
let payload_val = args.get(0);
|
|
@@ -529,60 +510,368 @@ fn native_db_query(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArgume
|
|
|
529
510
|
throw(scope, "db.query(): SQL query is required");
|
|
530
511
|
return;
|
|
531
512
|
}
|
|
513
|
+
|
|
514
|
+
// Return Metadata (Non-blocking)
|
|
515
|
+
let obj = v8::Object::new(scope);
|
|
516
|
+
let op_key = v8_str(scope, "__titanAsync");
|
|
517
|
+
let op_val = v8::Boolean::new(scope, true);
|
|
518
|
+
obj.set(scope, op_key.into(), op_val.into());
|
|
532
519
|
|
|
533
|
-
|
|
534
|
-
let
|
|
535
|
-
|
|
520
|
+
let type_key = v8_str(scope, "type");
|
|
521
|
+
let type_val = v8_str(scope, "db_query");
|
|
522
|
+
obj.set(scope, type_key.into(), type_val.into());
|
|
536
523
|
|
|
537
|
-
let
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
524
|
+
let data_obj = v8::Object::new(scope);
|
|
525
|
+
let conn_k = v8_str(scope, "conn");
|
|
526
|
+
let conn_v = v8_str(scope, &conn_string);
|
|
527
|
+
data_obj.set(scope, conn_k.into(), conn_v.into());
|
|
528
|
+
|
|
529
|
+
let q_k = v8_str(scope, "query");
|
|
530
|
+
let q_v = v8_str(scope, &query);
|
|
531
|
+
data_obj.set(scope, q_k.into(), q_v.into());
|
|
532
|
+
|
|
533
|
+
let data_key = v8_str(scope, "data");
|
|
534
|
+
obj.set(scope, data_key.into(), data_obj.into());
|
|
535
|
+
|
|
536
|
+
retval.set(obj.into());
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
fn native_fetch_meta(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
540
|
+
let url = v8_to_string(scope, args.get(0));
|
|
541
|
+
let opts = args.get(1);
|
|
542
|
+
|
|
543
|
+
let obj = v8::Object::new(scope);
|
|
544
|
+
let op_key = v8_str(scope, "__titanAsync");
|
|
545
|
+
let op_val = v8::Boolean::new(scope, true);
|
|
546
|
+
obj.set(scope, op_key.into(), op_val.into());
|
|
547
|
+
|
|
548
|
+
let type_key = v8_str(scope, "type");
|
|
549
|
+
let type_val = v8_str(scope, "fetch");
|
|
550
|
+
obj.set(scope, type_key.into(), type_val.into());
|
|
551
|
+
|
|
552
|
+
let data_obj = v8::Object::new(scope);
|
|
553
|
+
let url_key = v8_str(scope, "url");
|
|
554
|
+
let url_val = v8_str(scope, &url);
|
|
555
|
+
data_obj.set(scope, url_key.into(), url_val.into());
|
|
556
|
+
|
|
557
|
+
let opts_key = v8_str(scope, "opts");
|
|
558
|
+
data_obj.set(scope, opts_key.into(), opts);
|
|
559
|
+
|
|
560
|
+
let data_key = v8_str(scope, "data");
|
|
561
|
+
obj.set(scope, data_key.into(), data_obj.into());
|
|
562
|
+
|
|
563
|
+
retval.set(obj.into());
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
fn parse_async_op(scope: &mut v8::HandleScope, op_val: v8::Local<v8::Value>) -> Option<super::TitanAsyncOp> {
|
|
567
|
+
if !op_val.is_object() { return None; }
|
|
568
|
+
let op_obj = op_val.to_object(scope).unwrap();
|
|
569
|
+
|
|
570
|
+
let type_key = v8_str(scope, "type");
|
|
571
|
+
let type_obj = op_obj.get(scope, type_key.into())?;
|
|
572
|
+
let op_type = v8_to_string(scope, type_obj);
|
|
573
|
+
|
|
574
|
+
let data_key = v8_str(scope, "data");
|
|
575
|
+
let data_val = op_obj.get(scope, data_key.into())?;
|
|
576
|
+
if !data_val.is_object() { return None; }
|
|
577
|
+
let data_obj = data_val.to_object(scope).unwrap();
|
|
578
|
+
|
|
579
|
+
match op_type.as_str() {
|
|
580
|
+
"fetch" => {
|
|
581
|
+
let url_key = v8_str(scope, "url");
|
|
582
|
+
let url_obj = data_obj.get(scope, url_key.into())?;
|
|
583
|
+
let url = v8_to_string(scope, url_obj);
|
|
584
|
+
|
|
585
|
+
let mut method = "GET".to_string();
|
|
586
|
+
let mut body = None;
|
|
587
|
+
let mut headers = Vec::new();
|
|
588
|
+
|
|
589
|
+
let opts_key = v8_str(scope, "opts");
|
|
590
|
+
if let Some(opts_val) = data_obj.get(scope, opts_key.into()) {
|
|
591
|
+
if opts_val.is_object() {
|
|
592
|
+
let opts_obj = opts_val.to_object(scope).unwrap();
|
|
593
|
+
let m_key = v8_str(scope, "method");
|
|
594
|
+
if let Some(m_val) = opts_obj.get(scope, m_key.into()) {
|
|
595
|
+
if m_val.is_string() { method = v8_to_string(scope, m_val); }
|
|
596
|
+
}
|
|
597
|
+
let b_key = v8_str(scope, "body");
|
|
598
|
+
if let Some(b_val) = opts_obj.get(scope, b_key.into()) {
|
|
599
|
+
if b_val.is_string() {
|
|
600
|
+
body = Some(v8_to_string(scope, b_val));
|
|
601
|
+
} else if b_val.is_object() {
|
|
602
|
+
body = Some(v8::json::stringify(scope, b_val).unwrap().to_rust_string_lossy(scope));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
let h_key = v8_str(scope, "headers");
|
|
606
|
+
if let Some(h_val) = opts_obj.get(scope, h_key.into()) {
|
|
607
|
+
if h_val.is_object() {
|
|
608
|
+
let h_obj = h_val.to_object(scope).unwrap();
|
|
609
|
+
if let Some(keys) = h_obj.get_own_property_names(scope, Default::default()) {
|
|
610
|
+
for i in 0..keys.length() {
|
|
611
|
+
let key = keys.get_index(scope, i).unwrap();
|
|
612
|
+
let val = h_obj.get(scope, key).unwrap();
|
|
613
|
+
headers.push((v8_to_string(scope, key), v8_to_string(scope, val)));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
Some(super::TitanAsyncOp::Fetch { url, method, body, headers })
|
|
621
|
+
},
|
|
622
|
+
"db_query" => {
|
|
623
|
+
let conn_key = v8_str(scope, "conn");
|
|
624
|
+
let conn_obj = data_obj.get(scope, conn_key.into())?;
|
|
625
|
+
let conn = v8_to_string(scope, conn_obj);
|
|
626
|
+
let query_key = v8_str(scope, "query");
|
|
627
|
+
let query_obj = data_obj.get(scope, query_key.into())?;
|
|
628
|
+
let query = v8_to_string(scope, query_obj);
|
|
629
|
+
Some(super::TitanAsyncOp::DbQuery { conn, query })
|
|
630
|
+
},
|
|
631
|
+
"fs_read" => {
|
|
632
|
+
let path_key = v8_str(scope, "path");
|
|
633
|
+
let path_obj = data_obj.get(scope, path_key.into())?;
|
|
634
|
+
let path = v8_to_string(scope, path_obj);
|
|
635
|
+
Some(super::TitanAsyncOp::FsRead { path })
|
|
636
|
+
},
|
|
637
|
+
_ => None
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
fn native_drift_call(scope: &mut v8::HandleScope, mut args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
|
|
642
|
+
let runtime_ptr = unsafe { args.get_isolate() }.get_data(0) as *mut super::TitanRuntime;
|
|
643
|
+
let runtime = unsafe { &mut *runtime_ptr };
|
|
644
|
+
// let isolate_id = runtime.id; // We can use runtime.id directly later
|
|
645
|
+
|
|
646
|
+
let arg0 = args.get(0);
|
|
647
|
+
|
|
648
|
+
let (async_op, op_type) = if arg0.is_array() {
|
|
649
|
+
let arr = v8::Local::<v8::Array>::try_from(arg0).unwrap();
|
|
650
|
+
let mut ops = Vec::new();
|
|
651
|
+
for i in 0..arr.length() {
|
|
652
|
+
let op_val = arr.get_index(scope, i).unwrap();
|
|
653
|
+
if let Some(op) = parse_async_op(scope, op_val) {
|
|
654
|
+
ops.push(op);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
(super::TitanAsyncOp::Batch(ops), "batch".to_string())
|
|
658
|
+
} else {
|
|
659
|
+
match parse_async_op(scope, arg0) {
|
|
660
|
+
Some(op) => {
|
|
661
|
+
let t = match &op {
|
|
662
|
+
super::TitanAsyncOp::Fetch { .. } => "fetch",
|
|
663
|
+
super::TitanAsyncOp::DbQuery { .. } => "db_query",
|
|
664
|
+
super::TitanAsyncOp::FsRead { .. } => "fs_read",
|
|
665
|
+
_ => "unknown"
|
|
666
|
+
};
|
|
667
|
+
(op, t.to_string())
|
|
668
|
+
},
|
|
669
|
+
None => {
|
|
670
|
+
throw(scope, "drift() requires an async operation or array of operations");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
542
673
|
}
|
|
543
674
|
};
|
|
675
|
+
|
|
676
|
+
let runtime_ptr = unsafe { args.get_isolate() }.get_data(0) as *mut super::TitanRuntime;
|
|
677
|
+
let runtime = unsafe { &mut *runtime_ptr };
|
|
544
678
|
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
679
|
+
// Extract request_id from globalThis.__titan_req.__titan_request_id
|
|
680
|
+
let req_id = {
|
|
681
|
+
let context = scope.get_current_context();
|
|
682
|
+
let global = context.global(scope);
|
|
683
|
+
let req_key = v8_str(scope, "__titan_req");
|
|
684
|
+
if let Some(req_obj_val) = global.get(scope, req_key.into()) {
|
|
685
|
+
if req_obj_val.is_object() {
|
|
686
|
+
let req_obj = req_obj_val.to_object(scope).unwrap();
|
|
687
|
+
let id_key = v8_str(scope, "__titan_request_id");
|
|
688
|
+
req_obj.get(scope, id_key.into()).unwrap().uint32_value(scope).unwrap_or(0)
|
|
689
|
+
} else { 0 }
|
|
690
|
+
} else { 0 }
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
runtime.drift_counter += 1;
|
|
694
|
+
let drift_id = runtime.drift_counter;
|
|
695
|
+
|
|
696
|
+
if req_id != 0 {
|
|
697
|
+
runtime.drift_to_request.insert(drift_id, req_id);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// --- REPLAY CHECK ---
|
|
701
|
+
// If the result exists, return it immediately (Replay Phase)
|
|
702
|
+
// IMPORTANT: Use .get() not .remove() to allow multiple drifts in the same action
|
|
703
|
+
if let Some(res) = runtime.completed_drifts.get(&drift_id) {
|
|
704
|
+
let json_str = serde_json::to_string(res).unwrap_or_else(|_| "null".to_string());
|
|
705
|
+
let v8_str = v8::String::new(scope, &json_str).unwrap();
|
|
706
|
+
let mut try_catch = v8::TryCatch::new(scope);
|
|
707
|
+
if let Some(val) = v8::json::parse(&mut try_catch, v8_str) {
|
|
708
|
+
retval.set(val);
|
|
709
|
+
} else {
|
|
710
|
+
retval.set(v8::null(&mut try_catch).into());
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let (tx, rx) = tokio::sync::oneshot::channel::<super::WorkerAsyncResult>();
|
|
716
|
+
|
|
717
|
+
// Send to global async executor
|
|
718
|
+
let req = super::AsyncOpRequest {
|
|
719
|
+
op: async_op,
|
|
720
|
+
drift_id,
|
|
721
|
+
request_id: req_id,
|
|
722
|
+
op_type,
|
|
723
|
+
respond_tx: tx,
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
if let Err(e) = runtime.global_async_tx.try_send(req) {
|
|
727
|
+
println!("[Titan] Drift Call Failed to queue: {}", e);
|
|
728
|
+
retval.set(v8::null(scope).into());
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// --- SUSPENSION THROW ---
|
|
733
|
+
// We throw a specific exception to halt execution. The Runtime catches this,
|
|
734
|
+
// frees the worker, and waits for the async result.
|
|
735
|
+
|
|
736
|
+
// Trigger Tokio task completion handling in a separate bridge
|
|
737
|
+
let tokio_handle = runtime.tokio_handle.clone();
|
|
738
|
+
let worker_tx = runtime.worker_tx.clone();
|
|
739
|
+
|
|
740
|
+
tokio_handle.spawn(async move {
|
|
741
|
+
if let Ok(res) = rx.await {
|
|
742
|
+
// Signal the pool to RESUME (REPLAY) this specific isolate
|
|
743
|
+
let _ = worker_tx.send(crate::runtime::WorkerCommand::Resume {
|
|
744
|
+
drift_id,
|
|
745
|
+
result: res,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
throw(scope, "__SUSPEND__");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
fn native_finish_request(scope: &mut v8::HandleScope, mut args: v8::FunctionCallbackArguments, _retval: v8::ReturnValue) {
|
|
754
|
+
let request_id = args.get(0).uint32_value(scope).unwrap_or(0);
|
|
755
|
+
let result_val = args.get(1);
|
|
756
|
+
let json = super::v8_to_json(scope, result_val);
|
|
757
|
+
|
|
758
|
+
let runtime_ptr = unsafe { args.get_isolate() }.get_data(0) as *mut super::TitanRuntime;
|
|
759
|
+
let runtime = unsafe { &mut *runtime_ptr };
|
|
760
|
+
|
|
761
|
+
let timings = runtime.request_timings.remove(&request_id).unwrap_or_default();
|
|
762
|
+
|
|
763
|
+
// Cleanup drift mapping for this request
|
|
764
|
+
runtime.drift_to_request.retain(|drift_id, v| {
|
|
765
|
+
if *v == request_id {
|
|
766
|
+
runtime.completed_drifts.remove(drift_id);
|
|
767
|
+
false
|
|
768
|
+
} else {
|
|
769
|
+
true
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
if let Some(tx) = runtime.pending_requests.remove(&request_id) {
|
|
774
|
+
let _ = tx.send(crate::runtime::WorkerResult { json, timings });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
pub async fn run_single_op(op: super::TitanAsyncOp) -> serde_json::Value {
|
|
779
|
+
match op {
|
|
780
|
+
super::TitanAsyncOp::Fetch { url, method, body, headers } => {
|
|
781
|
+
let client = get_http_client();
|
|
782
|
+
let mut req = client.request(method.parse().unwrap_or(reqwest::Method::GET), &url);
|
|
783
|
+
if let Some(b) = body { req = req.body(b); }
|
|
784
|
+
for (k, v) in headers {
|
|
785
|
+
if let (Ok(name), Ok(val)) = (reqwest::header::HeaderName::from_bytes(k.as_bytes()), reqwest::header::HeaderValue::from_str(&v)) {
|
|
786
|
+
req = req.header(name, val);
|
|
570
787
|
}
|
|
571
|
-
|
|
572
|
-
result.push(serde_json::Value::Object(obj));
|
|
573
788
|
}
|
|
789
|
+
match req.send().await {
|
|
790
|
+
Ok(res) => {
|
|
791
|
+
let status = res.status().as_u16();
|
|
792
|
+
let text = res.text().await.unwrap_or_default();
|
|
793
|
+
serde_json::json!({ "status": status, "body": text, "ok": true })
|
|
794
|
+
},
|
|
795
|
+
Err(e) => serde_json::json!({ "error": e.to_string(), "ok": false })
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
super::TitanAsyncOp::FsRead { path } => {
|
|
799
|
+
let root = super::PROJECT_ROOT.get().cloned().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
|
800
|
+
let joined = root.join(&path);
|
|
574
801
|
|
|
575
|
-
|
|
576
|
-
let
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
802
|
+
// Basic security check
|
|
803
|
+
if let Ok(target) = joined.canonicalize() {
|
|
804
|
+
if target.starts_with(&root.canonicalize().unwrap_or(root)) {
|
|
805
|
+
match std::fs::read_to_string(&target) {
|
|
806
|
+
Ok(content) => serde_json::json!(content),
|
|
807
|
+
Err(e) => serde_json::json!({ "error": format!("File read failed: {}", e) })
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
serde_json::json!({ "error": "Path escapes project root" })
|
|
811
|
+
}
|
|
580
812
|
} else {
|
|
581
|
-
|
|
813
|
+
serde_json::json!({ "error": format!("File not found: {}", path) })
|
|
582
814
|
}
|
|
583
815
|
},
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
816
|
+
super::TitanAsyncOp::DbQuery { conn, query } => {
|
|
817
|
+
tokio::task::spawn_blocking(move || {
|
|
818
|
+
let mut pool = DB_POOL.lock().unwrap();
|
|
819
|
+
if let Some(map) = pool.as_mut() {
|
|
820
|
+
if let Some(client) = map.get_mut(&conn) {
|
|
821
|
+
return match client.query(&query, &[]) {
|
|
822
|
+
Ok(rows) => {
|
|
823
|
+
let mut result = Vec::new();
|
|
824
|
+
for row in rows {
|
|
825
|
+
let mut obj = serde_json::Map::new();
|
|
826
|
+
for (i, column) in row.columns().iter().enumerate() {
|
|
827
|
+
let col_name = column.name();
|
|
828
|
+
let col_value: serde_json::Value = if let Ok(val) = row.try_get::<_, Option<String>>(i) {
|
|
829
|
+
serde_json::json!(val)
|
|
830
|
+
} else if let Ok(val) = row.try_get::<_, Option<i32>>(i) {
|
|
831
|
+
serde_json::json!(val)
|
|
832
|
+
} else if let Ok(val) = row.try_get::<_, Option<i64>>(i) {
|
|
833
|
+
serde_json::json!(val)
|
|
834
|
+
} else if let Ok(val) = row.try_get::<_, Option<f64>>(i) {
|
|
835
|
+
serde_json::json!(val)
|
|
836
|
+
} else if let Ok(val) = row.try_get::<_, Option<bool>>(i) {
|
|
837
|
+
serde_json::json!(val)
|
|
838
|
+
} else {
|
|
839
|
+
serde_json::Value::Null
|
|
840
|
+
};
|
|
841
|
+
obj.insert(col_name.to_string(), col_value);
|
|
842
|
+
}
|
|
843
|
+
result.push(serde_json::Value::Object(obj));
|
|
844
|
+
}
|
|
845
|
+
serde_json::Value::Array(result)
|
|
846
|
+
},
|
|
847
|
+
Err(e) => serde_json::json!({ "error": e.to_string() })
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
serde_json::json!({ "error": "Database connection not found" })
|
|
852
|
+
}).await.unwrap_or_else(|e| serde_json::json!({ "error": e.to_string() }))
|
|
853
|
+
},
|
|
854
|
+
_ => serde_json::json!({ "error": "Invalid operation" })
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
pub async fn run_async_operation(op: super::TitanAsyncOp) -> serde_json::Value {
|
|
859
|
+
match op {
|
|
860
|
+
super::TitanAsyncOp::Batch(ops) => {
|
|
861
|
+
let mut set = tokio::task::JoinSet::new();
|
|
862
|
+
for (i, op) in ops.into_iter().enumerate() {
|
|
863
|
+
set.spawn(async move {
|
|
864
|
+
(i, run_single_op(op).await)
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
let mut results_map = std::collections::BTreeMap::new();
|
|
868
|
+
while let Some(res) = set.join_next().await {
|
|
869
|
+
if let Ok((i, val)) = res {
|
|
870
|
+
results_map.insert(i, val);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
serde_json::Value::Array(results_map.into_values().collect())
|
|
874
|
+
},
|
|
875
|
+
_ => run_single_op(op).await
|
|
587
876
|
}
|
|
588
877
|
}
|