use std::sync::Arc; use wasm_bindgen::prelude::*; use swactor::actor::{ActorAddress, ActorExited, ActorInterface}; use swactor::runtime::{Ctx, Inbox, Runtime, RuntimeConfig}; use swactor_std::{CtxGroups, CtxWatching, RuntimeNaming, RuntimeGroups, StdExtension}; // ─── 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, } #[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 { self.inner.try_recv() } } /// Inbox that receives byte arrays from actors. #[wasm_bindgen] pub struct WasmInboxBytes { inner: Inbox>, } #[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> { self.inner.try_recv() } } /// Inbox that receives string values (used for death notifications, etc.). #[wasm_bindgen] pub struct WasmInboxString { inner: Inbox, } #[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 { self.inner.try_recv() } } // ─── Runtime ──────────────────────────────────────────────────────────────── /// The browser-facing swactor runtime. /// /// Wraps `swactor::Runtime` in single-threaded mode 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, } #[wasm_bindgen] impl WasmRuntime { #[wasm_bindgen(constructor)] pub fn new() -> Self { let rt = Runtime::new(RuntimeConfig { num_threads: 1, ..RuntimeConfig::default() }) .with_extension(Arc::new(StdExtension::new())); Self { rt } } /// Drive one tick of the runtime. pub fn tick(&self) { self.rt.tick(); } /// 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 { 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 { 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::() as f64 } /// Total panics across all workers. pub fn total_panics(&self) -> f64 { self.rt.stats().workers.iter().map(|w| w.panics).sum::() 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) }