feat: basic wasm (#12)

Unfeatured wasm module. Mostly to test that wasm builds


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
zacheryasc 2026-02-06 16:47:43 +00:00
parent a01c3eb5f9
commit c9d1383d1c
5 changed files with 367 additions and 1 deletions

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
/target
**/target
tools/depgraph/target/
node_modules/
.vscode/

138
wasm/Cargo.lock generated Normal file
View file

@ -0,0 +1,138 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "swactor"
version = "0.1.0"
dependencies = [
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "swactor-wasm"
version = "0.1.0"
dependencies = [
"swactor",
"wasm-bindgen",
]
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]

13
wasm/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[workspace]
[package]
name = "swactor-wasm"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
swactor = { path = "..", default-features = false, features = ["no_random"] }
wasm-bindgen = "0.2"

113
wasm/src/lib.rs Normal file
View file

@ -0,0 +1,113 @@
use wasm_bindgen::prelude::*;
use swactor::actor::{ActorAddress, ActorInterface};
use swactor::runtime::{Ctx, Inbox, Runtime, RuntimeConfig};
// ---------------------------------------------------------------------------
// Actors (private — only exposed through the wasm API)
// ---------------------------------------------------------------------------
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);
}
}
// ---------------------------------------------------------------------------
// JS-facing runtime wrapper
// ---------------------------------------------------------------------------
#[wasm_bindgen]
pub struct SwactorRuntime {
rt: Runtime,
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
.spawn(Counter {
total: 0,
report_to: *self.inbox.addr(),
})
.expect("spawn counter");
let idx = self.actors.len();
self.actors.push(addr);
idx
}
/// Spawn a relay that forwards every message to `target_idx`.
pub fn spawn_relay(&mut self, target_idx: usize) -> usize {
let target = self.actors[target_idx];
let addr = self
.rt
.spawn(Relay { target })
.expect("spawn relay");
let idx = self.actors.len();
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()
}
}

102
wasm/test.mjs Normal file
View file

@ -0,0 +1,102 @@
import { SwactorRuntime } from "./pkg/swactor_wasm.js";
let passed = 0;
let failed = 0;
function assert(cond, msg) {
if (!cond) {
console.error(` FAIL: ${msg}`);
failed++;
} else {
passed++;
}
}
function assertEq(a, b, msg) {
const aj = JSON.stringify(a);
const bj = JSON.stringify(b);
if (aj !== bj) {
console.error(` FAIL: ${msg} (got ${aj}, expected ${bj})`);
failed++;
} else {
passed++;
}
}
function drain(rt) {
const results = [];
let v;
while ((v = rt.try_recv()) !== undefined) results.push(v);
return results;
}
// ---- accumulator ----------------------------------------------------------
{
console.log("test: accumulator processes messages");
const rt = new SwactorRuntime();
const c = rt.spawn_counter();
rt.send(c, 1);
rt.send(c, 2);
rt.send(c, 10);
rt.tick();
assertEq(drain(rt), [1, 3, 13], "running totals");
rt.free();
}
// ---- relay ----------------------------------------------------------------
{
console.log("test: relay forwards to counter");
const rt = new SwactorRuntime();
const c = rt.spawn_counter();
const r = rt.spawn_relay(c);
rt.send(r, 5);
rt.send(r, 7);
// tick 1: relay receives and forwards (cross-actor, same worker → pending_local)
// tick 2: counter receives forwarded messages
rt.tick();
rt.tick();
assertEq(drain(rt), [5, 12], "relayed totals");
rt.free();
}
// ---- multiple counters ----------------------------------------------------
{
console.log("test: multiple independent counters");
const rt = new SwactorRuntime();
const a = rt.spawn_counter();
const b = rt.spawn_counter();
rt.send(a, 10);
rt.send(b, 100);
rt.tick();
const results = drain(rt);
// order depends on HashMap iteration, so just check set equality
assert(
results.includes(10) && results.includes(100) && results.length === 2,
"both counters report"
);
rt.free();
}
// ---- actor_count ----------------------------------------------------------
{
console.log("test: actor_count tracks spawns");
const rt = new SwactorRuntime();
rt.spawn_counter();
rt.spawn_counter();
rt.spawn_counter();
rt.tick(); // drain spawn queue
assertEq(rt.actor_count(), 3, "three actors");
rt.free();
}
// ---- send to invalid index returns false ----------------------------------
{
console.log("test: send to bad index returns false");
const rt = new SwactorRuntime();
assert(!rt.send(999, 1), "out-of-bounds send");
rt.free();
}
// ---- results --------------------------------------------------------------
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);