Skip to main content

agentos_v8_runtime/
stream.rs

1// Async event dispatch for child process and HTTP server streams
2
3/// Dispatch a stream event into V8 by calling the registered callback function.
4///
5/// Stream events are sent by the host when async operations (child processes,
6/// HTTP servers) produce data. The event_type determines which V8 dispatch
7/// function is called:
8/// - "child_stdout", "child_stderr", "child_exit" → _childProcessDispatch
9/// - "http_request" → _httpServerDispatch
10/// - "http2" → _http2Dispatch
11/// - "stdin", "stdin_end" → _stdinDispatch
12/// - "net_socket" → _netSocketDispatch
13/// - "signal" → __secureExecWasmSignalDispatch or _signalDispatch
14/// - "timer" → _timerDispatch
15pub fn dispatch_stream_event(scope: &mut v8::HandleScope, event_type: &str, payload: &[u8]) {
16    // Look up the dispatch function on the global object
17    let context = scope.get_current_context();
18    let global = context.global(scope);
19
20    let dispatch_names: &[&str] = match event_type {
21        "child_stdout" | "child_stderr" | "child_exit" => &["_childProcessDispatch"],
22        "http_request" => &["_httpServerDispatch"],
23        "http2" => &["_http2Dispatch"],
24        "stdin" | "stdin_end" => &["_stdinDispatch"],
25        "net_socket" => &["_netSocketDispatch"],
26        "signal" => &["__secureExecWasmSignalDispatch", "_signalDispatch"],
27        "timer" => &["_timerDispatch"],
28        _ => return, // Unknown event type — ignore
29    };
30
31    for dispatch_name in dispatch_names {
32        let key = v8::String::new(scope, dispatch_name).unwrap();
33        let maybe_fn = global.get(scope, key.into());
34
35        if let Some(func_val) = maybe_fn {
36            if func_val.is_function() {
37                let func = v8::Local::<v8::Function>::try_from(func_val).unwrap();
38
39                // Pass event_type and payload as arguments.
40                let event_str = v8::String::new(scope, event_type).unwrap();
41                let payload_val = if !payload.is_empty() {
42                    let maybe_v8_payload = {
43                        let tc = &mut v8::TryCatch::new(scope);
44                        crate::bridge::deserialize_v8_value(tc, payload).ok()
45                    };
46                    match maybe_v8_payload {
47                        Some(v) => v,
48                        None => match std::str::from_utf8(payload) {
49                            Ok(text) => match v8::String::new(scope, text) {
50                                Some(json_text) => v8::json::parse(scope, json_text)
51                                    .unwrap_or_else(|| json_text.into()),
52                                None => v8::null(scope).into(),
53                            },
54                            Err(_) => v8::null(scope).into(),
55                        },
56                    }
57                } else {
58                    v8::null(scope).into()
59                };
60
61                let undefined = v8::undefined(scope);
62                let args: &[v8::Local<v8::Value>] = &[event_str.into(), payload_val];
63                func.call(scope, undefined.into(), args);
64                return;
65            }
66        }
67    }
68}
69
70pub fn dispatch_signal_event(scope: &mut v8::HandleScope, signal_name: &str, signal: i32) {
71    let payload = v8::Object::new(scope);
72    let signal_key = v8::String::new(scope, "signal").expect("static V8 string");
73    let signal_value = v8::String::new(scope, signal_name).expect("signal V8 string");
74    payload.set(scope, signal_key.into(), signal_value.into());
75    let number_key = v8::String::new(scope, "number").expect("static V8 string");
76    let number_value = v8::Integer::new(scope, signal);
77    payload.set(scope, number_key.into(), number_value.into());
78    let action_key = v8::String::new(scope, "action").expect("static V8 string");
79    let action_value = v8::String::new(scope, "default").expect("static V8 string");
80    payload.set(scope, action_key.into(), action_value.into());
81    dispatch_stream_value(scope, "signal", payload.into());
82}
83
84pub fn dispatch_timer_event(scope: &mut v8::HandleScope, timer_id: u64) {
85    let timer_id = v8::Number::new(scope, timer_id as f64);
86    dispatch_stream_value(scope, "timer", timer_id.into());
87}
88
89fn dispatch_stream_value(
90    scope: &mut v8::HandleScope,
91    event_type: &str,
92    payload: v8::Local<v8::Value>,
93) {
94    let context = scope.get_current_context();
95    let global = context.global(scope);
96    let dispatch_names: &[&str] = match event_type {
97        "signal" => &["__secureExecWasmSignalDispatch", "_signalDispatch"],
98        "timer" => &["_timerDispatch"],
99        _ => return,
100    };
101    for dispatch_name in dispatch_names {
102        let Some(key) = v8::String::new(scope, dispatch_name) else {
103            continue;
104        };
105        let Some(value) = global.get(scope, key.into()) else {
106            continue;
107        };
108        let Ok(function) = v8::Local::<v8::Function>::try_from(value) else {
109            continue;
110        };
111        let Some(event) = v8::String::new(scope, event_type) else {
112            return;
113        };
114        let undefined = v8::undefined(scope);
115        function.call(scope, undefined.into(), &[event.into(), payload]);
116        return;
117    }
118}
119
120/// Notify the guest that one registered sidecar capability has durable work to
121/// drain. No socket/signal/timer payload crosses this boundary: the guest uses
122/// the identity to issue bounded drain operations against the owning subsystem.
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum ReadinessDispatch {
125    Delivered,
126    TargetMissing,
127    BridgeMissing,
128}
129
130pub fn dispatch_readiness(
131    scope: &mut v8::HandleScope,
132    capability_id: u64,
133    capability_generation: u64,
134    flags: agentos_runtime::readiness::ReadyFlags,
135) -> ReadinessDispatch {
136    let context = scope.get_current_context();
137    let global = context.global(scope);
138    let Some(key) = v8::String::new(scope, "_agentOSReadyDispatch") else {
139        return ReadinessDispatch::BridgeMissing;
140    };
141    let Some(value) = global.get(scope, key.into()) else {
142        return ReadinessDispatch::BridgeMissing;
143    };
144    let Ok(function) = v8::Local::<v8::Function>::try_from(value) else {
145        return ReadinessDispatch::BridgeMissing;
146    };
147
148    let capability_id = v8::BigInt::new_from_u64(scope, capability_id);
149    let capability_generation = v8::BigInt::new_from_u64(scope, capability_generation);
150    let flags = v8::Integer::new_from_unsigned(scope, u32::from(flags.bits()));
151    let undefined = v8::undefined(scope);
152    match function.call(
153        scope,
154        undefined.into(),
155        &[
156            capability_id.into(),
157            capability_generation.into(),
158            flags.into(),
159        ],
160    ) {
161        Some(result) if result.is_true() => ReadinessDispatch::Delivered,
162        _ => ReadinessDispatch::TargetMissing,
163    }
164}