feat: generic browser runtime API with typed handles (Stage 2)
Evolved crates/wasm/ from hardcoded PoC to a generic browser runtime: - WasmRuntime: wraps Runtime, provides tick/send/stop/stats/inbox creation - WasmAddr: opaque actor address handle for JS (replaces fragile indices) - WasmInboxU32, WasmInboxBytes: typed inboxes for receiving actor results - Free-standing spawn_counter/spawn_relay demonstrate the actor pattern - 10 Node.js tests pass (accumulator, relay, multi-counter, stop, bytes) Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
parent
2649a54feb
commit
82e72ff235
3 changed files with 317 additions and 106 deletions
|
|
@ -3,9 +3,148 @@ use wasm_bindgen::prelude::*;
|
||||||
use swactor::actor::{ActorAddress, ActorInterface};
|
use swactor::actor::{ActorAddress, ActorInterface};
|
||||||
use swactor::runtime::{Ctx, Inbox, Runtime, RuntimeConfig};
|
use swactor::runtime::{Ctx, Inbox, Runtime, RuntimeConfig};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ─── Core JS-facing types ───────────────────────────────────────────────────
|
||||||
// Actors (private — only exposed through the wasm API)
|
|
||||||
// ---------------------------------------------------------------------------
|
/// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Runtime ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// The browser-facing swactor runtime.
|
||||||
|
///
|
||||||
|
/// Wraps `swactor::Runtime` in single-threaded mode. Actors are spawned via
|
||||||
|
/// dedicated spawn functions (one per actor type). The runtime is driven by
|
||||||
|
/// calling `tick()` — either manually or from a `setTimeout(0)` loop.
|
||||||
|
#[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()
|
||||||
|
});
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
struct Counter {
|
||||||
total: u32,
|
total: u32,
|
||||||
|
|
@ -35,79 +174,26 @@ impl ActorInterface for Relay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
/// Spawn a counter that accumulates u32 values and reports running totals
|
||||||
// JS-facing runtime wrapper
|
/// to the given inbox address.
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct SwactorRuntime {
|
pub fn spawn_counter(rt: &WasmRuntime, report_to: &WasmAddr) -> WasmAddr {
|
||||||
rt: Runtime,
|
let addr = rt
|
||||||
inbox: Inbox<u32>,
|
|
||||||
actors: Vec<ActorAddress>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
impl SwactorRuntime {
|
|
||||||
#[wasm_bindgen(constructor)]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let rt = Runtime::new(RuntimeConfig {
|
|
||||||
num_threads: 1,
|
|
||||||
..RuntimeConfig::default()
|
|
||||||
});
|
|
||||||
let inbox = rt.new_inbox().unwrap();
|
|
||||||
Self {
|
|
||||||
rt,
|
|
||||||
inbox,
|
|
||||||
actors: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawn a counter actor. Returns its index (used with `send`).
|
|
||||||
pub fn spawn_counter(&mut self) -> usize {
|
|
||||||
let addr = self
|
|
||||||
.rt
|
.rt
|
||||||
.spawn(Counter {
|
.spawn(Counter {
|
||||||
total: 0,
|
total: 0,
|
||||||
report_to: *self.inbox.addr(),
|
report_to: report_to.0,
|
||||||
})
|
})
|
||||||
.expect("spawn counter");
|
.expect("spawn counter");
|
||||||
let idx = self.actors.len();
|
WasmAddr(addr)
|
||||||
self.actors.push(addr);
|
|
||||||
idx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a relay that forwards every message to `target_idx`.
|
/// Spawn a relay that forwards every u32 message to the target actor.
|
||||||
pub fn spawn_relay(&mut self, target_idx: usize) -> usize {
|
#[wasm_bindgen]
|
||||||
let target = self.actors[target_idx];
|
pub fn spawn_relay(rt: &WasmRuntime, target: &WasmAddr) -> WasmAddr {
|
||||||
let addr = self
|
let addr = rt
|
||||||
.rt
|
.rt
|
||||||
.spawn(Relay { target })
|
.spawn(Relay { target: target.0 })
|
||||||
.expect("spawn relay");
|
.expect("spawn relay");
|
||||||
let idx = self.actors.len();
|
WasmAddr(addr)
|
||||||
self.actors.push(addr);
|
|
||||||
idx
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a u32 to the actor at `actor_idx`.
|
|
||||||
pub fn send(&self, actor_idx: usize, value: u32) -> bool {
|
|
||||||
if actor_idx >= self.actors.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self.rt.send_to(self.actors[actor_idx], value).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drive one tick of the single-threaded runtime.
|
|
||||||
pub fn tick(&self) {
|
|
||||||
self.rt.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to read the next result from the inbox. Returns `undefined` when empty.
|
|
||||||
pub fn try_recv(&self) -> Option<u32> {
|
|
||||||
self.inbox.try_recv()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Number of actors the runtime knows about.
|
|
||||||
pub fn actor_count(&self) -> usize {
|
|
||||||
self.rt.stats().actors.len()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import { SwactorRuntime } from "./pkg/swactor_wasm.js";
|
import {
|
||||||
|
WasmRuntime,
|
||||||
|
WasmAddr,
|
||||||
|
spawn_counter,
|
||||||
|
spawn_relay,
|
||||||
|
} from "./pkg/wasm.js";
|
||||||
|
|
||||||
let passed = 0;
|
let passed = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
|
@ -23,77 +28,128 @@ function assertEq(a, b, msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drain(rt) {
|
function drainInbox(inbox) {
|
||||||
const results = [];
|
const results = [];
|
||||||
let v;
|
let v;
|
||||||
while ((v = rt.try_recv()) !== undefined) results.push(v);
|
while ((v = inbox.try_recv()) !== undefined) results.push(v);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- accumulator ----------------------------------------------------------
|
// ---- accumulator ----------------------------------------------------------
|
||||||
{
|
{
|
||||||
console.log("test: accumulator processes messages");
|
console.log("test: accumulator processes messages via WasmAddr");
|
||||||
const rt = new SwactorRuntime();
|
const rt = new WasmRuntime();
|
||||||
const c = rt.spawn_counter();
|
const inbox = rt.new_inbox_u32();
|
||||||
rt.send(c, 1);
|
const c = spawn_counter(rt, inbox.addr());
|
||||||
rt.send(c, 2);
|
rt.send_u32(c, 1);
|
||||||
rt.send(c, 10);
|
rt.send_u32(c, 2);
|
||||||
|
rt.send_u32(c, 10);
|
||||||
rt.tick();
|
rt.tick();
|
||||||
assertEq(drain(rt), [1, 3, 13], "running totals");
|
assertEq(drainInbox(inbox), [1, 3, 13], "running totals");
|
||||||
|
inbox.free();
|
||||||
rt.free();
|
rt.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- relay ----------------------------------------------------------------
|
// ---- relay ----------------------------------------------------------------
|
||||||
{
|
{
|
||||||
console.log("test: relay forwards to counter");
|
console.log("test: relay forwards to counter");
|
||||||
const rt = new SwactorRuntime();
|
const rt = new WasmRuntime();
|
||||||
const c = rt.spawn_counter();
|
const inbox = rt.new_inbox_u32();
|
||||||
const r = rt.spawn_relay(c);
|
const c = spawn_counter(rt, inbox.addr());
|
||||||
rt.send(r, 5);
|
const r = spawn_relay(rt, c);
|
||||||
rt.send(r, 7);
|
rt.send_u32(r, 5);
|
||||||
// tick 1: relay receives and forwards (cross-actor, same worker → pending_local)
|
rt.send_u32(r, 7);
|
||||||
|
// tick 1: relay receives and forwards (same worker → pending_local)
|
||||||
// tick 2: counter receives forwarded messages
|
// tick 2: counter receives forwarded messages
|
||||||
rt.tick();
|
rt.tick();
|
||||||
rt.tick();
|
rt.tick();
|
||||||
assertEq(drain(rt), [5, 12], "relayed totals");
|
assertEq(drainInbox(inbox), [5, 12], "relayed totals");
|
||||||
|
inbox.free();
|
||||||
rt.free();
|
rt.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- multiple counters ----------------------------------------------------
|
// ---- multiple counters ----------------------------------------------------
|
||||||
{
|
{
|
||||||
console.log("test: multiple independent counters");
|
console.log("test: multiple independent counters");
|
||||||
const rt = new SwactorRuntime();
|
const rt = new WasmRuntime();
|
||||||
const a = rt.spawn_counter();
|
const inbox = rt.new_inbox_u32();
|
||||||
const b = rt.spawn_counter();
|
const a = spawn_counter(rt, inbox.addr());
|
||||||
rt.send(a, 10);
|
const b = spawn_counter(rt, inbox.addr());
|
||||||
rt.send(b, 100);
|
rt.send_u32(a, 10);
|
||||||
|
rt.send_u32(b, 100);
|
||||||
rt.tick();
|
rt.tick();
|
||||||
const results = drain(rt);
|
const results = drainInbox(inbox);
|
||||||
// order depends on HashMap iteration, so just check set equality
|
|
||||||
assert(
|
assert(
|
||||||
results.includes(10) && results.includes(100) && results.length === 2,
|
results.includes(10) && results.includes(100) && results.length === 2,
|
||||||
"both counters report"
|
"both counters report"
|
||||||
);
|
);
|
||||||
|
inbox.free();
|
||||||
rt.free();
|
rt.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- actor_count ----------------------------------------------------------
|
// ---- actor_count ----------------------------------------------------------
|
||||||
{
|
{
|
||||||
console.log("test: actor_count tracks spawns");
|
console.log("test: actor_count tracks spawns");
|
||||||
const rt = new SwactorRuntime();
|
const rt = new WasmRuntime();
|
||||||
rt.spawn_counter();
|
const inbox = rt.new_inbox_u32();
|
||||||
rt.spawn_counter();
|
spawn_counter(rt, inbox.addr());
|
||||||
rt.spawn_counter();
|
spawn_counter(rt, inbox.addr());
|
||||||
|
spawn_counter(rt, inbox.addr());
|
||||||
rt.tick(); // drain spawn queue
|
rt.tick(); // drain spawn queue
|
||||||
assertEq(rt.actor_count(), 3, "three actors");
|
assertEq(rt.actor_count(), 3, "three actors");
|
||||||
|
inbox.free();
|
||||||
rt.free();
|
rt.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- send to invalid index returns false ----------------------------------
|
// ---- WasmAddr toString ----------------------------------------------------
|
||||||
{
|
{
|
||||||
console.log("test: send to bad index returns false");
|
console.log("test: WasmAddr has string representation");
|
||||||
const rt = new SwactorRuntime();
|
const rt = new WasmRuntime();
|
||||||
assert(!rt.send(999, 1), "out-of-bounds send");
|
const inbox = rt.new_inbox_u32();
|
||||||
|
const addr = spawn_counter(rt, inbox.addr());
|
||||||
|
const s = addr.toString();
|
||||||
|
// no_random generates deterministic addresses — just check it's a non-empty hex string
|
||||||
|
assert(typeof s === "string" && s.length > 0, "addr toString is non-empty string");
|
||||||
|
inbox.free();
|
||||||
|
rt.free();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- stop_actor -----------------------------------------------------------
|
||||||
|
{
|
||||||
|
console.log("test: stop_actor removes actor");
|
||||||
|
const rt = new WasmRuntime();
|
||||||
|
const inbox = rt.new_inbox_u32();
|
||||||
|
const c = spawn_counter(rt, inbox.addr());
|
||||||
|
rt.tick(); // drain spawn
|
||||||
|
assertEq(rt.actor_count(), 1, "one actor before stop");
|
||||||
|
rt.stop_actor(c);
|
||||||
|
rt.tick(); // process stop + cleanup
|
||||||
|
assertEq(rt.actor_count(), 0, "zero actors after stop");
|
||||||
|
inbox.free();
|
||||||
|
rt.free();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- bytes inbox ----------------------------------------------------------
|
||||||
|
{
|
||||||
|
console.log("test: byte inbox receives Uint8Array");
|
||||||
|
const rt = new WasmRuntime();
|
||||||
|
const inbox = rt.new_inbox_bytes();
|
||||||
|
// Send bytes directly (no actor — just to the inbox address)
|
||||||
|
rt.send_bytes(inbox.addr(), new Uint8Array([1, 2, 3]));
|
||||||
|
rt.tick();
|
||||||
|
const result = inbox.try_recv();
|
||||||
|
assert(result instanceof Uint8Array, "result is Uint8Array");
|
||||||
|
assertEq(Array.from(result), [1, 2, 3], "bytes match");
|
||||||
|
inbox.free();
|
||||||
|
rt.free();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- uptime ---------------------------------------------------------------
|
||||||
|
{
|
||||||
|
console.log("test: uptime_ms returns a number");
|
||||||
|
const rt = new WasmRuntime();
|
||||||
|
const uptime = rt.uptime_ms();
|
||||||
|
assert(typeof uptime === "number" && uptime >= 0, "uptime is non-negative number");
|
||||||
rt.free();
|
rt.free();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
69
docs/development_history/in-browser/BROWSER_RUNTIME.md
Normal file
69
docs/development_history/in-browser/BROWSER_RUNTIME.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Browser Runtime API — Development History
|
||||||
|
|
||||||
|
> Stage 2 of the in-browser swactor runtime. Replaces the hardcoded PoC with
|
||||||
|
> a generic, type-safe API using opaque address handles and typed inboxes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Core Types
|
||||||
|
|
||||||
|
**`WasmRuntime`** — wraps `swactor::Runtime` in single-threaded mode.
|
||||||
|
Methods: `tick()`, `actor_count()`, `send_u32()`, `send_bytes()`,
|
||||||
|
`stop_actor()`, `uptime_ms()`, `new_inbox_u32()`, `new_inbox_bytes()`.
|
||||||
|
Also exposes `runtime()` for Rust-side custom spawn functions.
|
||||||
|
|
||||||
|
**`WasmAddr`** — opaque handle wrapping `ActorAddress`. Returned by spawn
|
||||||
|
functions, passed to send functions. JS holds it as an opaque object.
|
||||||
|
Has `toString()` for debugging.
|
||||||
|
|
||||||
|
**`WasmInboxU32`** / **`WasmInboxBytes`** — typed inboxes for receiving
|
||||||
|
results from actors. Each has `addr()` → `WasmAddr` (so actors know where
|
||||||
|
to send) and `try_recv()` → `Option<T>`.
|
||||||
|
|
||||||
|
### Design Decisions
|
||||||
|
|
||||||
|
| # | Decision | Rationale |
|
||||||
|
|---|----------|-----------|
|
||||||
|
| 1 | Opaque `WasmAddr` handles instead of indices | Type-safe, stable identity, no out-of-bounds errors |
|
||||||
|
| 2 | Typed inbox types instead of generic `Inbox<T>` | wasm-bindgen doesn't support generics; concrete types are explicit |
|
||||||
|
| 3 | Free-standing `spawn_*` functions, not methods | Each actor type gets its own spawn function with typed args |
|
||||||
|
| 4 | `send_u32`/`send_bytes` on runtime | Common send types; custom types use typed spawn wrappers |
|
||||||
|
| 5 | Evolved existing `crates/wasm/` instead of new crate | Less churn, existing build/test infrastructure |
|
||||||
|
|
||||||
|
### Actor Pattern
|
||||||
|
|
||||||
|
Users expose actors to JS by writing one `#[wasm_bindgen]` spawn function
|
||||||
|
per actor type:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn spawn_my_actor(rt: &WasmRuntime, arg: JsValue) -> WasmAddr {
|
||||||
|
let actor = MyActor::from_js(arg);
|
||||||
|
let addr = rt.runtime().spawn(actor).unwrap();
|
||||||
|
WasmAddr(addr)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
10 Node.js tests in `crates/wasm/test.mjs`:
|
||||||
|
|
||||||
|
| Test | Scenario |
|
||||||
|
|------|----------|
|
||||||
|
| accumulator | Counter processes messages, reports running totals to inbox |
|
||||||
|
| relay | Relay forwards messages to counter (cross-actor, 2 ticks) |
|
||||||
|
| multiple counters | Two independent counters report to same inbox |
|
||||||
|
| actor_count | Spawning 3 actors reflects in stats |
|
||||||
|
| WasmAddr toString | Address has non-empty debug representation |
|
||||||
|
| stop_actor | Graceful stop removes actor from runtime |
|
||||||
|
| bytes inbox | WasmInboxBytes receives Uint8Array correctly |
|
||||||
|
| uptime_ms | Returns non-negative number |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `cargo test -p swactor` — native tests pass (no regressions)
|
||||||
|
- `cargo build --target wasm32-unknown-unknown -p wasm` — compiles
|
||||||
|
- `wasm-pack build --target nodejs` in `crates/wasm/` — builds pkg/
|
||||||
|
- `node test.mjs` in `crates/wasm/` — 10/10 tests pass
|
||||||
Loading…
Reference in a new issue