@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.
@@ -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
+