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.
@@ -1,877 +1,1038 @@
1
- use v8;
2
- use reqwest::{
3
- blocking::Client,
4
- header::{HeaderMap, HeaderName, HeaderValue},
5
- };
6
- use std::path::PathBuf;
7
- use std::time::{SystemTime, UNIX_EPOCH};
8
- use serde_json::Value;
9
- use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
10
- use bcrypt::{hash, verify, DEFAULT_COST};
11
- use postgres::{Client as PgClient, NoTls};
12
- use std::sync::{Mutex, OnceLock};
13
- use std::collections::{HashMap, BTreeMap};
14
-
15
- use crate::utils::{blue, gray, red, parse_expires_in};
16
- use super::{TitanRuntime, v8_str, v8_to_string, throw, ShareContextStore};
17
-
18
- const TITAN_CORE_JS: &str = include_str!("titan_core.js");
19
-
20
- // Database connection pool
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
- }
34
-
35
-
36
- pub fn inject_builtin_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>, t_obj: v8::Local<v8::Object>) {
37
- // 1. Native API Bindings
38
-
39
- // defineAction (Native side)
40
- let def_fn = v8::Function::new(scope, native_define_action).unwrap();
41
- let def_key = v8_str(scope, "defineAction");
42
- global.set(scope, def_key.into(), def_fn.into());
43
-
44
-
45
- // t.read
46
- let read_fn = v8::Function::new(scope, native_read).unwrap();
47
- let read_key = v8_str(scope, "read");
48
- t_obj.set(scope, read_key.into(), read_fn.into());
49
-
50
- // t.decodeUtf8
51
- let dec_fn = v8::Function::new(scope, native_decode_utf8).unwrap();
52
- let dec_key = v8_str(scope, "decodeUtf8");
53
- t_obj.set(scope, dec_key.into(), dec_fn.into());
54
-
55
- // t.log
56
- let log_fn = v8::Function::new(scope, native_log).unwrap();
57
- let log_key = v8_str(scope, "log");
58
- t_obj.set(scope, log_key.into(), log_fn.into());
59
-
60
- // t.fetch (Metadata version for drift)
61
- let fetch_fn = v8::Function::new(scope, native_fetch_meta).unwrap();
62
- let fetch_key = v8_str(scope, "fetch");
63
- t_obj.set(scope, fetch_key.into(), fetch_fn.into());
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
-
75
- // t.loadEnv
76
- let env_fn = v8::Function::new(scope, native_load_env).unwrap();
77
- let env_key = v8_str(scope, "loadEnv");
78
- t_obj.set(scope, env_key.into(), env_fn.into());
79
-
80
- // auth, jwt, password, db, core ... (setup native objects BEFORE JS injection)
81
- setup_native_utils(scope, t_obj);
82
-
83
- // 2. JS Side Injection (Embedded)
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"));
93
- }
94
- }
95
-
96
- fn setup_native_utils(scope: &mut v8::HandleScope, t_obj: v8::Local<v8::Object>) {
97
- // t.jwt
98
- let jwt_obj = v8::Object::new(scope);
99
- let sign_fn = v8::Function::new(scope, native_jwt_sign).unwrap();
100
- let verify_fn = v8::Function::new(scope, native_jwt_verify).unwrap();
101
-
102
- let sign_key = v8_str(scope, "sign");
103
- jwt_obj.set(scope, sign_key.into(), sign_fn.into());
104
- let verify_key = v8_str(scope, "verify");
105
- jwt_obj.set(scope, verify_key.into(), verify_fn.into());
106
-
107
- let jwt_key = v8_str(scope, "jwt");
108
- t_obj.set(scope, jwt_key.into(), jwt_obj.into());
109
-
110
- // t.password
111
- let pw_obj = v8::Object::new(scope);
112
- let hash_fn = v8::Function::new(scope, native_password_hash).unwrap();
113
- let pw_verify_fn = v8::Function::new(scope, native_password_verify).unwrap();
114
-
115
- let hash_key = v8_str(scope, "hash");
116
- pw_obj.set(scope, hash_key.into(), hash_fn.into());
117
- let pw_v_key = v8_str(scope, "verify");
118
- pw_obj.set(scope, pw_v_key.into(), pw_verify_fn.into());
119
-
120
- let pw_key = v8_str(scope, "password");
121
- t_obj.set(scope, pw_key.into(), pw_obj.into());
122
-
123
- // t.shareContext (Native primitives)
124
- let sc_obj = v8::Object::new(scope);
125
- let n_get = v8::Function::new(scope, share_context_get).unwrap();
126
- let n_set = v8::Function::new(scope, share_context_set).unwrap();
127
- let n_del = v8::Function::new(scope, share_context_delete).unwrap();
128
- let n_keys = v8::Function::new(scope, share_context_keys).unwrap();
129
- let n_pub = v8::Function::new(scope, share_context_broadcast).unwrap();
130
-
131
- let get_key = v8_str(scope, "get");
132
- sc_obj.set(scope, get_key.into(), n_get.into());
133
- let set_key = v8_str(scope, "set");
134
- sc_obj.set(scope, set_key.into(), n_set.into());
135
- let del_key = v8_str(scope, "delete");
136
- sc_obj.set(scope, del_key.into(), n_del.into());
137
- let keys_key = v8_str(scope, "keys");
138
- sc_obj.set(scope, keys_key.into(), n_keys.into());
139
- let pub_key = v8_str(scope, "broadcast");
140
- sc_obj.set(scope, pub_key.into(), n_pub.into());
141
-
142
- let sc_key = v8_str(scope, "shareContext");
143
- let sc_val = sc_obj.into();
144
- t_obj.set(scope, sc_key.into(), sc_val);
145
-
146
- // t.db (Database operations)
147
- let db_obj = v8::Object::new(scope);
148
- let db_connect_fn = v8::Function::new(scope, native_db_connect).unwrap();
149
- let connect_key = v8_str(scope, "connect");
150
- db_obj.set(scope, connect_key.into(), db_connect_fn.into());
151
-
152
- let db_key = v8_str(scope, "db");
153
- t_obj.set(scope, db_key.into(), db_obj.into());
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
-
175
- }
176
-
177
- fn native_read_sync(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
178
- let path_val = args.get(0);
179
- if !path_val.is_string() {
180
- throw(scope, "readSync/readFile: path is required");
181
- return;
182
- }
183
- let path_str = v8_to_string(scope, path_val);
184
-
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);
187
-
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());
214
- }
215
- } else {
216
- // File doesn't exist usually
217
- retval.set(v8::null(scope).into());
218
- }
219
- }
220
-
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");
225
- return;
226
- }
227
- let path_str = v8_to_string(scope, path_val);
228
-
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());
248
- }
249
-
250
- fn native_decode_utf8(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
251
- let val = args.get(0);
252
- if let Ok(u8arr) = v8::Local::<v8::Uint8Array>::try_from(val) {
253
- let buf = u8arr.buffer(scope).unwrap();
254
- let store = v8::ArrayBuffer::get_backing_store(&buf);
255
- let offset = usize::from(u8arr.byte_offset());
256
- let length = usize::from(u8arr.byte_length());
257
- let slice = &store[offset..offset+length];
258
-
259
- let bytes: Vec<u8> = slice.iter().map(|b| b.get()).collect();
260
- let s = String::from_utf8_lossy(&bytes);
261
- retval.set(v8_str(scope, &s).into());
262
- } else if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(val) {
263
- let store = v8::ArrayBuffer::get_backing_store(&ab);
264
- let bytes: Vec<u8> = store.iter().map(|b| b.get()).collect();
265
- let s = String::from_utf8_lossy(&bytes);
266
- retval.set(v8_str(scope, &s).into());
267
- } else {
268
- retval.set(v8::null(scope).into());
269
- }
270
- }
271
-
272
- fn share_context_get(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
273
- let key = v8_to_string(scope, args.get(0));
274
- let store = ShareContextStore::get();
275
- if let Some(val) = store.kv.get(&key) {
276
- let json_str = val.to_string();
277
- let v8_str = v8::String::new(scope, &json_str).unwrap();
278
- if let Some(v8_val) = v8::json::parse(scope, v8_str) {
279
- retval.set(v8_val);
280
- return;
281
- }
282
- }
283
- retval.set(v8::null(scope).into());
284
- }
285
-
286
- fn share_context_set(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
287
- let key = v8_to_string(scope, args.get(0));
288
- let val_v8 = args.get(1);
289
-
290
- if let Some(json_v8) = v8::json::stringify(scope, val_v8) {
291
- let json_str = json_v8.to_rust_string_lossy(scope);
292
- if let Ok(val) = serde_json::from_str(&json_str) {
293
- ShareContextStore::get().kv.insert(key, val);
294
- }
295
- }
296
- }
297
-
298
- fn share_context_delete(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
299
- let key = v8_to_string(scope, args.get(0));
300
- ShareContextStore::get().kv.remove(&key);
301
- }
302
-
303
- fn share_context_keys(scope: &mut v8::HandleScope, _args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
304
- let store = ShareContextStore::get();
305
- let keys: Vec<v8::Local<v8::Value>> = store.kv.iter().map(|kv| v8_str(scope, kv.key()).into()).collect();
306
- let arr = v8::Array::new_with_elements(scope, &keys);
307
- retval.set(arr.into());
308
- }
309
-
310
- fn share_context_broadcast(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
311
- let event = v8_to_string(scope, args.get(0));
312
- let payload_v8 = args.get(1);
313
-
314
- if let Some(json_v8) = v8::json::stringify(scope, payload_v8) {
315
- let json_str = json_v8.to_rust_string_lossy(scope);
316
- if let Ok(payload) = serde_json::from_str(&json_str) {
317
- let _ = ShareContextStore::get().broadcast_tx.send((event, payload));
318
- }
319
- }
320
- }
321
-
322
-
323
-
324
- fn native_log(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
325
- let context = scope.get_current_context();
326
- let global = context.global(scope);
327
- let action_key = v8_str(scope, "__titan_action");
328
- let action_name = if let Some(action_val) = global.get(scope, action_key.into()) {
329
- if action_val.is_string() {
330
- v8_to_string(scope, action_val)
331
- } else {
332
- "init".to_string()
333
- }
334
- } else {
335
- "init".to_string()
336
- };
337
-
338
- let mut parts = Vec::new();
339
- for i in 0..args.length() {
340
- let val = args.get(i);
341
- let mut appended = false;
342
-
343
- if val.is_object() && !val.is_function() {
344
- if let Some(json) = v8::json::stringify(scope, val) {
345
- parts.push(json.to_rust_string_lossy(scope));
346
- appended = true;
347
- }
348
- }
349
-
350
- if !appended {
351
- parts.push(v8_to_string(scope, val));
352
- }
353
- }
354
-
355
- let titan_str = blue("[Titan]");
356
- let log_msg = gray(&format!("\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m", action_name, parts.join(" ")));
357
- println!(
358
- "{} {}",
359
- titan_str,
360
- log_msg
361
- );
362
- }
363
-
364
-
365
-
366
- fn native_jwt_sign(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
367
- let payload_val = args.get(0);
368
- let json_str = v8::json::stringify(scope, payload_val).unwrap().to_rust_string_lossy(scope);
369
- let mut payload: serde_json::Map<String, Value> = serde_json::from_str(&json_str).unwrap_or_default();
370
- let secret = v8_to_string(scope, args.get(1));
371
-
372
- let opts_val = args.get(2);
373
- if opts_val.is_object() {
374
- let opts_obj = opts_val.to_object(scope).unwrap();
375
- let exp_key = v8_str(scope, "expiresIn");
376
- if let Some(val) = opts_obj.get(scope, exp_key.into()) {
377
- let seconds = if val.is_number() {
378
- Some(val.to_number(scope).unwrap().value() as u64)
379
- } else if val.is_string() {
380
- parse_expires_in(&v8_to_string(scope, val))
381
- } else { None };
382
- if let Some(sec) = seconds {
383
- let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
384
- payload.insert("exp".to_string(), Value::Number(serde_json::Number::from(now + sec)));
385
- }
386
- }
387
- }
388
-
389
- let token = encode(&Header::default(), &Value::Object(payload), &EncodingKey::from_secret(secret.as_bytes()));
390
- match token {
391
- Ok(t) => {
392
- let res = v8_str(scope, &t);
393
- retval.set(res.into());
394
- },
395
- Err(e) => throw(scope, &e.to_string()),
396
- }
397
- }
398
-
399
- fn native_jwt_verify(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
400
- let token = v8_to_string(scope, args.get(0));
401
- let secret = v8_to_string(scope, args.get(1));
402
- let mut validation = Validation::default();
403
- validation.validate_exp = true;
404
- let data = decode::<Value>(&token, &DecodingKey::from_secret(secret.as_bytes()), &validation);
405
- match data {
406
- Ok(d) => {
407
- let json_str = serde_json::to_string(&d.claims).unwrap();
408
- let v8_json_str = v8_str(scope, &json_str);
409
- if let Some(val) = v8::json::parse(scope, v8_json_str) {
410
- retval.set(val);
411
- }
412
- },
413
- Err(e) => throw(scope, &format!("Invalid or expired JWT: {}", e)),
414
- }
415
- }
416
-
417
- fn native_password_hash(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
418
- let pw = v8_to_string(scope, args.get(0));
419
- match hash(pw, DEFAULT_COST) {
420
- Ok(h) => {
421
- let res = v8_str(scope, &h);
422
- retval.set(res.into());
423
- },
424
- Err(e) => throw(scope, &e.to_string()),
425
- }
426
- }
427
-
428
- fn native_password_verify(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
429
- let pw = v8_to_string(scope, args.get(0));
430
- let hash_str = v8_to_string(scope, args.get(1));
431
- let ok = verify(pw, &hash_str).unwrap_or(false);
432
- retval.set(v8::Boolean::new(scope, ok).into());
433
- }
434
-
435
- fn native_load_env(scope: &mut v8::HandleScope, _args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
436
- use serde_json::json;
437
-
438
- let mut map = serde_json::Map::new();
439
-
440
- for (key, value) in std::env::vars() {
441
- map.insert(key, json!(value));
442
- }
443
-
444
- let json_str = serde_json::to_string(&map).unwrap();
445
- let v8_str = v8::String::new(scope, &json_str).unwrap();
446
-
447
- if let Some(obj) = v8::json::parse(scope, v8_str) {
448
- retval.set(obj);
449
- } else {
450
- retval.set(v8::null(scope).into());
451
- }
452
- }
453
-
454
- fn native_define_action(_scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
455
- retval.set(args.get(0));
456
- }
457
-
458
- fn native_db_connect(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
459
- let conn_string = v8_to_string(scope, args.get(0));
460
-
461
- if conn_string.is_empty() {
462
- throw(scope, "t.db.connect(): connection string is required");
463
- return;
464
- }
465
-
466
- // Test connection immediately
467
- match PgClient::connect(&conn_string, NoTls) {
468
- Ok(mut client) => {
469
- // Store in pool
470
- let mut pool = DB_POOL.lock().unwrap();
471
- let map = pool.get_or_insert_with(HashMap::new);
472
- map.insert(conn_string.clone(), client);
473
- },
474
- Err(e) => {
475
- throw(scope, &format!("Database connection failed: {}", e));
476
- return;
477
- }
478
- }
479
-
480
- // Return a DB connection object with methods
481
- let db_conn_obj = v8::Object::new(scope);
482
-
483
- // Store connection string in a hidden property
484
- let conn_key = v8_str(scope, "__conn_string");
485
- let conn_val = v8_str(scope, &conn_string);
486
- db_conn_obj.set(scope, conn_key.into(), conn_val.into());
487
-
488
- // Add query method
489
- let query_fn = v8::Function::new(scope, native_db_query).unwrap();
490
- let query_key = v8_str(scope, "query");
491
- db_conn_obj.set(scope, query_key.into(), query_fn.into());
492
-
493
- retval.set(db_conn_obj.into());
494
- }
495
-
496
- fn native_db_query(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
497
- // Get 'this' context (the db connection object)
498
- let this = args.this();
499
- let this_obj = this.to_object(scope).unwrap();
500
-
501
- // Retrieve connection string
502
- let conn_key = v8_str(scope, "__conn_string");
503
- let conn_val = this_obj.get(scope, conn_key.into()).unwrap();
504
- let conn_string = v8_to_string(scope, conn_val);
505
-
506
- // Get query string
507
- let query = v8_to_string(scope, args.get(0));
508
-
509
- if query.is_empty() {
510
- throw(scope, "db.query(): SQL query is required");
511
- return;
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());
519
-
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());
523
-
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
- }
673
- }
674
- };
675
-
676
- let runtime_ptr = unsafe { args.get_isolate() }.get_data(0) as *mut super::TitanRuntime;
677
- let runtime = unsafe { &mut *runtime_ptr };
678
-
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);
787
- }
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);
801
-
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
- }
812
- } else {
813
- serde_json::json!({ "error": format!("File not found: {}", path) })
814
- }
815
- },
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
876
- }
877
- }
1
+ //! Built-in V8 extensions and native bindings.
2
+ //!
3
+ //! Includes:
4
+ //! - Native API bindings (t.read, t.log, etc.)
5
+ //! - JWT utilities
6
+ //! - Password hashing
7
+ //! - Database connection pool
8
+ //! - Shared context
9
+
10
+ use v8;
11
+ use reqwest::{
12
+ blocking::Client,
13
+ header::{HeaderMap, HeaderName, HeaderValue},
14
+ };
15
+ use std::path::PathBuf;
16
+ use std::time::{SystemTime, UNIX_EPOCH};
17
+ use serde_json::Value;
18
+ use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
19
+ use bcrypt::{hash, verify, DEFAULT_COST};
20
+ use std::sync::OnceLock;
21
+ use deadpool_postgres::{Manager, Pool};
22
+ use tokio_postgres::{NoTls, Config};
23
+
24
+
25
+ use crate::utils::{blue, gray, red, parse_expires_in};
26
+ use super::{TitanRuntime, v8_str, v8_to_string, throw, ShareContextStore};
27
+
28
+ const TITAN_CORE_JS: &str = include_str!("titan_core.js");
29
+
30
+ // Database connection pool
31
+ static DB_POOL: OnceLock<Pool> = OnceLock::new();
32
+ static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
33
+
34
+ fn get_http_client() -> &'static reqwest::Client {
35
+ HTTP_CLIENT.get_or_init(|| {
36
+ reqwest::Client::builder()
37
+ .use_rustls_tls()
38
+ .tcp_nodelay(true)
39
+ .user_agent("TitanPL/1.0")
40
+ .build()
41
+ .unwrap_or_else(|_| reqwest::Client::new())
42
+ })
43
+ }
44
+
45
+
46
+ pub fn inject_builtin_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>, t_obj: v8::Local<v8::Object>) {
47
+ // 1. Native API Bindings
48
+
49
+ // defineAction (Native side)
50
+ let def_fn = v8::Function::new(scope, native_define_action).unwrap();
51
+ let def_key = v8_str(scope, "defineAction");
52
+ global.set(scope, def_key.into(), def_fn.into());
53
+
54
+
55
+ // t.read
56
+ let read_fn = v8::Function::new(scope, native_read).unwrap();
57
+ let read_key = v8_str(scope, "read");
58
+ t_obj.set(scope, read_key.into(), read_fn.into());
59
+
60
+ // t.decodeUtf8
61
+ let dec_fn = v8::Function::new(scope, native_decode_utf8).unwrap();
62
+ let dec_key = v8_str(scope, "decodeUtf8");
63
+ t_obj.set(scope, dec_key.into(), dec_fn.into());
64
+
65
+ // t.log
66
+ let log_fn = v8::Function::new(scope, native_log).unwrap();
67
+ let log_key = v8_str(scope, "log");
68
+ t_obj.set(scope, log_key.into(), log_fn.into());
69
+
70
+ // t.fetch (Metadata version for drift)
71
+ let fetch_fn = v8::Function::new(scope, native_fetch_meta).unwrap();
72
+ let fetch_key = v8_str(scope, "fetch");
73
+ t_obj.set(scope, fetch_key.into(), fetch_fn.into());
74
+
75
+ // t._drift_call
76
+ let drift_fn = v8::Function::new(scope, native_drift_call).unwrap();
77
+ let drift_key = v8_str(scope, "_drift_call");
78
+ t_obj.set(scope, drift_key.into(), drift_fn.into());
79
+
80
+ // t._finish_request
81
+ let finish_fn = v8::Function::new(scope, native_finish_request).unwrap();
82
+ let finish_key = v8_str(scope, "_finish_request");
83
+ t_obj.set(scope, finish_key.into(), finish_fn.into());
84
+
85
+ // t.loadEnv
86
+ let env_fn = v8::Function::new(scope, native_load_env).unwrap();
87
+ let env_key = v8_str(scope, "loadEnv");
88
+ t_obj.set(scope, env_key.into(), env_fn.into());
89
+
90
+ // auth, jwt, password, db, core ... (setup native objects BEFORE JS injection)
91
+ setup_native_utils(scope, t_obj);
92
+
93
+ // 2. JS Side Injection (Embedded)
94
+ let tc = &mut v8::TryCatch::new(scope);
95
+ let source = v8_str(tc, TITAN_CORE_JS);
96
+ if let Some(script) = v8::Script::compile(tc, source, None) {
97
+ if script.run(tc).is_none() {
98
+ let msg = tc.message().map(|m| m.get(tc).to_rust_string_lossy(tc)).unwrap_or("Unknown".to_string());
99
+ println!("{} {} {}", blue("[Titan]"), red("Core JS Init Failed:"), msg);
100
+ }
101
+ } else {
102
+ println!("{} {}", blue("[Titan]"), red("Core JS Compilation Failed"));
103
+ }
104
+ }
105
+
106
+ fn setup_native_utils(scope: &mut v8::HandleScope, t_obj: v8::Local<v8::Object>) {
107
+ // t.jwt
108
+ let jwt_obj = v8::Object::new(scope);
109
+ let sign_fn = v8::Function::new(scope, native_jwt_sign).unwrap();
110
+ let verify_fn = v8::Function::new(scope, native_jwt_verify).unwrap();
111
+
112
+ let sign_key = v8_str(scope, "sign");
113
+ jwt_obj.set(scope, sign_key.into(), sign_fn.into());
114
+ let verify_key = v8_str(scope, "verify");
115
+ jwt_obj.set(scope, verify_key.into(), verify_fn.into());
116
+
117
+ let jwt_key = v8_str(scope, "jwt");
118
+ t_obj.set(scope, jwt_key.into(), jwt_obj.into());
119
+
120
+ // t.password
121
+ let pw_obj = v8::Object::new(scope);
122
+ let hash_fn = v8::Function::new(scope, native_password_hash).unwrap();
123
+ let pw_verify_fn = v8::Function::new(scope, native_password_verify).unwrap();
124
+
125
+ let hash_key = v8_str(scope, "hash");
126
+ pw_obj.set(scope, hash_key.into(), hash_fn.into());
127
+ let pw_v_key = v8_str(scope, "verify");
128
+ pw_obj.set(scope, pw_v_key.into(), pw_verify_fn.into());
129
+
130
+ let pw_key = v8_str(scope, "password");
131
+ t_obj.set(scope, pw_key.into(), pw_obj.into());
132
+
133
+ // t.shareContext (Native primitives)
134
+ let sc_obj = v8::Object::new(scope);
135
+ let n_get = v8::Function::new(scope, share_context_get).unwrap();
136
+ let n_set = v8::Function::new(scope, share_context_set).unwrap();
137
+ let n_del = v8::Function::new(scope, share_context_delete).unwrap();
138
+ let n_keys = v8::Function::new(scope, share_context_keys).unwrap();
139
+ let n_pub = v8::Function::new(scope, share_context_broadcast).unwrap();
140
+
141
+ let get_key = v8_str(scope, "get");
142
+ sc_obj.set(scope, get_key.into(), n_get.into());
143
+ let set_key = v8_str(scope, "set");
144
+ sc_obj.set(scope, set_key.into(), n_set.into());
145
+ let del_key = v8_str(scope, "delete");
146
+ sc_obj.set(scope, del_key.into(), n_del.into());
147
+ let keys_key = v8_str(scope, "keys");
148
+ sc_obj.set(scope, keys_key.into(), n_keys.into());
149
+ let pub_key = v8_str(scope, "broadcast");
150
+ sc_obj.set(scope, pub_key.into(), n_pub.into());
151
+
152
+ let sc_key = v8_str(scope, "shareContext");
153
+ let sc_val = sc_obj.into();
154
+ t_obj.set(scope, sc_key.into(), sc_val);
155
+
156
+ // t.db (Database operations)
157
+ let db_obj = v8::Object::new(scope);
158
+ let db_connect_fn = v8::Function::new(scope, native_db_connect).unwrap();
159
+ let connect_key = v8_str(scope, "connect");
160
+ db_obj.set(scope, connect_key.into(), db_connect_fn.into());
161
+
162
+ let db_key = v8_str(scope, "db");
163
+ t_obj.set(scope, db_key.into(), db_obj.into());
164
+
165
+ // t.core (System operations)
166
+ let core_obj = v8::Object::new(scope);
167
+ let fs_obj = v8::Object::new(scope);
168
+ let fs_read_fn = v8::Function::new(scope, native_read).unwrap();
169
+ let read_key = v8_str(scope, "read");
170
+ fs_obj.set(scope, read_key.into(), fs_read_fn.into());
171
+
172
+ let fs_read_sync_fn = v8::Function::new(scope, native_read_sync).unwrap();
173
+ let read_sync_key = v8_str(scope, "readFile");
174
+ fs_obj.set(scope, read_sync_key.into(), fs_read_sync_fn.into());
175
+
176
+ // Also Expose as t.readSync
177
+ let t_read_sync_fn = v8::Function::new(scope, native_read_sync).unwrap();
178
+ let t_read_sync_key = v8_str(scope, "readSync");
179
+ t_obj.set(scope, t_read_sync_key.into(), t_read_sync_fn.into());
180
+
181
+ let fs_key = v8_str(scope, "fs");
182
+ core_obj.set(scope, fs_key.into(), fs_obj.into());
183
+
184
+
185
+ }
186
+
187
+ fn native_read_sync(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
188
+ let path_val = args.get(0);
189
+ if !path_val.is_string() {
190
+ throw(scope, "readSync/readFile: path is required");
191
+ return;
192
+ }
193
+ let path_str = v8_to_string(scope, path_val);
194
+
195
+ let root = super::PROJECT_ROOT.get().cloned().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
196
+ let joined = root.join(&path_str);
197
+
198
+ if let Ok(target) = joined.canonicalize() {
199
+ if target.starts_with(&root.canonicalize().unwrap_or(root.clone())) {
200
+ match std::fs::read_to_string(&target) {
201
+ Ok(content) => {
202
+ let v8_content = v8_str(scope, &content);
203
+ retval.set(v8_content.into());
204
+ },
205
+ Err(e) => {
206
+ retval.set(v8::null(scope).into());
207
+ }
208
+ }
209
+ } else {
210
+ retval.set(v8::null(scope).into());
211
+ }
212
+ } else {
213
+ retval.set(v8::null(scope).into());
214
+ }
215
+ }
216
+
217
+ fn native_read(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
218
+ let path_val = args.get(0);
219
+ if !path_val.is_string() {
220
+ throw(scope, "t.read(path): path is required");
221
+ return;
222
+ }
223
+ let path_str = v8_to_string(scope, path_val);
224
+
225
+ let obj = v8::Object::new(scope);
226
+ let op_key = v8_str(scope, "__titanAsync");
227
+ let op_val = v8::Boolean::new(scope, true);
228
+ obj.set(scope, op_key.into(), op_val.into());
229
+
230
+ let type_key = v8_str(scope, "type");
231
+ let type_val = v8_str(scope, "fs_read");
232
+ obj.set(scope, type_key.into(), type_val.into());
233
+
234
+ let data_obj = v8::Object::new(scope);
235
+ let path_k = v8_str(scope, "path");
236
+ let path_v = v8_str(scope, &path_str);
237
+ data_obj.set(scope, path_k.into(), path_v.into());
238
+
239
+ let data_key = v8_str(scope, "data");
240
+ obj.set(scope, data_key.into(), data_obj.into());
241
+
242
+ retval.set(obj.into());
243
+ }
244
+
245
+ fn native_decode_utf8(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
246
+ let val = args.get(0);
247
+ if let Ok(u8arr) = v8::Local::<v8::Uint8Array>::try_from(val) {
248
+ let buf = u8arr.buffer(scope).unwrap();
249
+ let store = v8::ArrayBuffer::get_backing_store(&buf);
250
+ let offset = usize::from(u8arr.byte_offset());
251
+ let length = usize::from(u8arr.byte_length());
252
+ let slice = &store[offset..offset+length];
253
+
254
+ let bytes: Vec<u8> = slice.iter().map(|b| b.get()).collect();
255
+ let s = String::from_utf8_lossy(&bytes);
256
+ retval.set(v8_str(scope, &s).into());
257
+ } else if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(val) {
258
+ let store = v8::ArrayBuffer::get_backing_store(&ab);
259
+ let bytes: Vec<u8> = store.iter().map(|b| b.get()).collect();
260
+ let s = String::from_utf8_lossy(&bytes);
261
+ retval.set(v8_str(scope, &s).into());
262
+ } else {
263
+ retval.set(v8::null(scope).into());
264
+ }
265
+ }
266
+
267
+ fn share_context_get(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
268
+ let key = v8_to_string(scope, args.get(0));
269
+ let store = ShareContextStore::get();
270
+ if let Some(val) = store.kv.get(&key) {
271
+ let json_str = val.to_string();
272
+ let v8_str = v8::String::new(scope, &json_str).unwrap();
273
+ if let Some(v8_val) = v8::json::parse(scope, v8_str) {
274
+ retval.set(v8_val);
275
+ return;
276
+ }
277
+ }
278
+ retval.set(v8::null(scope).into());
279
+ }
280
+
281
+ fn share_context_set(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
282
+ let key = v8_to_string(scope, args.get(0));
283
+ let val_v8 = args.get(1);
284
+
285
+ if let Some(json_v8) = v8::json::stringify(scope, val_v8) {
286
+ let json_str = json_v8.to_rust_string_lossy(scope);
287
+ if let Ok(val) = serde_json::from_str(&json_str) {
288
+ ShareContextStore::get().kv.insert(key, val);
289
+ }
290
+ }
291
+ }
292
+
293
+ fn share_context_delete(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
294
+ let key = v8_to_string(scope, args.get(0));
295
+ ShareContextStore::get().kv.remove(&key);
296
+ }
297
+
298
+ fn share_context_keys(scope: &mut v8::HandleScope, _args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
299
+ let store = ShareContextStore::get();
300
+ let keys: Vec<v8::Local<v8::Value>> = store.kv.iter().map(|kv| v8_str(scope, kv.key()).into()).collect();
301
+ let arr = v8::Array::new_with_elements(scope, &keys);
302
+ retval.set(arr.into());
303
+ }
304
+
305
+ fn share_context_broadcast(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
306
+ let event = v8_to_string(scope, args.get(0));
307
+ let payload_v8 = args.get(1);
308
+
309
+ if let Some(json_v8) = v8::json::stringify(scope, payload_v8) {
310
+ let json_str = json_v8.to_rust_string_lossy(scope);
311
+ if let Ok(payload) = serde_json::from_str(&json_str) {
312
+ let _ = ShareContextStore::get().broadcast_tx.send((event, payload));
313
+ }
314
+ }
315
+ }
316
+
317
+
318
+
319
+ fn native_log(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue) {
320
+ let context = scope.get_current_context();
321
+ let global = context.global(scope);
322
+ let action_key = v8_str(scope, "__titan_action");
323
+ let action_name = if let Some(action_val) = global.get(scope, action_key.into()) {
324
+ if action_val.is_string() {
325
+ v8_to_string(scope, action_val)
326
+ } else {
327
+ "init".to_string()
328
+ }
329
+ } else {
330
+ "init".to_string()
331
+ };
332
+
333
+ let mut parts = Vec::new();
334
+ for i in 0..args.length() {
335
+ let val = args.get(i);
336
+ let mut appended = false;
337
+
338
+ if val.is_object() && !val.is_function() {
339
+ if let Some(json) = v8::json::stringify(scope, val) {
340
+ parts.push(json.to_rust_string_lossy(scope));
341
+ appended = true;
342
+ }
343
+ }
344
+
345
+ if !appended {
346
+ parts.push(v8_to_string(scope, val));
347
+ }
348
+ }
349
+
350
+ let titan_str = blue("[Titan]");
351
+ let log_msg = gray(&format!("\x1b[90mlog({})\x1b[0m\x1b[97m: {}\x1b[0m", action_name, parts.join(" ")));
352
+ println!(
353
+ "{} {}",
354
+ titan_str,
355
+ log_msg
356
+ );
357
+ }
358
+
359
+
360
+
361
+ fn native_jwt_sign(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
362
+ let payload_val = args.get(0);
363
+ let json_str = v8::json::stringify(scope, payload_val).unwrap().to_rust_string_lossy(scope);
364
+ let mut payload: serde_json::Map<String, Value> = serde_json::from_str(&json_str).unwrap_or_default();
365
+ let secret = v8_to_string(scope, args.get(1));
366
+
367
+ let opts_val = args.get(2);
368
+ if opts_val.is_object() {
369
+ let opts_obj = opts_val.to_object(scope).unwrap();
370
+ let exp_key = v8_str(scope, "expiresIn");
371
+ if let Some(val) = opts_obj.get(scope, exp_key.into()) {
372
+ let seconds = if val.is_number() {
373
+ Some(val.to_number(scope).unwrap().value() as u64)
374
+ } else if val.is_string() {
375
+ parse_expires_in(&v8_to_string(scope, val))
376
+ } else { None };
377
+ if let Some(sec) = seconds {
378
+ let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
379
+ payload.insert("exp".to_string(), Value::Number(serde_json::Number::from(now + sec)));
380
+ }
381
+ }
382
+ }
383
+
384
+ let token = encode(&Header::default(), &Value::Object(payload), &EncodingKey::from_secret(secret.as_bytes()));
385
+ match token {
386
+ Ok(t) => {
387
+ let res = v8_str(scope, &t);
388
+ retval.set(res.into());
389
+ },
390
+ Err(e) => throw(scope, &e.to_string()),
391
+ }
392
+ }
393
+
394
+ fn native_jwt_verify(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
395
+ let token = v8_to_string(scope, args.get(0));
396
+ let secret = v8_to_string(scope, args.get(1));
397
+ let mut validation = Validation::default();
398
+ validation.validate_exp = true;
399
+ let data = decode::<Value>(&token, &DecodingKey::from_secret(secret.as_bytes()), &validation);
400
+ match data {
401
+ Ok(d) => {
402
+ let json_str = serde_json::to_string(&d.claims).unwrap();
403
+ let v8_json_str = v8_str(scope, &json_str);
404
+ if let Some(val) = v8::json::parse(scope, v8_json_str) {
405
+ retval.set(val);
406
+ }
407
+ },
408
+ Err(e) => throw(scope, &format!("Invalid or expired JWT: {}", e)),
409
+ }
410
+ }
411
+
412
+ fn native_password_hash(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
413
+ let pw = v8_to_string(scope, args.get(0));
414
+ match hash(pw, DEFAULT_COST) {
415
+ Ok(h) => {
416
+ let res = v8_str(scope, &h);
417
+ retval.set(res.into());
418
+ },
419
+ Err(e) => throw(scope, &e.to_string()),
420
+ }
421
+ }
422
+
423
+ fn native_password_verify(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
424
+ let pw = v8_to_string(scope, args.get(0));
425
+ let hash_str = v8_to_string(scope, args.get(1));
426
+ let ok = verify(pw, &hash_str).unwrap_or(false);
427
+ retval.set(v8::Boolean::new(scope, ok).into());
428
+ }
429
+
430
+ fn native_load_env(scope: &mut v8::HandleScope, _args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
431
+ use serde_json::json;
432
+
433
+ let mut map = serde_json::Map::new();
434
+
435
+ for (key, value) in std::env::vars() {
436
+ map.insert(key, json!(value));
437
+ }
438
+
439
+ let json_str = serde_json::to_string(&map).unwrap();
440
+ let v8_str = v8::String::new(scope, &json_str).unwrap();
441
+
442
+ if let Some(obj) = v8::json::parse(scope, v8_str) {
443
+ retval.set(obj);
444
+ } else {
445
+ retval.set(v8::null(scope).into());
446
+ }
447
+ }
448
+
449
+ fn native_define_action(_scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
450
+ retval.set(args.get(0));
451
+ }
452
+
453
+ fn native_db_connect(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
454
+
455
+ let conn_string = v8_to_string(scope, args.get(0));
456
+
457
+ if conn_string.is_empty() {
458
+ throw(scope, "t.db.connect(): connection string required");
459
+ return;
460
+ }
461
+
462
+ let mut max_size = 16;
463
+
464
+ if args.length() > 1 && args.get(1).is_object() {
465
+ let opts = args.get(1).to_object(scope).unwrap();
466
+ let max_key = v8_str(scope, "max");
467
+ if let Some(v) = opts.get(scope, max_key.into()) {
468
+ if let Some(n) = v.number_value(scope) {
469
+ max_size = n as usize;
470
+ }
471
+ }
472
+ }
473
+
474
+ if DB_POOL.get().is_none() {
475
+ let cfg: Config = conn_string.parse().unwrap();
476
+ let mgr = Manager::new(cfg, NoTls);
477
+
478
+ let pool = Pool::builder(mgr)
479
+ .max_size(max_size)
480
+ .build()
481
+ .unwrap();
482
+
483
+ DB_POOL.set(pool).ok();
484
+ }
485
+
486
+ let db_conn_obj = v8::Object::new(scope);
487
+
488
+ let query_fn = v8::Function::new(scope, native_db_query).unwrap();
489
+ let query_key = v8_str(scope, "query");
490
+ db_conn_obj.set(scope, query_key.into(), query_fn.into());
491
+
492
+ retval.set(db_conn_obj.into());
493
+ }
494
+
495
+ fn native_db_query(
496
+ scope: &mut v8::HandleScope,
497
+ args: v8::FunctionCallbackArguments,
498
+ mut retval: v8::ReturnValue,
499
+ ) {
500
+ let sql = v8_to_string(scope, args.get(0));
501
+
502
+ // Collect params
503
+ let mut params = Vec::new();
504
+ if args.length() > 1 && args.get(1).is_array() {
505
+ let arr = v8::Local::<v8::Array>::try_from(args.get(1)).unwrap();
506
+ for i in 0..arr.length() {
507
+ if let Some(v) = arr.get_index(scope, i) {
508
+ params.push(v8_to_string(scope, v));
509
+ }
510
+ }
511
+ }
512
+
513
+ // Main async wrapper object
514
+ let obj = v8::Object::new(scope);
515
+
516
+ let async_key = v8_str(scope, "__titanAsync");
517
+ let async_val = v8::Boolean::new(scope, true);
518
+ obj.set(scope, async_key.into(), async_val.into());
519
+
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());
523
+
524
+ // Data object
525
+ let data_obj = v8::Object::new(scope);
526
+
527
+ let conn_key = v8_str(scope, "conn");
528
+ let conn_val = v8_str(scope, "default");
529
+ data_obj.set(scope, conn_key.into(), conn_val.into());
530
+
531
+ let query_key = v8_str(scope, "query");
532
+ let query_val = v8_str(scope, &sql);
533
+ data_obj.set(scope, query_key.into(), query_val.into());
534
+
535
+ // Params array
536
+ let params_arr = v8::Array::new(scope, params.len() as i32);
537
+
538
+ for (i, p) in params.iter().enumerate() {
539
+ let param_val = v8_str(scope, p);
540
+ params_arr.set_index(scope, i as u32, param_val.into());
541
+ }
542
+
543
+ let params_key = v8_str(scope, "params");
544
+ data_obj.set(scope, params_key.into(), params_arr.into());
545
+
546
+ let data_key = v8_str(scope, "data");
547
+ obj.set(scope, data_key.into(), data_obj.into());
548
+
549
+ retval.set(obj.into());
550
+ }
551
+
552
+ fn native_fetch_meta(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
553
+ let url = v8_to_string(scope, args.get(0));
554
+ let opts = args.get(1);
555
+
556
+ let obj = v8::Object::new(scope);
557
+ let op_key = v8_str(scope, "__titanAsync");
558
+ let op_val = v8::Boolean::new(scope, true);
559
+ obj.set(scope, op_key.into(), op_val.into());
560
+
561
+ let type_key = v8_str(scope, "type");
562
+ let type_val = v8_str(scope, "fetch");
563
+ obj.set(scope, type_key.into(), type_val.into());
564
+
565
+ let data_obj = v8::Object::new(scope);
566
+ let url_key = v8_str(scope, "url");
567
+ let url_val = v8_str(scope, &url);
568
+ data_obj.set(scope, url_key.into(), url_val.into());
569
+
570
+ let opts_key = v8_str(scope, "opts");
571
+ data_obj.set(scope, opts_key.into(), opts);
572
+
573
+ let data_key = v8_str(scope, "data");
574
+ obj.set(scope, data_key.into(), data_obj.into());
575
+
576
+ retval.set(obj.into());
577
+ }
578
+
579
+ fn parse_async_op(scope: &mut v8::HandleScope, op_val: v8::Local<v8::Value>) -> Option<super::TitanAsyncOp> {
580
+ if !op_val.is_object() { return None; }
581
+ let op_obj = op_val.to_object(scope).unwrap();
582
+
583
+ let type_key = v8_str(scope, "type");
584
+ let type_obj = op_obj.get(scope, type_key.into())?;
585
+ let op_type = v8_to_string(scope, type_obj);
586
+
587
+ let data_key = v8_str(scope, "data");
588
+ let data_val = op_obj.get(scope, data_key.into())?;
589
+ if !data_val.is_object() { return None; }
590
+ let data_obj = data_val.to_object(scope).unwrap();
591
+
592
+ match op_type.as_str() {
593
+ "fetch" => {
594
+ let url_key = v8_str(scope, "url");
595
+ let url_obj = data_obj.get(scope, url_key.into())?;
596
+ let url = v8_to_string(scope, url_obj);
597
+
598
+ let mut method = "GET".to_string();
599
+ let mut body = None;
600
+ let mut headers = Vec::new();
601
+
602
+ let opts_key = v8_str(scope, "opts");
603
+ if let Some(opts_val) = data_obj.get(scope, opts_key.into()) {
604
+ if opts_val.is_object() {
605
+ let opts_obj = opts_val.to_object(scope).unwrap();
606
+ let m_key = v8_str(scope, "method");
607
+ if let Some(m_val) = opts_obj.get(scope, m_key.into()) {
608
+ if m_val.is_string() { method = v8_to_string(scope, m_val); }
609
+ }
610
+ let b_key = v8_str(scope, "body");
611
+ if let Some(b_val) = opts_obj.get(scope, b_key.into()) {
612
+ if b_val.is_string() {
613
+ body = Some(v8_to_string(scope, b_val));
614
+ } else if b_val.is_object() {
615
+ body = Some(v8::json::stringify(scope, b_val).unwrap().to_rust_string_lossy(scope));
616
+ }
617
+ }
618
+ let h_key = v8_str(scope, "headers");
619
+ if let Some(h_val) = opts_obj.get(scope, h_key.into()) {
620
+ if h_val.is_object() {
621
+ let h_obj = h_val.to_object(scope).unwrap();
622
+ if let Some(keys) = h_obj.get_own_property_names(scope, Default::default()) {
623
+ for i in 0..keys.length() {
624
+ let key = keys.get_index(scope, i).unwrap();
625
+ let val = h_obj.get(scope, key).unwrap();
626
+ headers.push((v8_to_string(scope, key), v8_to_string(scope, val)));
627
+ }
628
+ }
629
+ }
630
+ }
631
+ }
632
+ }
633
+ Some(super::TitanAsyncOp::Fetch { url, method, body, headers })
634
+ },
635
+
636
+ "db_query" => {
637
+
638
+ let conn_key = v8_str(scope, "conn");
639
+ let conn_val = data_obj.get(scope, conn_key.into())?;
640
+ let conn = v8_to_string(scope, conn_val);
641
+
642
+ let query_key = v8_str(scope, "query");
643
+ let query_val = data_obj.get(scope, query_key.into())?;
644
+ let query = v8_to_string(scope, query_val);
645
+
646
+ let params_key = v8_str(scope, "params");
647
+ let mut params = Vec::new();
648
+
649
+ if let Some(p_val) = data_obj.get(scope, params_key.into()) {
650
+ if p_val.is_array() {
651
+ let arr = v8::Local::<v8::Array>::try_from(p_val).unwrap();
652
+ for i in 0..arr.length() {
653
+ if let Some(v) = arr.get_index(scope, i) {
654
+ params.push(v8_to_string(scope, v));
655
+ }
656
+ }
657
+ }
658
+ }
659
+
660
+ Some(super::TitanAsyncOp::DbQuery { conn, query, params })
661
+ }
662
+
663
+
664
+ "fs_read" => {
665
+ let path_key = v8_str(scope, "path");
666
+ let path_obj = data_obj.get(scope, path_key.into())?;
667
+ let path = v8_to_string(scope, path_obj);
668
+ Some(super::TitanAsyncOp::FsRead { path })
669
+ },
670
+ _ => None
671
+ }
672
+ }
673
+
674
+ fn native_drift_call(scope: &mut v8::HandleScope, mut args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
675
+ let runtime_ptr = unsafe { args.get_isolate() }.get_data(0) as *mut super::TitanRuntime;
676
+ let runtime = unsafe { &mut *runtime_ptr };
677
+
678
+ let arg0 = args.get(0);
679
+
680
+ let (async_op, op_type) = if arg0.is_array() {
681
+ let arr = v8::Local::<v8::Array>::try_from(arg0).unwrap();
682
+ let mut ops = Vec::new();
683
+ for i in 0..arr.length() {
684
+ let op_val = arr.get_index(scope, i).unwrap();
685
+ if let Some(op) = parse_async_op(scope, op_val) {
686
+ ops.push(op);
687
+ }
688
+ }
689
+ (super::TitanAsyncOp::Batch(ops), "batch".to_string())
690
+ } else {
691
+ match parse_async_op(scope, arg0) {
692
+ Some(op) => {
693
+ let t = match &op {
694
+ super::TitanAsyncOp::Fetch { .. } => "fetch",
695
+ super::TitanAsyncOp::DbQuery { .. } => "db_query",
696
+ super::TitanAsyncOp::FsRead { .. } => "fs_read",
697
+ _ => "unknown"
698
+ };
699
+ (op, t.to_string())
700
+ },
701
+ None => {
702
+ throw(scope, "drift() requires an async operation or array of operations");
703
+ return;
704
+ }
705
+ }
706
+ };
707
+
708
+ let runtime_ptr = unsafe { args.get_isolate() }.get_data(0) as *mut super::TitanRuntime;
709
+ let runtime = unsafe { &mut *runtime_ptr };
710
+
711
+ let req_id = {
712
+ let context = scope.get_current_context();
713
+ let global = context.global(scope);
714
+ let req_key = v8_str(scope, "__titan_req");
715
+ if let Some(req_obj_val) = global.get(scope, req_key.into()) {
716
+ if req_obj_val.is_object() {
717
+ let req_obj = req_obj_val.to_object(scope).unwrap();
718
+ let id_key = v8_str(scope, "__titan_request_id");
719
+ req_obj.get(scope, id_key.into()).unwrap().uint32_value(scope).unwrap_or(0)
720
+ } else { 0 }
721
+ } else { 0 }
722
+ };
723
+
724
+ runtime.drift_counter += 1;
725
+ let drift_id = runtime.drift_counter;
726
+
727
+ if req_id != 0 {
728
+ runtime.drift_to_request.insert(drift_id, req_id);
729
+ }
730
+
731
+ // --- REPLAY CHECK ---
732
+ if let Some(res) = runtime.completed_drifts.get(&drift_id) {
733
+ let json_str = serde_json::to_string(res).unwrap_or_else(|_| "null".to_string());
734
+ let v8_str = v8::String::new(scope, &json_str).unwrap();
735
+ let mut try_catch = v8::TryCatch::new(scope);
736
+ if let Some(val) = v8::json::parse(&mut try_catch, v8_str) {
737
+ retval.set(val);
738
+ } else {
739
+ retval.set(v8::null(&mut try_catch).into());
740
+ }
741
+ return;
742
+ }
743
+
744
+ let (tx, rx) = tokio::sync::oneshot::channel::<super::WorkerAsyncResult>();
745
+
746
+ let req = super::AsyncOpRequest {
747
+ op: async_op,
748
+ drift_id,
749
+ request_id: req_id,
750
+ op_type,
751
+ respond_tx: tx,
752
+ };
753
+
754
+ if let Err(e) = runtime.global_async_tx.try_send(req) {
755
+ println!("[Titan] Drift Call Failed to queue: {}", e);
756
+ retval.set(v8::null(scope).into());
757
+ return;
758
+ }
759
+
760
+ let tokio_handle = runtime.tokio_handle.clone();
761
+ let worker_tx = runtime.worker_tx.clone();
762
+
763
+ tokio_handle.spawn(async move {
764
+ if let Ok(res) = rx.await {
765
+ let _ = worker_tx.send(crate::runtime::WorkerCommand::Resume {
766
+ drift_id,
767
+ result: res,
768
+ });
769
+ }
770
+ });
771
+
772
+ throw(scope, "__SUSPEND__");
773
+ }
774
+
775
+ fn native_finish_request(scope: &mut v8::HandleScope, mut args: v8::FunctionCallbackArguments, _retval: v8::ReturnValue) {
776
+ let request_id = args.get(0).uint32_value(scope).unwrap_or(0);
777
+ let result_val = args.get(1);
778
+
779
+ // --- OPTIMIZATION: Direct field extraction for _isResponse objects ---
780
+ let json = if result_val.is_object() {
781
+ let obj = result_val.to_object(scope).unwrap();
782
+ let is_resp_key = v8_str(scope, "_isResponse");
783
+ let is_response = obj
784
+ .get(scope, is_resp_key.into())
785
+ .map(|v| v.boolean_value(scope))
786
+ .unwrap_or(false);
787
+
788
+ if is_response {
789
+ // Hot path: extract fields directly without full stringify+parse.
790
+ let mut map = serde_json::Map::with_capacity(5);
791
+ map.insert("_isResponse".into(), Value::Bool(true));
792
+
793
+ // status (number u64)
794
+ let status_key = v8_str(scope, "status");
795
+ if let Some(s) = obj.get(scope, status_key.into()) {
796
+ if let Some(n) = s.number_value(scope) {
797
+ map.insert(
798
+ "status".into(),
799
+ Value::Number(serde_json::Number::from(n as u64)),
800
+ );
801
+ }
802
+ }
803
+
804
+ // body (already a JSON string from JS — extract as-is, no re-serialization)
805
+ let body_key = v8_str(scope, "body");
806
+ if let Some(b) = obj.get(scope, body_key.into()) {
807
+ if b.is_string() {
808
+ let body_str = b.to_string(scope).unwrap().to_rust_string_lossy(scope);
809
+ map.insert("body".into(), Value::String(body_str));
810
+ } else if !b.is_null_or_undefined() {
811
+ // Non-string body (rare) — stringify it
812
+ let body_str = v8_to_string(scope, b);
813
+ map.insert("body".into(), Value::String(body_str));
814
+ }
815
+ }
816
+
817
+ // headers (flat object with ~2-3 keys typically)
818
+ let headers_key = v8_str(scope, "headers");
819
+ if let Some(h) = obj.get(scope, headers_key.into()) {
820
+ if h.is_object() {
821
+ let h_obj = h.to_object(scope).unwrap();
822
+ if let Some(keys) =
823
+ h_obj.get_own_property_names(scope, Default::default())
824
+ {
825
+ let mut h_map = serde_json::Map::with_capacity(keys.length() as usize);
826
+ for i in 0..keys.length() {
827
+ if let Some(key) = keys.get_index(scope, i) {
828
+ if let Some(val) = h_obj.get(scope, key) {
829
+ let k_str =
830
+ key.to_string(scope).unwrap().to_rust_string_lossy(scope);
831
+ let v_str =
832
+ val.to_string(scope).unwrap().to_rust_string_lossy(scope);
833
+ h_map.insert(k_str, Value::String(v_str));
834
+ }
835
+ }
836
+ }
837
+ map.insert("headers".into(), Value::Object(h_map));
838
+ }
839
+ }
840
+ }
841
+ serde_json::Value::Object(map)
842
+ } else {
843
+ super::v8_to_json(scope, result_val)
844
+ }
845
+ } else {
846
+ super::v8_to_json(scope, result_val)
847
+ };
848
+
849
+ let runtime_ptr = unsafe { args.get_isolate() }.get_data(0) as *mut super::TitanRuntime;
850
+ let runtime = unsafe { &mut *runtime_ptr };
851
+
852
+ if let Some(tx) = runtime.pending_requests.remove(&request_id) {
853
+ let timings = runtime.request_timings.remove(&request_id).unwrap_or_default();
854
+ let _ = tx.send(crate::runtime::WorkerResult {
855
+ json,
856
+ timings
857
+ });
858
+ }
859
+ }
860
+
861
+ pub fn run_async_operation(
862
+ op: super::TitanAsyncOp,
863
+ ) -> std::pin::Pin<Box<dyn std::future::Future<Output = serde_json::Value> + Send>> {
864
+ Box::pin(async move {
865
+ match op {
866
+
867
+ // =========================
868
+ // FETCH
869
+ // =========================
870
+ super::TitanAsyncOp::Fetch {
871
+ url,
872
+ method,
873
+ body,
874
+ headers,
875
+ } => {
876
+ let client = get_http_client();
877
+
878
+ let method = reqwest::Method::from_bytes(method.as_bytes())
879
+ .unwrap_or(reqwest::Method::GET);
880
+
881
+ let mut req = client.request(method, &url);
882
+
883
+ for (k, v) in headers {
884
+ req = req.header(k, v);
885
+ }
886
+
887
+ if let Some(b) = body {
888
+ req = req.body(b);
889
+ }
890
+
891
+ match req.send().await {
892
+ Ok(resp) => {
893
+ let status = resp.status().as_u16();
894
+ let api_headers = resp.headers().clone();
895
+ let text = resp.text().await.unwrap_or_default();
896
+
897
+ let mut h_map = serde_json::Map::new();
898
+ for (k, v) in api_headers.iter() {
899
+ if let Ok(s) = v.to_str() {
900
+ h_map.insert(
901
+ k.as_str().to_string(),
902
+ serde_json::Value::String(s.to_string()),
903
+ );
904
+ }
905
+ }
906
+
907
+ serde_json::json!({
908
+ "_isResponse": true,
909
+ "status": status,
910
+ "body": text,
911
+ "headers": h_map
912
+ })
913
+ }
914
+ Err(e) => serde_json::json!({ "error": e.to_string() }),
915
+ }
916
+ }
917
+
918
+ // =========================
919
+ // DB QUERY
920
+ // =========================
921
+ super::TitanAsyncOp::DbQuery { conn: _, query, params } => {
922
+
923
+ let pool = match DB_POOL.get() {
924
+ Some(p) => p,
925
+ None => {
926
+ return serde_json::json!({
927
+ "error": "DB pool not initialized"
928
+ });
929
+ }
930
+ };
931
+
932
+ match pool.get().await {
933
+ Ok(client) => {
934
+
935
+ let stmt = match client.prepare(&query).await {
936
+ Ok(s) => s,
937
+ Err(e) => {
938
+ return serde_json::json!({
939
+ "error": e.to_string()
940
+ });
941
+ }
942
+ };
943
+
944
+ let param_refs: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> =
945
+ params.iter()
946
+ .map(|p| p as &(dyn tokio_postgres::types::ToSql + Sync))
947
+ .collect();
948
+
949
+ match client.query(&stmt, &param_refs).await {
950
+ Ok(rows) => {
951
+
952
+ let mut result = Vec::new();
953
+
954
+ for row in rows {
955
+ let mut obj = serde_json::Map::new();
956
+
957
+ for (i, col) in row.columns().iter().enumerate() {
958
+
959
+ let val =
960
+ if let Ok(v) = row.try_get::<_, String>(i) {
961
+ serde_json::Value::String(v)
962
+ } else if let Ok(v) = row.try_get::<_, i64>(i) {
963
+ serde_json::Value::Number(v.into())
964
+ } else if let Ok(v) = row.try_get::<_, i32>(i) {
965
+ serde_json::Value::Number(v.into())
966
+ } else if let Ok(v) = row.try_get::<_, bool>(i) {
967
+ serde_json::Value::Bool(v)
968
+ } else {
969
+ serde_json::Value::Null
970
+ };
971
+
972
+ obj.insert(col.name().to_string(), val);
973
+ }
974
+
975
+ result.push(serde_json::Value::Object(obj));
976
+ }
977
+
978
+ serde_json::Value::Array(result)
979
+ }
980
+ Err(e) => serde_json::json!({
981
+ "error": e.to_string()
982
+ }),
983
+ }
984
+ }
985
+ Err(e) => serde_json::json!({
986
+ "error": e.to_string()
987
+ }),
988
+ }
989
+ }
990
+
991
+ // =========================
992
+ // FS READ
993
+ // =========================
994
+ super::TitanAsyncOp::FsRead { path } => {
995
+
996
+ let root = super::PROJECT_ROOT
997
+ .get()
998
+ .cloned()
999
+ .unwrap_or(std::path::PathBuf::from("."));
1000
+
1001
+ let target = root.join(&path);
1002
+
1003
+ let safe = target
1004
+ .canonicalize()
1005
+ .map(|p| {
1006
+ p.starts_with(
1007
+ root.canonicalize()
1008
+ .unwrap_or(root.clone())
1009
+ )
1010
+ })
1011
+ .unwrap_or(false);
1012
+
1013
+ if safe {
1014
+ match tokio::fs::read_to_string(target).await {
1015
+ Ok(c) => serde_json::json!({ "data": c }),
1016
+ Err(e) => serde_json::json!({ "error": e.to_string() }),
1017
+ }
1018
+ } else {
1019
+ serde_json::json!({ "error": "Access denied" })
1020
+ }
1021
+ }
1022
+
1023
+ // =========================
1024
+ // BATCH
1025
+ // =========================
1026
+ super::TitanAsyncOp::Batch(ops) => {
1027
+
1028
+ let mut res = Vec::new();
1029
+
1030
+ for op in ops {
1031
+ res.push(run_async_operation(op).await);
1032
+ }
1033
+
1034
+ serde_json::Value::Array(res)
1035
+ }
1036
+ }
1037
+ })
1038
+ }