feat(servo-shell): per-app WebView lifecycle driven by appd events (servo-embed only)
Task 10 -- App WebView lifecycle.
appd_ws module (servo-embed gated):
Background thread connects to the appd WebSocket on startup.
Sends QUERY_RUNNING to receive initial running sessions.
Translates LAUNCH_ACK -> AppdCmd::Launch and APP_STATE stopped
-> AppdCmd::Stop, then wakes the winit event loop via the
shared EventLoopWaker.
embedder changes:
App struct gains app_rx (mpsc receiver), app_webviews
(HashMap<session_id, WebView>), active_session, and a stored
rendering_context used when creating app WebViews later.
create_app_webview(): resolves weft-app://<app_id>/index.html
to a file URL, creates a dedicated UserContentManager with the
weftIpc bridge injected (includes window.weftSessionId), builds
and registers a new WebView.
about_to_wait() drains app_rx: creates WebViews for Launch
commands, removes and clears active_session for Stop commands.
active_webview() returns the active-session WebView when one
exists, falling back to the system-ui WebView. Rendering,
keyboard, and mouse events all route through active_webview().
Resize propagates to both the system WebView and all app WebViews.
run() creates the mpsc channel and spawns the appd listener
before entering the winit event loop.
2026-03-11 16:59:12 +00:00
|
|
|
#![cfg(feature = "servo-embed")]
|
|
|
|
|
|
|
|
|
|
use std::sync::mpsc;
|
|
|
|
|
|
|
|
|
|
pub enum AppdCmd {
|
|
|
|
|
Launch { session_id: u64, app_id: String },
|
|
|
|
|
Stop { session_id: u64 },
|
2026-03-11 17:41:00 +00:00
|
|
|
SyncSessions { active: Vec<(u64, String)> },
|
feat(servo-shell): per-app WebView lifecycle driven by appd events (servo-embed only)
Task 10 -- App WebView lifecycle.
appd_ws module (servo-embed gated):
Background thread connects to the appd WebSocket on startup.
Sends QUERY_RUNNING to receive initial running sessions.
Translates LAUNCH_ACK -> AppdCmd::Launch and APP_STATE stopped
-> AppdCmd::Stop, then wakes the winit event loop via the
shared EventLoopWaker.
embedder changes:
App struct gains app_rx (mpsc receiver), app_webviews
(HashMap<session_id, WebView>), active_session, and a stored
rendering_context used when creating app WebViews later.
create_app_webview(): resolves weft-app://<app_id>/index.html
to a file URL, creates a dedicated UserContentManager with the
weftIpc bridge injected (includes window.weftSessionId), builds
and registers a new WebView.
about_to_wait() drains app_rx: creates WebViews for Launch
commands, removes and clears active_session for Stop commands.
active_webview() returns the active-session WebView when one
exists, falling back to the system-ui WebView. Rendering,
keyboard, and mouse events all route through active_webview().
Resize propagates to both the system WebView and all app WebViews.
run() creates the mpsc channel and spawns the appd listener
before entering the winit event loop.
2026-03-11 16:59:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn spawn_appd_listener(
|
|
|
|
|
ws_port: u16,
|
|
|
|
|
tx: mpsc::SyncSender<AppdCmd>,
|
|
|
|
|
wake: Box<dyn Fn() + Send>,
|
|
|
|
|
) {
|
|
|
|
|
std::thread::Builder::new()
|
|
|
|
|
.name("appd-ws".into())
|
|
|
|
|
.spawn(move || run_listener(ws_port, tx, wake))
|
|
|
|
|
.ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn run_listener(ws_port: u16, tx: mpsc::SyncSender<AppdCmd>, wake: Box<dyn Fn() + Send>) {
|
|
|
|
|
let url = format!("ws://127.0.0.1:{ws_port}");
|
2026-03-11 17:24:54 +00:00
|
|
|
let mut backoff = std::time::Duration::from_millis(500);
|
|
|
|
|
const MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(16);
|
feat(servo-shell): per-app WebView lifecycle driven by appd events (servo-embed only)
Task 10 -- App WebView lifecycle.
appd_ws module (servo-embed gated):
Background thread connects to the appd WebSocket on startup.
Sends QUERY_RUNNING to receive initial running sessions.
Translates LAUNCH_ACK -> AppdCmd::Launch and APP_STATE stopped
-> AppdCmd::Stop, then wakes the winit event loop via the
shared EventLoopWaker.
embedder changes:
App struct gains app_rx (mpsc receiver), app_webviews
(HashMap<session_id, WebView>), active_session, and a stored
rendering_context used when creating app WebViews later.
create_app_webview(): resolves weft-app://<app_id>/index.html
to a file URL, creates a dedicated UserContentManager with the
weftIpc bridge injected (includes window.weftSessionId), builds
and registers a new WebView.
about_to_wait() drains app_rx: creates WebViews for Launch
commands, removes and clears active_session for Stop commands.
active_webview() returns the active-session WebView when one
exists, falling back to the system-ui WebView. Rendering,
keyboard, and mouse events all route through active_webview().
Resize propagates to both the system WebView and all app WebViews.
run() creates the mpsc channel and spawns the appd listener
before entering the winit event loop.
2026-03-11 16:59:12 +00:00
|
|
|
|
|
|
|
|
loop {
|
2026-03-11 17:24:54 +00:00
|
|
|
match tungstenite::connect(&url) {
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::debug!("appd WebSocket connect failed: {e}; retry in {backoff:?}");
|
|
|
|
|
std::thread::sleep(backoff);
|
|
|
|
|
backoff = (backoff * 2).min(MAX_BACKOFF);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
Ok((mut ws, _)) => {
|
|
|
|
|
backoff = std::time::Duration::from_millis(500);
|
|
|
|
|
let _ = ws.send(tungstenite::Message::Text(
|
|
|
|
|
r#"{"type":"QUERY_RUNNING"}"#.into(),
|
|
|
|
|
));
|
|
|
|
|
loop {
|
|
|
|
|
match ws.read() {
|
|
|
|
|
Ok(tungstenite::Message::Text(text)) => {
|
|
|
|
|
process_message(&text, &tx, &*wake);
|
|
|
|
|
}
|
|
|
|
|
Ok(_) => {}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::debug!("appd WebSocket read error: {e}; reconnecting");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat(servo-shell): per-app WebView lifecycle driven by appd events (servo-embed only)
Task 10 -- App WebView lifecycle.
appd_ws module (servo-embed gated):
Background thread connects to the appd WebSocket on startup.
Sends QUERY_RUNNING to receive initial running sessions.
Translates LAUNCH_ACK -> AppdCmd::Launch and APP_STATE stopped
-> AppdCmd::Stop, then wakes the winit event loop via the
shared EventLoopWaker.
embedder changes:
App struct gains app_rx (mpsc receiver), app_webviews
(HashMap<session_id, WebView>), active_session, and a stored
rendering_context used when creating app WebViews later.
create_app_webview(): resolves weft-app://<app_id>/index.html
to a file URL, creates a dedicated UserContentManager with the
weftIpc bridge injected (includes window.weftSessionId), builds
and registers a new WebView.
about_to_wait() drains app_rx: creates WebViews for Launch
commands, removes and clears active_session for Stop commands.
active_webview() returns the active-session WebView when one
exists, falling back to the system-ui WebView. Rendering,
keyboard, and mouse events all route through active_webview().
Resize propagates to both the system WebView and all app WebViews.
run() creates the mpsc channel and spawns the appd listener
before entering the winit event loop.
2026-03-11 16:59:12 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-11 17:24:54 +00:00
|
|
|
std::thread::sleep(backoff);
|
|
|
|
|
backoff = (backoff * 2).min(MAX_BACKOFF);
|
feat(servo-shell): per-app WebView lifecycle driven by appd events (servo-embed only)
Task 10 -- App WebView lifecycle.
appd_ws module (servo-embed gated):
Background thread connects to the appd WebSocket on startup.
Sends QUERY_RUNNING to receive initial running sessions.
Translates LAUNCH_ACK -> AppdCmd::Launch and APP_STATE stopped
-> AppdCmd::Stop, then wakes the winit event loop via the
shared EventLoopWaker.
embedder changes:
App struct gains app_rx (mpsc receiver), app_webviews
(HashMap<session_id, WebView>), active_session, and a stored
rendering_context used when creating app WebViews later.
create_app_webview(): resolves weft-app://<app_id>/index.html
to a file URL, creates a dedicated UserContentManager with the
weftIpc bridge injected (includes window.weftSessionId), builds
and registers a new WebView.
about_to_wait() drains app_rx: creates WebViews for Launch
commands, removes and clears active_session for Stop commands.
active_webview() returns the active-session WebView when one
exists, falling back to the system-ui WebView. Rendering,
keyboard, and mouse events all route through active_webview().
Resize propagates to both the system WebView and all app WebViews.
run() creates the mpsc channel and spawns the appd listener
before entering the winit event loop.
2026-03-11 16:59:12 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn process_message(text: &str, tx: &mpsc::SyncSender<AppdCmd>, wake: &dyn Fn()) {
|
|
|
|
|
let Ok(v) = serde_json::from_str::<serde_json::Value>(text) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match v["type"].as_str() {
|
|
|
|
|
Some("LAUNCH_ACK") => {
|
|
|
|
|
let Some(session_id) = v["session_id"].as_u64() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let Some(app_id) = v["app_id"].as_str().map(str::to_string) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
if tx.try_send(AppdCmd::Launch { session_id, app_id }).is_ok() {
|
|
|
|
|
wake();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Some("RUNNING_APPS") => {
|
|
|
|
|
let Some(sessions) = v["sessions"].as_array() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
2026-03-11 17:41:00 +00:00
|
|
|
let active: Vec<(u64, String)> = sessions
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|s| {
|
|
|
|
|
let sid = s["session_id"].as_u64()?;
|
|
|
|
|
let aid = s["app_id"].as_str()?.to_string();
|
|
|
|
|
Some((sid, aid))
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
if tx.try_send(AppdCmd::SyncSessions { active }).is_ok() {
|
|
|
|
|
wake();
|
feat(servo-shell): per-app WebView lifecycle driven by appd events (servo-embed only)
Task 10 -- App WebView lifecycle.
appd_ws module (servo-embed gated):
Background thread connects to the appd WebSocket on startup.
Sends QUERY_RUNNING to receive initial running sessions.
Translates LAUNCH_ACK -> AppdCmd::Launch and APP_STATE stopped
-> AppdCmd::Stop, then wakes the winit event loop via the
shared EventLoopWaker.
embedder changes:
App struct gains app_rx (mpsc receiver), app_webviews
(HashMap<session_id, WebView>), active_session, and a stored
rendering_context used when creating app WebViews later.
create_app_webview(): resolves weft-app://<app_id>/index.html
to a file URL, creates a dedicated UserContentManager with the
weftIpc bridge injected (includes window.weftSessionId), builds
and registers a new WebView.
about_to_wait() drains app_rx: creates WebViews for Launch
commands, removes and clears active_session for Stop commands.
active_webview() returns the active-session WebView when one
exists, falling back to the system-ui WebView. Rendering,
keyboard, and mouse events all route through active_webview().
Resize propagates to both the system WebView and all app WebViews.
run() creates the mpsc channel and spawns the appd listener
before entering the winit event loop.
2026-03-11 16:59:12 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Some("APP_STATE") if v["state"].as_str() == Some("stopped") => {
|
|
|
|
|
let Some(session_id) = v["session_id"].as_u64() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
if tx.try_send(AppdCmd::Stop { session_id }).is_ok() {
|
|
|
|
|
wake();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|