feat: power reference site with in-process WIP WASM

Add the public-document Worldspace, Rust Client/Host bridge, and WIP-backed tree and document rendering while preserving static fallback and navigation.

Pin the Rust source and build tools, verify resource and trap boundaries, and cover dev and preview loading including same-origin generated module URLs.
This commit is contained in:
2026-10-16 05:59:46 +09:00
parent ad08037f4f
commit c480b67301
26 changed files with 2719 additions and 63 deletions
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "wip-browser"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
publish = false
[features]
test-probes = []
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wip-client = { path = "../../../wip-rs/crates/wip-client", default-features = false }
wip-protocol = { path = "../../../wip-rs/crates/wip-protocol" }
wip-site-worldspace = { path = "../worldspace" }
serde_json = "1"
wasm-bindgen = "=0.2.121"
+212
View File
@@ -0,0 +1,212 @@
//! Browser-owned Client/Host session. JS conversion is outside the direct WIP exchange.
use std::collections::BTreeMap;
use serde_json::{Value as Json, json};
use wasm_bindgen::prelude::*;
use wip_client::direct::{Client, Request, Response};
use wip_client::{ClientLimits, SecurityContext, SessionId};
use wip_protocol::{InterfaceReference, Value};
use wip_site_worldspace::{Worldspace, WorldspaceData};
/// One isolated in-memory Worldspace and its stateful WIP client.
#[wasm_bindgen]
pub struct BrowserSession {
world: Worldspace,
client: Client,
session: SessionId,
display_paths: BTreeMap<String, String>,
observes: u32,
interfaces: u32,
calls: u32,
}
fn error(value: impl std::fmt::Display) -> JsValue {
JsValue::from_str(&value.to_string())
}
fn value_json(value: Value) -> Json {
match value {
Value::Unit => Json::Null,
Value::Boolean(value) => json!(value),
Value::Integer(value) => json!(value),
Value::Number(value) => json!(value),
Value::String(value) => json!(value),
Value::Bytes(value) => json!(value),
Value::Record(value) => Json::Object(
value
.into_iter()
.map(|(key, value)| (key, value_json(value)))
.collect(),
),
Value::List(value) => Json::Array(value.into_iter().map(value_json).collect()),
}
}
#[wasm_bindgen]
impl BrowserSession {
/// Creates a fresh local view. The supplied manifest is build-generated public content.
#[wasm_bindgen(constructor)]
pub fn new(manifest: &str) -> Result<BrowserSession, JsValue> {
let world = Worldspace::from_json(manifest).map_err(error)?;
let data: WorldspaceData = serde_json::from_str(manifest).map_err(error)?;
let display_paths = data
.nodes
.into_iter()
.map(|node| (node.path, node.display_path))
.collect();
let limits = ClientLimits::new(1, 4096, 64, 32, 64).map_err(error)?;
let mut client = Client::new(limits);
let session = client
.open_session("reference-site", SecurityContext::new("public"))
.map_err(error)?;
Ok(Self {
world,
client,
session,
display_paths,
observes: 0,
interfaces: 0,
calls: 0,
})
}
/// Returns a tree projected from actual client observations and document operations.
pub fn tree(&mut self) -> Result<String, JsValue> {
self.observe_path("/", 32)?;
let mut paths = Vec::new();
let mut pending = vec!["/".to_owned()];
while let Some(path) = pending.pop() {
if let Some(tree) = self.client.tree(&self.session, &path) {
pending.extend(tree.children.iter().rev().cloned());
}
paths.push(path);
}
let mut rows = Vec::new();
for path in paths {
let mut row = self.read_document(&path)?;
let object = self
.client
.object(&self.session, &path)
.and_then(|entry| entry.object)
.ok_or_else(|| error("missing client object"))?;
let fields = row
.as_object_mut()
.ok_or_else(|| error("invalid document result"))?;
fields.remove("html");
fields.insert("name".into(), json!(object.name));
if path != "/" {
let parent = path
.rsplit_once('/')
.map(|(parent, _)| if parent.is_empty() { "/" } else { parent })
.unwrap_or("/");
fields.insert("parent".into(), json!(parent));
}
rows.push(row);
}
Ok(Json::Array(rows).to_string())
}
/// Reads a document through observe, descriptor fetch and validated Host dispatch.
pub fn read(&mut self, path: &str) -> Result<String, JsValue> {
Ok(self.read_document(path)?.to_string())
}
/// Calls a zero-argument operation on the site's document interface.
/// Unknown names are rejected by the same Client descriptor validation as reads.
pub fn call(&mut self, path: &str, operation: &str) -> Result<String, JsValue> {
Ok(self.call_document(path, operation)?.to_string())
}
/// Deliberate abort probe, compiled only into the separately served test artifact.
#[cfg(feature = "test-probes")]
pub fn panic_probe(&mut self) {
panic!("controlled release-WASM panic probe");
}
/// Attempted exchange counters aid integration diagnostics without exposing Host state.
pub fn stats(&self) -> String {
json!({"observes": self.observes, "interfaces": self.interfaces, "calls": self.calls})
.to_string()
}
}
impl BrowserSession {
fn exchange(&mut self, request: wip_client::direct::PreparedRequest) -> Result<(), JsValue> {
let id = request.id;
let response = match request.request {
Request::Observe(request) => {
self.observes += 1;
Response::Observe(self.world.observe(request))
}
Request::FetchInterface(request) => {
self.interfaces += 1;
Response::FetchInterface(self.world.fetch_interface(request))
}
Request::CallOperation(request) => {
self.client.mark_dispatched(id).map_err(error)?;
self.calls += 1;
Response::CallOperation(self.world.call_operation(request))
}
};
let completion = self.client.complete(id, response).map_err(error)?;
if let wip_client::Completion::ProtocolFailure(failure) = completion {
return Err(error(failure));
}
Ok(())
}
fn observe_path(&mut self, path: &str, depth: u32) -> Result<(), JsValue> {
if let Some(request) = self
.client
.ensure_observed(&self.session, path, depth)
.map_err(error)?
{
self.exchange(request)?;
}
Ok(())
}
fn read_document(&mut self, path: &str) -> Result<Json, JsValue> {
self.call_document(path, "read")
}
fn call_document(&mut self, path: &str, operation: &str) -> Result<Json, JsValue> {
self.observe_path(path, 0)?;
let reference = InterfaceReference {
scope: "/".into(),
name: "document".into(),
};
if let Some(request) = self
.client
.ensure_interface(&self.session, reference.clone())
.map_err(error)?
{
self.exchange(request)?;
}
let request = self
.client
.prepare_call(&self.session, path, &reference, operation, BTreeMap::new())
.map_err(error)?;
self.exchange(request)?;
let result = self
.client
.call_history(&self.session)
.and_then(|history| history.back())
.ok_or_else(|| error("missing call result"))?;
match &result.outcome {
wip_client::CallOutcome::Success(response) => {
let mut value = value_json(response.result.clone());
let display_path = self
.display_paths
.get(path)
.ok_or_else(|| error("missing display path"))?;
value
.as_object_mut()
.ok_or_else(|| error("invalid document result"))?
.insert("displayPath".into(), json!(display_path));
Ok(value)
}
outcome => Err(error(format!("document read failed: {outcome:?}"))),
}
}
}