swactor/crates/bindings/wasm-runtime/src/lib.rs

386 lines
11 KiB
Rust
Raw Normal View History

use std::sync::Arc;
use wasm_bindgen::prelude::*;
use swactor::actor::{ActorAddress, ActorExited, ActorInterface};
feat(myelin): enforce actor-owned control flow Architecture enforcement: - Install a repository-owned rustc wrapper for ordinary cargo check, build, and test commands. Resolve compiler item identities so renamed imports and helper wrappers cannot hide spawning, timing, blocking, polling, thread, or runtime-driving capabilities. - Define the execution-owner crates and reject dependencies from those substrates back into Myelin policy. Add compile-pass and compile-fail contracts for actor helpers, execution owners, test waits, forbidden capabilities, suppression attempts, and owner dependency inversions. Execution ownership: - Add engine-owned actor timers with cancellation and generation identity, then migrate lifecycle deadlines and protocol ticks off application tasks. Keep networking, process output, telemetry, and blocking provider calls in their approved I/O substrates. - Move process spawn, wait, signal, Unix listener, and output-following mechanics into swactor-process. Isolate Vast.ai blocking HTTP mechanics behind its adapter while actors retain retry, recovery, and provisioning decisions. Myelin control flow: - Rework manual control, worker lifecycle, provisioning, provider recovery, job deployment, distribution, edge orchestration, and shutdown as actor state transitions and typed effects. Preserve durable provider adoption and command outcomes across graceful and abrupt restarts. - Replace controller loops and timer-forwarding tasks with actor messages; leave substrate tasks as cancellable observation streams with no durable policy state. Properties and resource ownership: - Add deterministic engine and component properties, a stateful mock-VastAI lifecycle model, persisted regression cases, controlled fault injection, and a bounded nightly workflow covering restart and teardown behavior. - Terminate reply observers, cancel telemetry collectors, bound dashboard projections, and release child observers, file descriptors, process records, and inode-verified Unix sockets on every terminal path. Verified with the compiler-policy contracts, 105 Myelin library tests, 32 swactor-process tests, telemetry cancellation contracts, randomized stateful restart cases, cargo check, and formatting checks.
2026-08-19 21:38:14 +00:00
use swactor::runtime::{Ctx, Inbox, Runtime, RuntimeConfig, RuntimeParts};
use swactor::std::{CtxGroups, CtxWatching, RuntimeGroups, RuntimeNaming, StdExtension};
feat(myelin): enforce actor-owned control flow Architecture enforcement: - Install a repository-owned rustc wrapper for ordinary cargo check, build, and test commands. Resolve compiler item identities so renamed imports and helper wrappers cannot hide spawning, timing, blocking, polling, thread, or runtime-driving capabilities. - Define the execution-owner crates and reject dependencies from those substrates back into Myelin policy. Add compile-pass and compile-fail contracts for actor helpers, execution owners, test waits, forbidden capabilities, suppression attempts, and owner dependency inversions. Execution ownership: - Add engine-owned actor timers with cancellation and generation identity, then migrate lifecycle deadlines and protocol ticks off application tasks. Keep networking, process output, telemetry, and blocking provider calls in their approved I/O substrates. - Move process spawn, wait, signal, Unix listener, and output-following mechanics into swactor-process. Isolate Vast.ai blocking HTTP mechanics behind its adapter while actors retain retry, recovery, and provisioning decisions. Myelin control flow: - Rework manual control, worker lifecycle, provisioning, provider recovery, job deployment, distribution, edge orchestration, and shutdown as actor state transitions and typed effects. Preserve durable provider adoption and command outcomes across graceful and abrupt restarts. - Replace controller loops and timer-forwarding tasks with actor messages; leave substrate tasks as cancellable observation streams with no durable policy state. Properties and resource ownership: - Add deterministic engine and component properties, a stateful mock-VastAI lifecycle model, persisted regression cases, controlled fault injection, and a bounded nightly workflow covering restart and teardown behavior. - Terminate reply observers, cancel telemetry collectors, bound dashboard projections, and release child observers, file descriptors, process records, and inode-verified Unix sockets on every terminal path. Verified with the compiler-policy contracts, 105 Myelin library tests, 32 swactor-process tests, telemetry cancellation contracts, randomized stateful restart cases, cargo check, and formatting checks.
2026-08-19 21:38:14 +00:00
use swactor_engine::{Engine, SteppingBackend};
// ─── Core JS-facing types ───────────────────────────────────────────────────
/// Opaque actor address handle for JavaScript.
///
/// Returned by spawn functions, passed to send functions. JS never sees
/// the raw 32-byte address — it just holds and forwards this handle.
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmAddr(ActorAddress);
#[wasm_bindgen]
impl WasmAddr {
/// Debug representation of the address (first 8 hex bytes + ellipsis).
#[wasm_bindgen(js_name = toString)]
pub fn to_js_string(&self) -> String {
format!("{}", self.0)
}
}
impl WasmAddr {
/// Access the inner address from Rust (not exposed to JS).
pub fn inner(&self) -> ActorAddress {
self.0
}
}
/// Inbox that receives `u32` values from actors.
#[wasm_bindgen]
pub struct WasmInboxU32 {
inner: Inbox<u32>,
}
#[wasm_bindgen]
impl WasmInboxU32 {
/// The address actors should send results to.
pub fn addr(&self) -> WasmAddr {
WasmAddr(*self.inner.addr())
}
/// Poll for the next value. Returns `undefined` when empty.
pub fn try_recv(&self) -> Option<u32> {
self.inner.try_recv()
}
}
/// Inbox that receives byte arrays from actors.
#[wasm_bindgen]
pub struct WasmInboxBytes {
inner: Inbox<Vec<u8>>,
}
#[wasm_bindgen]
impl WasmInboxBytes {
pub fn addr(&self) -> WasmAddr {
WasmAddr(*self.inner.addr())
}
/// Poll for the next byte array. Returns `undefined` when empty.
pub fn try_recv(&self) -> Option<Vec<u8>> {
self.inner.try_recv()
}
}
/// Inbox that receives string values (used for death notifications, etc.).
#[wasm_bindgen]
pub struct WasmInboxString {
inner: Inbox<String>,
}
#[wasm_bindgen]
impl WasmInboxString {
pub fn addr(&self) -> WasmAddr {
WasmAddr(*self.inner.addr())
}
/// Poll for the next string. Returns `undefined` when empty.
pub fn try_recv(&self) -> Option<String> {
self.inner.try_recv()
}
}
// ─── Runtime ────────────────────────────────────────────────────────────────
/// The browser-facing swactor runtime.
///
feat(myelin): enforce actor-owned control flow Architecture enforcement: - Install a repository-owned rustc wrapper for ordinary cargo check, build, and test commands. Resolve compiler item identities so renamed imports and helper wrappers cannot hide spawning, timing, blocking, polling, thread, or runtime-driving capabilities. - Define the execution-owner crates and reject dependencies from those substrates back into Myelin policy. Add compile-pass and compile-fail contracts for actor helpers, execution owners, test waits, forbidden capabilities, suppression attempts, and owner dependency inversions. Execution ownership: - Add engine-owned actor timers with cancellation and generation identity, then migrate lifecycle deadlines and protocol ticks off application tasks. Keep networking, process output, telemetry, and blocking provider calls in their approved I/O substrates. - Move process spawn, wait, signal, Unix listener, and output-following mechanics into swactor-process. Isolate Vast.ai blocking HTTP mechanics behind its adapter while actors retain retry, recovery, and provisioning decisions. Myelin control flow: - Rework manual control, worker lifecycle, provisioning, provider recovery, job deployment, distribution, edge orchestration, and shutdown as actor state transitions and typed effects. Preserve durable provider adoption and command outcomes across graceful and abrupt restarts. - Replace controller loops and timer-forwarding tasks with actor messages; leave substrate tasks as cancellable observation streams with no durable policy state. Properties and resource ownership: - Add deterministic engine and component properties, a stateful mock-VastAI lifecycle model, persisted regression cases, controlled fault injection, and a bounded nightly workflow covering restart and teardown behavior. - Terminate reply observers, cancel telemetry collectors, bound dashboard projections, and release child observers, file descriptors, process records, and inode-verified Unix sockets on every terminal path. Verified with the compiler-policy contracts, 105 Myelin library tests, 32 swactor-process tests, telemetry cancellation contracts, randomized stateful restart cases, cargo check, and formatting checks.
2026-08-19 21:38:14 +00:00
/// Owns a cloneable `swactor::Runtime` handle plus an engine-backed stepping
/// substrate, with StdExtension installed (naming, monitoring, groups).
/// Actors are spawned via dedicated spawn functions (one per actor type). The
/// runtime is driven by calling `tick()`.
#[wasm_bindgen]
pub struct WasmRuntime {
rt: Runtime,
feat(myelin): enforce actor-owned control flow Architecture enforcement: - Install a repository-owned rustc wrapper for ordinary cargo check, build, and test commands. Resolve compiler item identities so renamed imports and helper wrappers cannot hide spawning, timing, blocking, polling, thread, or runtime-driving capabilities. - Define the execution-owner crates and reject dependencies from those substrates back into Myelin policy. Add compile-pass and compile-fail contracts for actor helpers, execution owners, test waits, forbidden capabilities, suppression attempts, and owner dependency inversions. Execution ownership: - Add engine-owned actor timers with cancellation and generation identity, then migrate lifecycle deadlines and protocol ticks off application tasks. Keep networking, process output, telemetry, and blocking provider calls in their approved I/O substrates. - Move process spawn, wait, signal, Unix listener, and output-following mechanics into swactor-process. Isolate Vast.ai blocking HTTP mechanics behind its adapter while actors retain retry, recovery, and provisioning decisions. Myelin control flow: - Rework manual control, worker lifecycle, provisioning, provider recovery, job deployment, distribution, edge orchestration, and shutdown as actor state transitions and typed effects. Preserve durable provider adoption and command outcomes across graceful and abrupt restarts. - Replace controller loops and timer-forwarding tasks with actor messages; leave substrate tasks as cancellable observation streams with no durable policy state. Properties and resource ownership: - Add deterministic engine and component properties, a stateful mock-VastAI lifecycle model, persisted regression cases, controlled fault injection, and a bounded nightly workflow covering restart and teardown behavior. - Terminate reply observers, cancel telemetry collectors, bound dashboard projections, and release child observers, file descriptors, process records, and inode-verified Unix sockets on every terminal path. Verified with the compiler-policy contracts, 105 Myelin library tests, 32 swactor-process tests, telemetry cancellation contracts, randomized stateful restart cases, cargo check, and formatting checks.
2026-08-19 21:38:14 +00:00
_engine: Engine,
backend: SteppingBackend,
}
#[wasm_bindgen]
impl WasmRuntime {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
let parts = RuntimeParts::new(RuntimeConfig::default())
.with_extension(Arc::new(StdExtension::new()));
let rt = parts.runtime().clone();
feat(myelin): enforce actor-owned control flow Architecture enforcement: - Install a repository-owned rustc wrapper for ordinary cargo check, build, and test commands. Resolve compiler item identities so renamed imports and helper wrappers cannot hide spawning, timing, blocking, polling, thread, or runtime-driving capabilities. - Define the execution-owner crates and reject dependencies from those substrates back into Myelin policy. Add compile-pass and compile-fail contracts for actor helpers, execution owners, test waits, forbidden capabilities, suppression attempts, and owner dependency inversions. Execution ownership: - Add engine-owned actor timers with cancellation and generation identity, then migrate lifecycle deadlines and protocol ticks off application tasks. Keep networking, process output, telemetry, and blocking provider calls in their approved I/O substrates. - Move process spawn, wait, signal, Unix listener, and output-following mechanics into swactor-process. Isolate Vast.ai blocking HTTP mechanics behind its adapter while actors retain retry, recovery, and provisioning decisions. Myelin control flow: - Rework manual control, worker lifecycle, provisioning, provider recovery, job deployment, distribution, edge orchestration, and shutdown as actor state transitions and typed effects. Preserve durable provider adoption and command outcomes across graceful and abrupt restarts. - Replace controller loops and timer-forwarding tasks with actor messages; leave substrate tasks as cancellable observation streams with no durable policy state. Properties and resource ownership: - Add deterministic engine and component properties, a stateful mock-VastAI lifecycle model, persisted regression cases, controlled fault injection, and a bounded nightly workflow covering restart and teardown behavior. - Terminate reply observers, cancel telemetry collectors, bound dashboard projections, and release child observers, file descriptors, process records, and inode-verified Unix sockets on every terminal path. Verified with the compiler-policy contracts, 105 Myelin library tests, 32 swactor-process tests, telemetry cancellation contracts, randomized stateful restart cases, cargo check, and formatting checks.
2026-08-19 21:38:14 +00:00
let backend = SteppingBackend::new();
let engine = Engine::new(parts, backend.clone()).expect("create wasm actor engine");
Self {
rt,
_engine: engine,
backend,
}
}
/// Drive one tick of the runtime.
pub fn tick(&mut self) {
feat(myelin): enforce actor-owned control flow Architecture enforcement: - Install a repository-owned rustc wrapper for ordinary cargo check, build, and test commands. Resolve compiler item identities so renamed imports and helper wrappers cannot hide spawning, timing, blocking, polling, thread, or runtime-driving capabilities. - Define the execution-owner crates and reject dependencies from those substrates back into Myelin policy. Add compile-pass and compile-fail contracts for actor helpers, execution owners, test waits, forbidden capabilities, suppression attempts, and owner dependency inversions. Execution ownership: - Add engine-owned actor timers with cancellation and generation identity, then migrate lifecycle deadlines and protocol ticks off application tasks. Keep networking, process output, telemetry, and blocking provider calls in their approved I/O substrates. - Move process spawn, wait, signal, Unix listener, and output-following mechanics into swactor-process. Isolate Vast.ai blocking HTTP mechanics behind its adapter while actors retain retry, recovery, and provisioning decisions. Myelin control flow: - Rework manual control, worker lifecycle, provisioning, provider recovery, job deployment, distribution, edge orchestration, and shutdown as actor state transitions and typed effects. Preserve durable provider adoption and command outcomes across graceful and abrupt restarts. - Replace controller loops and timer-forwarding tasks with actor messages; leave substrate tasks as cancellable observation streams with no durable policy state. Properties and resource ownership: - Add deterministic engine and component properties, a stateful mock-VastAI lifecycle model, persisted regression cases, controlled fault injection, and a bounded nightly workflow covering restart and teardown behavior. - Terminate reply observers, cancel telemetry collectors, bound dashboard projections, and release child observers, file descriptors, process records, and inode-verified Unix sockets on every terminal path. Verified with the compiler-policy contracts, 105 Myelin library tests, 32 swactor-process tests, telemetry cancellation contracts, randomized stateful restart cases, cargo check, and formatting checks.
2026-08-19 21:38:14 +00:00
self.backend.step();
}
/// Number of actors currently alive.
pub fn actor_count(&self) -> usize {
self.rt.stats().actors.len()
}
/// Create an inbox that receives u32 values.
pub fn new_inbox_u32(&self) -> WasmInboxU32 {
WasmInboxU32 {
inner: self.rt.new_inbox().expect("new_inbox_u32"),
}
}
/// Create an inbox that receives byte arrays.
pub fn new_inbox_bytes(&self) -> WasmInboxBytes {
WasmInboxBytes {
inner: self.rt.new_inbox().expect("new_inbox_bytes"),
}
}
/// Create an inbox that receives strings.
pub fn new_inbox_string(&self) -> WasmInboxString {
WasmInboxString {
inner: self.rt.new_inbox().expect("new_inbox_string"),
}
}
/// Send a u32 to an actor. Returns false if the address is invalid.
pub fn send_u32(&self, addr: &WasmAddr, value: u32) -> bool {
self.rt.send_to(addr.0, value).is_ok()
}
/// Send a byte array to an actor. Returns false if the address is invalid.
pub fn send_bytes(&self, addr: &WasmAddr, data: &[u8]) -> bool {
self.rt.send_to(addr.0, data.to_vec()).is_ok()
}
/// Stop an actor gracefully.
pub fn stop_actor(&self, addr: &WasmAddr) -> bool {
self.rt.stop_actor(addr.0).is_ok()
}
/// Runtime uptime in milliseconds.
pub fn uptime_ms(&self) -> f64 {
self.rt.stats().uptime_ms as f64
}
// ─── Naming ─────────────────────────────────────────────────────────────
/// Register a name for an actor address. Returns false if the name is taken.
pub fn register_name(&self, name: &str, addr: &WasmAddr) -> bool {
self.rt.register_name(name.to_string(), addr.0).is_ok()
}
/// Look up an actor address by name. Returns undefined if not found.
pub fn where_is(&self, name: &str) -> Option<WasmAddr> {
self.rt.where_is(name).map(WasmAddr)
}
/// Unregister a name. Returns the address it was bound to, or undefined.
pub fn unregister_name(&self, name: &str) -> Option<WasmAddr> {
self.rt.unregister(name).map(WasmAddr)
}
/// Return all registered actor names as a comma-separated string.
pub fn registered_names(&self) -> String {
self.rt.registered_names().join(",")
}
// ─── Groups ─────────────────────────────────────────────────────────────
/// Add an actor to a named group.
pub fn join_group(&self, addr: &WasmAddr, group: &str) {
self.rt.join_group(addr.0, group.to_string());
}
/// Remove an actor from a named group.
pub fn leave_group(&self, addr: &WasmAddr, group: &str) {
self.rt.leave_group(addr.0, group);
}
/// Broadcast a u32 message to all members of a group. Returns count sent.
pub fn publish_to_group_u32(&self, group: &str, msg: u32) -> usize {
self.rt.publish_to(group, msg)
}
/// Number of actors in a group.
pub fn group_member_count(&self, group: &str) -> usize {
self.rt.group_members(group).len()
}
/// Return all group names as a comma-separated string.
pub fn group_names(&self) -> String {
self.rt.groups().join(",")
}
// ─── Stats ──────────────────────────────────────────────────────────────
/// Total messages processed across all workers.
pub fn total_messages(&self) -> f64 {
self.rt
.stats()
.workers
.iter()
.map(|w| w.messages_processed)
.sum::<u64>() as f64
}
/// Total panics across all workers.
pub fn total_panics(&self) -> f64 {
self.rt
.stats()
.workers
.iter()
.map(|w| w.panics)
.sum::<u64>() as f64
}
}
impl WasmRuntime {
/// Access the inner Runtime from Rust (for custom spawn functions).
pub fn runtime(&self) -> &Runtime {
&self.rt
}
}
// ─── Demo actors ────────────────────────────────────────────────────────────
//
// These demonstrate the pattern for exposing actors to JavaScript.
// Each actor type gets a `spawn_*` function that returns a WasmAddr.
struct Counter {
total: u32,
report_to: ActorAddress,
}
impl ActorInterface for Counter {
type Incoming = u32;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: u32) {
self.total += msg;
let _ = ctx.send(self.report_to, self.total);
}
}
struct Relay {
target: ActorAddress,
}
impl ActorInterface for Relay {
type Incoming = u32;
type Response = ();
fn handle(&mut self, ctx: &Ctx, msg: u32) {
let _ = ctx.send(self.target, msg);
}
}
/// A sentinel actor that watches a target and reports its death to an inbox.
///
/// Uses the std monitoring extension (CtxMonitoring::monitor). When the target
/// dies, the sentinel receives a `Down` message and sends the dead actor's
/// string representation to the report inbox, then stops itself.
struct Sentinel {
target: ActorAddress,
report_to: ActorAddress,
}
impl ActorInterface for Sentinel {
type Incoming = ();
type Response = ();
fn handle(&mut self, _ctx: &Ctx, _msg: ()) {}
fn on_start(&mut self, ctx: &Ctx) {
ctx.watch(self.target);
}
fn on_actor_exit(&mut self, ctx: &Ctx, exited: ActorExited) {
let msg = format!("{}:{:?}", exited.addr, exited.reason);
let _ = ctx.send(self.report_to, msg);
ctx.stop_self();
}
}
/// A group member that joins a named group and forwards u32 messages to a report inbox.
struct GroupMember {
group: String,
report_to: ActorAddress,
}
impl ActorInterface for GroupMember {
type Incoming = u32;
type Response = ();
fn on_start(&mut self, ctx: &Ctx) {
ctx.join_group(self.group.clone());
}
fn handle(&mut self, ctx: &Ctx, msg: u32) {
let _ = ctx.send(self.report_to, msg);
}
}
/// Spawn a counter that accumulates u32 values and reports running totals
/// to the given inbox address.
#[wasm_bindgen]
pub fn spawn_counter(rt: &WasmRuntime, report_to: &WasmAddr) -> WasmAddr {
let addr = rt
.rt
.spawn(Counter {
total: 0,
report_to: report_to.0,
})
.expect("spawn counter");
WasmAddr(addr)
}
/// Spawn a relay that forwards every u32 message to the target actor.
#[wasm_bindgen]
pub fn spawn_relay(rt: &WasmRuntime, target: &WasmAddr) -> WasmAddr {
let addr = rt
.rt
.spawn(Relay { target: target.0 })
.expect("spawn relay");
WasmAddr(addr)
}
/// Spawn a sentinel that watches a target actor and reports its death
/// to the given string inbox.
#[wasm_bindgen]
pub fn spawn_sentinel(
rt: &WasmRuntime,
target: &WasmAddr,
report_to: &WasmInboxString,
) -> WasmAddr {
let addr = rt
.rt
.spawn(Sentinel {
target: target.0,
report_to: *report_to.inner.addr(),
})
.expect("spawn sentinel");
WasmAddr(addr)
}
/// Spawn a group member that joins the given group and forwards u32 messages
/// to the report inbox.
#[wasm_bindgen]
pub fn spawn_group_member(rt: &WasmRuntime, group: &str, report_to: &WasmAddr) -> WasmAddr {
let addr = rt
.rt
.spawn(GroupMember {
group: group.to_string(),
report_to: report_to.0,
})
.expect("spawn group_member");
WasmAddr(addr)
}