@t8n/ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -0
- package/index.d.ts +65 -0
- package/index.js +131 -0
- package/jsconfig.json +13 -0
- package/package.json +32 -0
- package/test-app/.dockerignore +3 -0
- package/test-app/Dockerfile +66 -0
- package/test-app/app/actions/hello.js +7 -0
- package/test-app/app/app.js +10 -0
- package/test-app/app/static/app.html +9 -0
- package/test-app/app/static/styles.css +9 -0
- package/test-app/app/titan.d.ts +249 -0
- package/test-app/eslint.config.js +8 -0
- package/test-app/jsconfig.json +20 -0
- package/test-app/package.json +25 -0
- package/test-app/server/Cargo.toml +32 -0
- package/test-app/server/src/action_management.rs +171 -0
- package/test-app/server/src/errors.rs +10 -0
- package/test-app/server/src/extensions/builtin.rs +828 -0
- package/test-app/server/src/extensions/external.rs +309 -0
- package/test-app/server/src/extensions/mod.rs +430 -0
- package/test-app/server/src/extensions/titan_core.js +178 -0
- package/test-app/server/src/main.rs +433 -0
- package/test-app/server/src/runtime.rs +314 -0
- package/test-app/server/src/utils.rs +33 -0
- package/test-app/server/titan_storage.json +5 -0
- package/test-app/titan/bundle.js +264 -0
- package/test-app/titan/dev.js +350 -0
- package/test-app/titan/error-box.js +268 -0
- package/test-app/titan/titan.js +129 -0
- package/titan.json +20 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#![allow(unused)]
|
|
2
|
+
pub mod builtin;
|
|
3
|
+
pub mod external;
|
|
4
|
+
|
|
5
|
+
use crate::action_management::scan_actions;
|
|
6
|
+
use crate::utils::{blue, gray, green, red};
|
|
7
|
+
use bytes::Bytes;
|
|
8
|
+
use crossbeam::channel::Sender;
|
|
9
|
+
use dashmap::DashMap;
|
|
10
|
+
use serde_json::Value;
|
|
11
|
+
use std::collections::HashMap;
|
|
12
|
+
use std::fs;
|
|
13
|
+
use std::path::PathBuf;
|
|
14
|
+
use std::sync::Once;
|
|
15
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
|
16
|
+
use tokio::sync::broadcast;
|
|
17
|
+
use v8;
|
|
18
|
+
|
|
19
|
+
// ----------------------------------------------------------------------------
|
|
20
|
+
// GLOBALS
|
|
21
|
+
// ----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
pub static SHARE_CONTEXT: OnceLock<ShareContextStore> = OnceLock::new();
|
|
24
|
+
pub static PROJECT_ROOT: OnceLock<PathBuf> = OnceLock::new();
|
|
25
|
+
|
|
26
|
+
pub struct ShareContextStore {
|
|
27
|
+
pub kv: DashMap<String, serde_json::Value>,
|
|
28
|
+
pub broadcast_tx: broadcast::Sender<(String, serde_json::Value)>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl ShareContextStore {
|
|
32
|
+
pub fn get() -> &'static Self {
|
|
33
|
+
SHARE_CONTEXT.get_or_init(|| {
|
|
34
|
+
let (tx, _) = broadcast::channel(1000);
|
|
35
|
+
Self {
|
|
36
|
+
kv: DashMap::new(),
|
|
37
|
+
broadcast_tx: tx,
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Re-exports for easier access
|
|
44
|
+
pub fn load_project_extensions(root: PathBuf) {
|
|
45
|
+
PROJECT_ROOT.get_or_init(|| root.clone());
|
|
46
|
+
external::load_project_extensions(root);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ----------------------------------------------------------------------------
|
|
50
|
+
// TITAN RUNTIME
|
|
51
|
+
// ----------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
pub enum TitanAsyncOp {
|
|
54
|
+
Fetch {
|
|
55
|
+
url: String,
|
|
56
|
+
method: String,
|
|
57
|
+
body: Option<String>,
|
|
58
|
+
headers: Vec<(String, String)>,
|
|
59
|
+
},
|
|
60
|
+
DbQuery {
|
|
61
|
+
conn: String,
|
|
62
|
+
query: String,
|
|
63
|
+
},
|
|
64
|
+
FsRead {
|
|
65
|
+
path: String,
|
|
66
|
+
},
|
|
67
|
+
Batch(Vec<TitanAsyncOp>),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pub struct WorkerAsyncResult {
|
|
71
|
+
pub drift_id: u32,
|
|
72
|
+
pub result: serde_json::Value,
|
|
73
|
+
pub duration_ms: f64,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub struct AsyncOpRequest {
|
|
77
|
+
pub op: TitanAsyncOp,
|
|
78
|
+
pub drift_id: u32,
|
|
79
|
+
pub request_id: u32,
|
|
80
|
+
pub op_type: String,
|
|
81
|
+
pub respond_tx: tokio::sync::oneshot::Sender<WorkerAsyncResult>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub struct TitanRuntime {
|
|
85
|
+
pub id: usize,
|
|
86
|
+
pub isolate: v8::OwnedIsolate,
|
|
87
|
+
pub context: v8::Global<v8::Context>,
|
|
88
|
+
pub actions: HashMap<String, v8::Global<v8::Function>>,
|
|
89
|
+
pub worker_tx: crossbeam::channel::Sender<crate::runtime::WorkerCommand>,
|
|
90
|
+
|
|
91
|
+
// Async State
|
|
92
|
+
pub async_rx: crossbeam::channel::Receiver<WorkerAsyncResult>,
|
|
93
|
+
pub async_tx: crossbeam::channel::Sender<WorkerAsyncResult>,
|
|
94
|
+
pub pending_drifts: HashMap<u32, v8::Global<v8::PromiseResolver>>,
|
|
95
|
+
pub pending_requests: HashMap<u32, tokio::sync::oneshot::Sender<crate::runtime::WorkerResult>>,
|
|
96
|
+
pub drift_counter: u32,
|
|
97
|
+
pub request_counter: u32,
|
|
98
|
+
|
|
99
|
+
pub tokio_handle: tokio::runtime::Handle,
|
|
100
|
+
pub global_async_tx: tokio::sync::mpsc::Sender<AsyncOpRequest>,
|
|
101
|
+
pub request_timings: HashMap<u32, Vec<(String, f64)>>,
|
|
102
|
+
pub drift_to_request: HashMap<u32, u32>,
|
|
103
|
+
pub completed_drifts: HashMap<u32, serde_json::Value>,
|
|
104
|
+
pub active_requests: HashMap<u32, RequestData>,
|
|
105
|
+
pub request_start_counters: HashMap<u32, u32>,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[derive(Clone)]
|
|
109
|
+
pub struct RequestData {
|
|
110
|
+
pub action_name: String,
|
|
111
|
+
pub body: Option<Bytes>,
|
|
112
|
+
pub method: String,
|
|
113
|
+
pub path: String,
|
|
114
|
+
pub headers: Vec<(String, String)>,
|
|
115
|
+
pub params: Vec<(String, String)>,
|
|
116
|
+
pub query: Vec<(String, String)>,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
unsafe impl Send for TitanRuntime {}
|
|
120
|
+
unsafe impl Sync for TitanRuntime {}
|
|
121
|
+
|
|
122
|
+
static V8_INIT: Once = Once::new();
|
|
123
|
+
|
|
124
|
+
pub fn init_v8() {
|
|
125
|
+
V8_INIT.call_once(|| {
|
|
126
|
+
let platform = v8::new_default_platform(0, false).make_shared();
|
|
127
|
+
v8::V8::initialize_platform(platform);
|
|
128
|
+
v8::V8::initialize();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
pub fn init_runtime_worker(
|
|
133
|
+
id: usize,
|
|
134
|
+
root: PathBuf,
|
|
135
|
+
worker_tx: crossbeam::channel::Sender<crate::runtime::WorkerCommand>,
|
|
136
|
+
tokio_handle: tokio::runtime::Handle,
|
|
137
|
+
global_async_tx: tokio::sync::mpsc::Sender<AsyncOpRequest>,
|
|
138
|
+
) -> TitanRuntime {
|
|
139
|
+
init_v8();
|
|
140
|
+
|
|
141
|
+
// Memory optimization strategy (v8 0.106.0 limitations):
|
|
142
|
+
// - V8 snapshots reduce memory footprint by sharing compiled code
|
|
143
|
+
// - Each isolate still has its own heap, but the snapshot reduces base overhead
|
|
144
|
+
// - For explicit heap limits, use V8 flags: --max-old-space-size=128
|
|
145
|
+
|
|
146
|
+
let params = v8::CreateParams::default();
|
|
147
|
+
let mut isolate = v8::Isolate::new(params);
|
|
148
|
+
|
|
149
|
+
let (global_context, actions_map) = {
|
|
150
|
+
let handle_scope = &mut v8::HandleScope::new(&mut isolate);
|
|
151
|
+
let context = v8::Context::new(handle_scope, v8::ContextOptions::default());
|
|
152
|
+
let scope = &mut v8::ContextScope::new(handle_scope, context);
|
|
153
|
+
let global = context.global(scope);
|
|
154
|
+
|
|
155
|
+
// Inject Titan Runtime APIs
|
|
156
|
+
inject_extensions(scope, global);
|
|
157
|
+
|
|
158
|
+
// Root Metadata (Dynamic per app instance)
|
|
159
|
+
let root_str = v8::String::new(scope, root.to_str().unwrap_or(".")).unwrap();
|
|
160
|
+
let root_key = v8_str(scope, "__titan_root");
|
|
161
|
+
global.set(scope, root_key.into(), root_str.into());
|
|
162
|
+
|
|
163
|
+
// Load Actions (Cold start optimization target)
|
|
164
|
+
let mut map = HashMap::new();
|
|
165
|
+
let action_files = scan_actions(&root);
|
|
166
|
+
for (name, path) in action_files {
|
|
167
|
+
if let Ok(code) = fs::read_to_string(&path) {
|
|
168
|
+
let wrapped_source =
|
|
169
|
+
format!("(function() {{ {} }})(); globalThis[\"{}\"];", code, name);
|
|
170
|
+
let source_str = v8_str(scope, &wrapped_source);
|
|
171
|
+
let try_catch = &mut v8::TryCatch::new(scope);
|
|
172
|
+
if let Some(script) = v8::Script::compile(try_catch, source_str, None) {
|
|
173
|
+
if let Some(val) = script.run(try_catch) {
|
|
174
|
+
if val.is_function() {
|
|
175
|
+
let func = v8::Local::<v8::Function>::try_from(val).unwrap();
|
|
176
|
+
map.insert(name.clone(), v8::Global::new(try_catch, func));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
(v8::Global::new(scope, context), map)
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
let (async_tx, async_rx) = crossbeam::channel::unbounded();
|
|
186
|
+
|
|
187
|
+
TitanRuntime {
|
|
188
|
+
id,
|
|
189
|
+
isolate,
|
|
190
|
+
context: global_context,
|
|
191
|
+
actions: actions_map,
|
|
192
|
+
worker_tx,
|
|
193
|
+
async_rx,
|
|
194
|
+
async_tx,
|
|
195
|
+
pending_drifts: HashMap::new(),
|
|
196
|
+
pending_requests: HashMap::new(),
|
|
197
|
+
drift_counter: 0,
|
|
198
|
+
request_counter: 0,
|
|
199
|
+
tokio_handle,
|
|
200
|
+
global_async_tx,
|
|
201
|
+
request_timings: HashMap::new(),
|
|
202
|
+
drift_to_request: HashMap::new(),
|
|
203
|
+
completed_drifts: HashMap::new(),
|
|
204
|
+
active_requests: HashMap::new(),
|
|
205
|
+
request_start_counters: HashMap::new(),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
pub fn inject_extensions(scope: &mut v8::HandleScope, global: v8::Local<v8::Object>) {
|
|
210
|
+
// Ensuring globalThis
|
|
211
|
+
let gt_key = v8_str(scope, "globalThis");
|
|
212
|
+
global.set(scope, gt_key.into(), global.into());
|
|
213
|
+
|
|
214
|
+
let t_obj = v8::Object::new(scope);
|
|
215
|
+
let t_key = v8_str(scope, "t");
|
|
216
|
+
global
|
|
217
|
+
.create_data_property(scope, t_key.into(), t_obj.into())
|
|
218
|
+
.unwrap();
|
|
219
|
+
|
|
220
|
+
// Call individual injectors
|
|
221
|
+
builtin::inject_builtin_extensions(scope, global, t_obj);
|
|
222
|
+
external::inject_external_extensions(scope, global, t_obj);
|
|
223
|
+
|
|
224
|
+
global.set(scope, t_key.into(), t_obj.into());
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
pub fn v8_to_json<'s>(
|
|
228
|
+
scope: &mut v8::HandleScope<'s>,
|
|
229
|
+
value: v8::Local<v8::Value>,
|
|
230
|
+
) -> serde_json::Value {
|
|
231
|
+
if value.is_null_or_undefined() {
|
|
232
|
+
return serde_json::Value::Null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Boolean
|
|
236
|
+
if value.is_boolean() {
|
|
237
|
+
return serde_json::Value::Bool(value.boolean_value(scope));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Number
|
|
241
|
+
if value.is_number() {
|
|
242
|
+
let n = value.number_value(scope).unwrap_or(0.0);
|
|
243
|
+
return serde_json::Value::Number(
|
|
244
|
+
serde_json::Number::from_f64(n).unwrap_or_else(|| serde_json::Number::from(0)),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// String
|
|
249
|
+
if value.is_string() {
|
|
250
|
+
let s = value.to_string(scope).unwrap().to_rust_string_lossy(scope);
|
|
251
|
+
return serde_json::Value::String(s);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Array
|
|
255
|
+
if value.is_array() {
|
|
256
|
+
let arr = v8::Local::<v8::Array>::try_from(value).unwrap();
|
|
257
|
+
let mut list = Vec::with_capacity(arr.length() as usize);
|
|
258
|
+
for i in 0..arr.length() {
|
|
259
|
+
let element = arr
|
|
260
|
+
.get_index(scope, i)
|
|
261
|
+
.unwrap_or_else(|| v8::null(scope).into());
|
|
262
|
+
list.push(v8_to_json(scope, element));
|
|
263
|
+
}
|
|
264
|
+
return serde_json::Value::Array(list);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Object
|
|
268
|
+
if value.is_object() {
|
|
269
|
+
let obj = value.to_object(scope).unwrap();
|
|
270
|
+
|
|
271
|
+
let props = obj
|
|
272
|
+
.get_own_property_names(scope, v8::GetPropertyNamesArgs::default())
|
|
273
|
+
.unwrap();
|
|
274
|
+
|
|
275
|
+
let mut map = serde_json::Map::new();
|
|
276
|
+
|
|
277
|
+
for i in 0..props.length() {
|
|
278
|
+
let key_val = props
|
|
279
|
+
.get_index(scope, i)
|
|
280
|
+
.unwrap_or_else(|| v8::null(scope).into());
|
|
281
|
+
|
|
282
|
+
let key = key_val
|
|
283
|
+
.to_string(scope)
|
|
284
|
+
.unwrap()
|
|
285
|
+
.to_rust_string_lossy(scope);
|
|
286
|
+
|
|
287
|
+
let val = obj
|
|
288
|
+
.get(scope, key_val.into())
|
|
289
|
+
.unwrap_or_else(|| v8::null(scope).into());
|
|
290
|
+
|
|
291
|
+
map.insert(key, v8_to_json(scope, val));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return serde_json::Value::Object(map);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
serde_json::Value::Null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ----------------------------------------------------------------------------
|
|
301
|
+
// EXECUTION HELPERS
|
|
302
|
+
// ----------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
pub fn execute_action_optimized(
|
|
305
|
+
runtime: &mut TitanRuntime,
|
|
306
|
+
request_id: u32,
|
|
307
|
+
action_name: &str,
|
|
308
|
+
req_body: Option<bytes::Bytes>,
|
|
309
|
+
req_method: &str,
|
|
310
|
+
req_path: &str,
|
|
311
|
+
headers: &[(String, String)],
|
|
312
|
+
params: &[(String, String)],
|
|
313
|
+
query: &[(String, String)],
|
|
314
|
+
) {
|
|
315
|
+
let context_global = runtime.context.clone();
|
|
316
|
+
let actions_map = runtime.actions.clone(); // Clone the map of globals (cheap)
|
|
317
|
+
let isolate = &mut runtime.isolate;
|
|
318
|
+
|
|
319
|
+
let handle_scope = &mut v8::HandleScope::new(isolate);
|
|
320
|
+
let context = v8::Local::new(handle_scope, context_global);
|
|
321
|
+
let scope = &mut v8::ContextScope::new(handle_scope, context);
|
|
322
|
+
|
|
323
|
+
let req_obj = v8::Object::new(scope);
|
|
324
|
+
|
|
325
|
+
let req_id_key = v8_str(scope, "__titan_request_id");
|
|
326
|
+
let req_id_val = v8::Integer::new(scope, request_id as i32);
|
|
327
|
+
req_obj.set(scope, req_id_key.into(), req_id_val.into());
|
|
328
|
+
|
|
329
|
+
let m_key = v8_str(scope, "method");
|
|
330
|
+
let m_val = v8_str(scope, req_method);
|
|
331
|
+
req_obj.set(scope, m_key.into(), m_val.into());
|
|
332
|
+
|
|
333
|
+
let p_key = v8_str(scope, "path");
|
|
334
|
+
let p_val = v8_str(scope, req_path);
|
|
335
|
+
req_obj.set(scope, p_key.into(), p_val.into());
|
|
336
|
+
|
|
337
|
+
let body_val: v8::Local<v8::Value> = if let Some(bytes) = req_body {
|
|
338
|
+
let vec = bytes.to_vec();
|
|
339
|
+
let store = v8::ArrayBuffer::new_backing_store_from_boxed_slice(vec.into_boxed_slice());
|
|
340
|
+
let ab = v8::ArrayBuffer::with_backing_store(scope, &store.make_shared());
|
|
341
|
+
ab.into()
|
|
342
|
+
} else {
|
|
343
|
+
v8::null(scope).into()
|
|
344
|
+
};
|
|
345
|
+
let rb_key = v8_str(scope, "rawBody");
|
|
346
|
+
req_obj.set(scope, rb_key.into(), body_val);
|
|
347
|
+
|
|
348
|
+
let h_obj = v8::Object::new(scope);
|
|
349
|
+
for (k, v) in headers {
|
|
350
|
+
let k_v8 = v8_str(scope, k);
|
|
351
|
+
let v_v8 = v8_str(scope, v);
|
|
352
|
+
h_obj.set(scope, k_v8.into(), v_v8.into());
|
|
353
|
+
}
|
|
354
|
+
let h_key = v8_str(scope, "headers");
|
|
355
|
+
req_obj.set(scope, h_key.into(), h_obj.into());
|
|
356
|
+
|
|
357
|
+
let p_obj = v8::Object::new(scope);
|
|
358
|
+
for (k, v) in params {
|
|
359
|
+
let k_v8 = v8_str(scope, k);
|
|
360
|
+
let v_v8 = v8_str(scope, v);
|
|
361
|
+
p_obj.set(scope, k_v8.into(), v_v8.into());
|
|
362
|
+
}
|
|
363
|
+
let params_key = v8_str(scope, "params");
|
|
364
|
+
req_obj.set(scope, params_key.into(), p_obj.into());
|
|
365
|
+
|
|
366
|
+
let q_obj = v8::Object::new(scope);
|
|
367
|
+
for (k, v) in query {
|
|
368
|
+
let k_v8 = v8_str(scope, k);
|
|
369
|
+
let v_v8 = v8_str(scope, v);
|
|
370
|
+
q_obj.set(scope, k_v8.into(), v_v8.into());
|
|
371
|
+
}
|
|
372
|
+
let q_key = v8_str(scope, "query");
|
|
373
|
+
req_obj.set(scope, q_key.into(), q_obj.into());
|
|
374
|
+
|
|
375
|
+
let global = context.global(scope);
|
|
376
|
+
let req_tr_key = v8_str(scope, "__titan_req");
|
|
377
|
+
global.set(scope, req_tr_key.into(), req_obj.into());
|
|
378
|
+
|
|
379
|
+
if let Some(action_global) = actions_map.get(action_name) {
|
|
380
|
+
let action_fn = v8::Local::new(scope, action_global);
|
|
381
|
+
let tr_act_key = v8_str(scope, "__titan_action");
|
|
382
|
+
let tr_act_val = v8_str(scope, action_name);
|
|
383
|
+
global.set(scope, tr_act_key.into(), tr_act_val.into());
|
|
384
|
+
let try_catch = &mut v8::TryCatch::new(scope);
|
|
385
|
+
|
|
386
|
+
if let Some(_) = action_fn.call(try_catch, global.into(), &[req_obj.into()]) {
|
|
387
|
+
// JS side is responsible for calling t._finish_request(requestId, result)
|
|
388
|
+
// Even if the action is NOT async, our JS wrapper in titan_core.js will handle it.
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
let msg = try_catch
|
|
393
|
+
.message()
|
|
394
|
+
.map(|m| m.get(try_catch).to_rust_string_lossy(try_catch))
|
|
395
|
+
.unwrap_or("Unknown error".to_string());
|
|
396
|
+
|
|
397
|
+
// Check for suspension
|
|
398
|
+
if msg.contains("SUSPEND") {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if let Some(tx) = runtime.pending_requests.remove(&request_id) {
|
|
403
|
+
let _ = tx.send(crate::runtime::WorkerResult {
|
|
404
|
+
json: serde_json::json!({"error": msg}),
|
|
405
|
+
timings: vec![]
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
if let Some(tx) = runtime.pending_requests.remove(&request_id) {
|
|
410
|
+
let _ = tx.send(crate::runtime::WorkerResult {
|
|
411
|
+
json: serde_json::json!({"error": format!("Action '{}' not found", action_name)}),
|
|
412
|
+
timings: vec![]
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
pub fn v8_str<'s>(scope: &mut v8::HandleScope<'s>, s: &str) -> v8::Local<'s, v8::String> {
|
|
419
|
+
v8::String::new(scope, s).unwrap()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
pub fn v8_to_string(scope: &mut v8::HandleScope, value: v8::Local<v8::Value>) -> String {
|
|
423
|
+
value.to_string(scope).unwrap().to_rust_string_lossy(scope)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
pub fn throw(scope: &mut v8::HandleScope, msg: &str) {
|
|
427
|
+
let message = v8_str(scope, msg);
|
|
428
|
+
let exception = v8::Exception::error(scope, message);
|
|
429
|
+
scope.throw_exception(exception);
|
|
430
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
|
|
2
|
+
// Titan Core Runtime JS
|
|
3
|
+
// This is embedded in the binary for ultra-fast startup.
|
|
4
|
+
|
|
5
|
+
globalThis.global = globalThis;
|
|
6
|
+
|
|
7
|
+
// defineAction identity helper
|
|
8
|
+
globalThis.defineAction = (fn) => {
|
|
9
|
+
if (fn.__titanWrapped) return fn;
|
|
10
|
+
const wrapped = function (req) {
|
|
11
|
+
const requestId = req.__titan_request_id;
|
|
12
|
+
|
|
13
|
+
const isSuspend = (err) => {
|
|
14
|
+
const msg = err && (err.message || String(err));
|
|
15
|
+
return msg && (msg.includes("__SUSPEND__") || msg.includes("SUSPEND"));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const result = fn(req);
|
|
20
|
+
|
|
21
|
+
if (result && typeof result.then === 'function') {
|
|
22
|
+
// It's a Promise (or thenable)
|
|
23
|
+
result.then(
|
|
24
|
+
(data) => t._finish_request(requestId, data),
|
|
25
|
+
(err) => {
|
|
26
|
+
if (isSuspend(err)) return;
|
|
27
|
+
t._finish_request(requestId, { error: err.message || String(err) })
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
// Synchronous direct return
|
|
32
|
+
t._finish_request(requestId, result);
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (isSuspend(err)) return;
|
|
36
|
+
t._finish_request(requestId, { error: err.message || String(err) });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
wrapped.__titanWrapped = true;
|
|
40
|
+
return wrapped;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// TextDecoder Polyfill using native t.decodeUtf8
|
|
44
|
+
globalThis.TextDecoder = class TextDecoder {
|
|
45
|
+
decode(buffer) {
|
|
46
|
+
return t.decodeUtf8(buffer);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Process environment variables
|
|
51
|
+
globalThis.process = {
|
|
52
|
+
env: t.loadEnv()
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Everything is strictly synchronous and request-driven.
|
|
56
|
+
|
|
57
|
+
function createAsyncOp(op) {
|
|
58
|
+
return new Proxy(op, {
|
|
59
|
+
get(target, prop) {
|
|
60
|
+
// Internal properties accessed by drift()
|
|
61
|
+
if (prop === "__titanAsync" || prop === "type" || prop === "data" || typeof prop === 'symbol') {
|
|
62
|
+
return target[prop];
|
|
63
|
+
}
|
|
64
|
+
// If they access anything else (body, status, ok, etc.), it's a mistake
|
|
65
|
+
throw new Error(`[Titan Error] Attempted to access response property '${String(prop)}' without using drift(). \n` +
|
|
66
|
+
`Fix: const result = drift(t.fetch(...));`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- Response API ---
|
|
72
|
+
const titanResponse = {
|
|
73
|
+
json(data, status = 200, extraHeaders = {}) {
|
|
74
|
+
return {
|
|
75
|
+
_isResponse: true,
|
|
76
|
+
status,
|
|
77
|
+
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
78
|
+
body: JSON.stringify(data)
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
text(data, status = 200, extraHeaders = {}) {
|
|
82
|
+
return {
|
|
83
|
+
_isResponse: true,
|
|
84
|
+
status,
|
|
85
|
+
headers: { "Content-Type": "text/plain", ...extraHeaders },
|
|
86
|
+
body: String(data)
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
html(data, status = 200, extraHeaders = {}) {
|
|
90
|
+
return {
|
|
91
|
+
_isResponse: true,
|
|
92
|
+
status,
|
|
93
|
+
headers: { "Content-Type": "text/html", ...extraHeaders },
|
|
94
|
+
body: String(data)
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
redirect(url, status = 302, extraHeaders = {}) {
|
|
98
|
+
return {
|
|
99
|
+
_isResponse: true,
|
|
100
|
+
status,
|
|
101
|
+
headers: { "Location": url, ...extraHeaders },
|
|
102
|
+
redirect: url
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (globalThis.t) {
|
|
108
|
+
globalThis.t.response = titanResponse;
|
|
109
|
+
} else {
|
|
110
|
+
globalThis.t = { response: titanResponse };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Drift Support ---
|
|
114
|
+
|
|
115
|
+
globalThis.drift = function (value) {
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
for (const item of value) {
|
|
118
|
+
if (!item || !item.__titanAsync) {
|
|
119
|
+
throw new Error("drift() array must contain t.fetch/t.db.query/t.read async ops only.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (!value || !value.__titanAsync) {
|
|
123
|
+
throw new Error("drift() must wrap t.fetch/t.db.query/t.read async ops only.");
|
|
124
|
+
}
|
|
125
|
+
return t._drift_call(value);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Wrap native fetch
|
|
129
|
+
if (t.fetch && !t.fetch.__titanWrapped) {
|
|
130
|
+
const nativeFetch = t.fetch;
|
|
131
|
+
t.fetch = function (...args) {
|
|
132
|
+
return createAsyncOp(nativeFetch(...args));
|
|
133
|
+
};
|
|
134
|
+
t.fetch.__titanWrapped = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Wrap t.read (it's now async metadata)
|
|
138
|
+
if (t.read && !t.read.__titanWrapped) {
|
|
139
|
+
const nativeRead = t.read;
|
|
140
|
+
t.read = function (path) {
|
|
141
|
+
return createAsyncOp(nativeRead(path));
|
|
142
|
+
};
|
|
143
|
+
t.read.__titanWrapped = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Fix t.core.fs.read mapping
|
|
147
|
+
if (t.core && t.core.fs) {
|
|
148
|
+
if (t.core.fs.read && !t.core.fs.read.__titanWrapped) {
|
|
149
|
+
const nativeFsRead = t.core.fs.read;
|
|
150
|
+
t.core.fs.read = function (path) {
|
|
151
|
+
return createAsyncOp(nativeFsRead(path));
|
|
152
|
+
};
|
|
153
|
+
t.core.fs.read.__titanWrapped = true;
|
|
154
|
+
// Alias
|
|
155
|
+
t.core.fs.readFile = t.core.fs.read;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
// Wrap t.db.connect
|
|
162
|
+
const nativeDbConnect = t.db.connect;
|
|
163
|
+
t.db.connect = function (connString) {
|
|
164
|
+
const conn = nativeDbConnect(connString);
|
|
165
|
+
const nativeQuery = conn.query;
|
|
166
|
+
conn.query = (sql) => {
|
|
167
|
+
return createAsyncOp({
|
|
168
|
+
__titanAsync: true,
|
|
169
|
+
type: "db_query",
|
|
170
|
+
data: {
|
|
171
|
+
conn: connString,
|
|
172
|
+
query: sql
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
return conn;
|
|
177
|
+
};
|
|
178
|
+
|