host: expose harness state and actions over the world API

This commit is contained in:
2026-11-07 19:22:06 +00:00
parent c3c5b36d09
commit 5dc4833e28
33 changed files with 33178 additions and 112 deletions
+42
View File
@@ -0,0 +1,42 @@
//! HTTP requests share the harness inbox. Replies never leave the V8 thread as JS values.
use deno_core::{OpState, op2};
use serde_json::Value;
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use tokio::sync::mpsc::UnboundedSender;
pub(super) struct Reply {
pub sender: UnboundedSender<Value>,
pub finished: std::sync::Arc<tokio::sync::Notify>,
}
pub(super) type Replies = HashMap<String, Reply>;
#[op2]
pub fn op_harness_reply(
state: &mut OpState,
#[string] id: String,
#[serde] value: serde_json::Value,
) {
let replies = state.borrow_mut::<Replies>();
if value.is_null() {
if let Some(reply) = replies.remove(&id) {
reply.finished.notify_one();
}
} else if let Some(reply) = replies.get(&id) {
let _ = reply.sender.send(value);
}
}
#[op2]
pub async fn op_harness_closed(state: Rc<RefCell<OpState>>, #[string] id: String) {
let reply = state
.borrow()
.borrow::<Replies>()
.get(&id)
.map(|reply| (reply.sender.clone(), reply.finished.clone()));
if let Some((sender, finished)) = reply {
tokio::select! {
_ = sender.closed() => {},
_ = finished.notified() => {},
}
}
state.borrow_mut().borrow_mut::<Replies>().remove(&id);
}
+22 -2
View File
@@ -15,6 +15,14 @@ pub enum Input {
#[serde(rename = "requestId")]
request_id: String,
},
Api {
id: String,
method: String,
path: String,
body: Value,
#[serde(skip)]
reply: mpsc::UnboundedSender<Value>,
},
Stop,
}
@@ -31,8 +39,20 @@ pub(super) struct Inbox(pub tokio::sync::Mutex<Receiver>);
#[serde]
pub async fn op_input(state: Rc<RefCell<OpState>>) -> Result<serde_json::Value, JsErrorBox> {
let inbox = state.borrow().borrow::<Rc<Inbox>>().clone();
serde_json::to_value(inbox.0.lock().await.recv().await)
.map_err(|error| JsErrorBox::generic(error.to_string()))
let input = inbox.0.lock().await.recv().await;
if let Some(Input::Api { id, reply, .. }) = &input {
state
.borrow_mut()
.borrow_mut::<super::http::Replies>()
.insert(
id.clone(),
super::http::Reply {
sender: reply.clone(),
finished: Default::default(),
},
);
}
serde_json::to_value(input).map_err(|error| JsErrorBox::generic(error.to_string()))
}
pub(super) fn console(sender: Sender) {
+18
View File
@@ -1,6 +1,7 @@
//! Durable V8 harness, host-side SQLite storage, and world tool operations.
mod credentials;
mod environment;
mod http;
pub mod input;
pub(crate) mod loader;
use crate::{exec::Exec, world::World};
@@ -88,6 +89,8 @@ deno_core::extension!(
environment::op_exec,
environment::op_exec_next,
input::op_input,
http::op_harness_reply,
http::op_harness_closed,
environment::op_watch,
environment::op_watch_next,
loader::op_modules,
@@ -181,6 +184,10 @@ pub async fn run_module_with_input(
..Default::default()
});
runtime.op_state().borrow_mut().put(loader);
runtime
.op_state()
.borrow_mut()
.put(http::Replies::default());
runtime
.op_state()
.borrow_mut()
@@ -252,6 +259,17 @@ pub async fn run_console(
cred: Arc<crate::cred::store::Cred>,
) -> anyhow::Result<()> {
let (sender, receiver) = input::channel();
run_console_with_input(world, exec, cred, sender, receiver).await
}
/// Run the console with the inbox shared by the host HTTP API.
pub async fn run_console_with_input(
world: World,
exec: Arc<dyn Exec>,
cred: Arc<crate::cred::store::Cred>,
sender: input::Sender,
receiver: input::Receiver,
) -> anyhow::Result<()> {
input::console(sender.clone());
let signals = tokio::spawn(input::stop_signal(sender)?);
let source = concat!(