research: in-browser runtime Cycle 0
Research phase for browser-native swactor runtime: - constraints.md: wasm-threads (SharedArrayBuffer), Rust-only actors, COOP/COEP required, no new modules in src/ - research_synthesis.md: P0-P4 priority ranking, platform dependency analysis, ecosystem comparison (Lunatic, wasmCloud, Actix) - 6 feature-stage docs: platform abstraction → single-worker → multi-worker parallelism → feature parity → transport → DX - Key decision: SharedArrayBuffer + wasm-threads over postMessage isolation (swactor's shared-memory architecture demands it) Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
parent
cd1816e398
commit
63cba1c4eb
10 changed files with 870 additions and 0 deletions
58
big-feature-phase/notes/constraints.md
Normal file
58
big-feature-phase/notes/constraints.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Constraints — In-Browser Swactor Runtime
|
||||||
|
|
||||||
|
## Threading Model
|
||||||
|
|
||||||
|
- **wasm-threads is mandatory** — the runtime uses SharedArrayBuffer + WebAssembly atomics for multi-worker parallelism. There is no single-threaded degraded mode for MVP.
|
||||||
|
- Browsers must serve pages with COOP/COEP headers:
|
||||||
|
- `Cross-Origin-Opener-Policy: same-origin`
|
||||||
|
- `Cross-Origin-Embedder-Policy: require-corp`
|
||||||
|
- Build requires nightly Rust + `-Z build-std=std,panic_abort` + target features `+atomics,+bulk-memory,+mutable-globals`.
|
||||||
|
|
||||||
|
## Architecture Rules
|
||||||
|
|
||||||
|
- **Platform abstractions live in core swactor** (`src/`), gated by `#[cfg(target_arch = "wasm32")]`. They do not belong in the wasm crate.
|
||||||
|
- **Do not add new modules** to `src/` — modify existing files only (TASK.md style rule).
|
||||||
|
- **Do not restructure** existing module boundaries. The abstraction is a thin layer (type aliases, cfg-gated imports), not a trait-based HAL.
|
||||||
|
- The browser crate (`crates/wasm-browser/` or evolved `crates/wasm/`) is a **thin wasm-bindgen shell**. All scheduling, routing, and actor logic stays in core Rust.
|
||||||
|
|
||||||
|
## Actor Model
|
||||||
|
|
||||||
|
- **Rust-only actors** — actors are written in Rust and compiled to wasm. JavaScript does not define actor behavior.
|
||||||
|
- JS interacts through the wasm-bindgen API: create runtime, spawn actors (by registered type), send messages, receive results.
|
||||||
|
- Actor types are registered at compile time via Rust generics, not dynamically from JS.
|
||||||
|
|
||||||
|
## Performance Priorities
|
||||||
|
|
||||||
|
- Maximize throughput: auto-scheduling via `setTimeout(0)` tight loop, not `requestAnimationFrame` (which caps at display refresh rate).
|
||||||
|
- Web Worker count defaults to `navigator.hardwareConcurrency` for full core utilization.
|
||||||
|
- Zero-copy where possible: SharedArrayBuffer eliminates serialization between workers.
|
||||||
|
- Minimize JS↔Wasm boundary crossings — batch operations where feasible.
|
||||||
|
|
||||||
|
## Feature Scope
|
||||||
|
|
||||||
|
- All core features that compile for wasm32: spawn, send, receive, tick, actor lifecycle, watching, extensions.
|
||||||
|
- swactor-std features (naming, groups, monitoring) should work if they compile.
|
||||||
|
- Transport: WebSocket adapter for distributed clusters. STUN/TURN (WebRTC) deferred to later.
|
||||||
|
- Features that require OS primitives not available in wasm (filesystem, raw TCP) are excluded.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Tests must pass on both native (`cargo test`) and wasm targets.
|
||||||
|
- Wasm tests use `wasm-pack test --headless --chrome` or Node.js with `--experimental-wasm-threads`.
|
||||||
|
- No test-only code paths that diverge native vs wasm behavior — if it works differently, it's a bug.
|
||||||
|
- Prefer scenario tests over structural tests (per project testing rules).
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `web-time` — drop-in replacement for `std::time::Instant` on wasm32
|
||||||
|
- `wasm-bindgen` + `js-sys` + `web-sys` — browser API bindings (in the wasm crate only, not core)
|
||||||
|
- `gloo-timers` — optional, for ergonomic setTimeout/setInterval
|
||||||
|
- No new dependencies in core swactor beyond `web-time` (which is no-op on native)
|
||||||
|
|
||||||
|
## What We Don't Do
|
||||||
|
|
||||||
|
- No async/await runtime (tokio, async-std) — swactor is synchronous tick-based
|
||||||
|
- No Emscripten — target is `wasm32-unknown-unknown` only
|
||||||
|
- No WASI — browser environment, not server-side wasm
|
||||||
|
- No JS actor definitions — Rust only
|
||||||
|
- No polyfills for missing atomics — if SharedArrayBuffer isn't available, the runtime doesn't start
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
# Stage 1 — Platform Abstraction Layer
|
||||||
|
|
||||||
|
**Priority**: P0
|
||||||
|
**Depends on**: Nothing
|
||||||
|
**Enables**: All subsequent stages
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make core swactor compile for `wasm32-unknown-unknown` with `+atomics,+bulk-memory,+mutable-globals` target features. No behavioral changes on native targets. No new modules — only modify existing files with `cfg` gates.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
### 1. Instant → web_time::Instant
|
||||||
|
|
||||||
|
**Files**: `src/runtime.rs`, `src/worker.rs`
|
||||||
|
|
||||||
|
Add `web-time` to `[dependencies]` (it's a no-op on non-wasm targets). Replace:
|
||||||
|
```rust
|
||||||
|
use std::time::Instant;
|
||||||
|
```
|
||||||
|
with:
|
||||||
|
```rust
|
||||||
|
use web_time::Instant;
|
||||||
|
```
|
||||||
|
|
||||||
|
`web-time` is a drop-in replacement. The `Instant` type has identical API on native (delegates to `std::time::Instant`) and on wasm32 (uses `performance.now()`).
|
||||||
|
|
||||||
|
**Scope**: 2 `use` statements, 0 logic changes.
|
||||||
|
|
||||||
|
### 2. Thread Parking → ParkHandle
|
||||||
|
|
||||||
|
**Files**: `src/runtime.rs`, `src/delivery.rs`, `src/worker.rs`
|
||||||
|
|
||||||
|
Currently uses `OnceLock<Thread>` + `thread::park_timeout` + `Thread::unpark`. On wasm32, there's no `Thread` type accessible from Rust (workers are JS objects). But wasm-threads supports `Atomics.wait`/`Atomics.notify` through Rust's `std::sync::atomic` and futex primitives.
|
||||||
|
|
||||||
|
Approach: Define a `ParkHandle` abstraction in `src/runtime.rs`:
|
||||||
|
|
||||||
|
**Native**:
|
||||||
|
```rust
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
mod parking {
|
||||||
|
pub type ParkHandle = OnceLock<Thread>;
|
||||||
|
pub fn register(handle: &ParkHandle) { handle.set(thread::current()).ok(); }
|
||||||
|
pub fn unpark(handle: &ParkHandle) { if let Some(t) = handle.get() { t.unpark(); } }
|
||||||
|
pub fn park_timeout_us(micros: u64) { thread::park_timeout(Duration::from_micros(micros)); }
|
||||||
|
pub fn yield_now() { thread::yield_now(); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wasm32**:
|
||||||
|
```rust
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod parking {
|
||||||
|
// Use an AtomicI32 as a futex-like signal. Atomics.wait blocks the
|
||||||
|
// wasm thread, Atomics.notify wakes it — same semantics as park/unpark.
|
||||||
|
pub struct ParkHandle(AtomicI32);
|
||||||
|
pub fn register(_: &ParkHandle) {} // no-op, handle is pre-initialized
|
||||||
|
pub fn unpark(handle: &ParkHandle) {
|
||||||
|
handle.0.store(1, Ordering::Release);
|
||||||
|
std::sync::atomic::fence(Ordering::SeqCst);
|
||||||
|
// Atomics.notify via core::arch::wasm32::memory_atomic_notify
|
||||||
|
core::arch::wasm32::memory_atomic_notify(&handle.0 as *const _ as *mut i32, 1);
|
||||||
|
}
|
||||||
|
pub fn park_timeout_us(micros: u64) {
|
||||||
|
// Atomics.wait via core::arch::wasm32::memory_atomic_wait32
|
||||||
|
core::arch::wasm32::memory_atomic_wait32(ptr, 0, timeout_ns as i64);
|
||||||
|
}
|
||||||
|
pub fn yield_now() {} // no-op on wasm
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scope**: New `parking` sub-module in `runtime.rs` (~30 lines), update `TickContext` to use `ParkHandle` instead of `OnceLock<Thread>`, update `worker.rs` backoff loop.
|
||||||
|
|
||||||
|
### 3. Thread Spawning — No Change in Core
|
||||||
|
|
||||||
|
Thread spawning (`std::thread::Builder::new().spawn()`) only happens in `Runtime::run()` (line 351). This method will be overridden/wrapped by the browser crate — it won't be called on wasm32. We can gate it:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn run(self) -> Result<RuntimeHandle, Error> { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
The wasm browser crate will provide its own `run()` that spawns Web Workers instead.
|
||||||
|
|
||||||
|
### 4. Validate crossbeam Compilation
|
||||||
|
|
||||||
|
Test that `crossbeam-queue` compiles for wasm32 with atomics. If it doesn't, provide a cfg-gated fallback in `src/channel.rs` using `VecDeque` wrapped in `Mutex`. (Likely not needed — crossbeam uses `core::sync::atomic` which works with wasm atomics.)
|
||||||
|
|
||||||
|
### 5. Feature Flag
|
||||||
|
|
||||||
|
Add a `wasm` feature to `Cargo.toml`:
|
||||||
|
```toml
|
||||||
|
[features]
|
||||||
|
wasm = ["web-time", "no_random"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
web-time = { version = "0.2", optional = true }
|
||||||
|
```
|
||||||
|
|
||||||
|
On wasm32, this feature enables `web-time` and `no_random` together.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. `cargo test` passes unchanged on native
|
||||||
|
2. `cargo build --target wasm32-unknown-unknown --features wasm -Z build-std=std,panic_abort` compiles (may need `+atomics` RUSTFLAGS)
|
||||||
|
3. No runtime behavior changes on native (confirm with existing test suite)
|
||||||
|
|
||||||
|
## Estimated Scope
|
||||||
|
|
||||||
|
~50-80 lines of new/changed code across 4 files. No new modules.
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
# Stage 2 — Single-Worker Browser Runtime
|
||||||
|
|
||||||
|
**Priority**: P0
|
||||||
|
**Depends on**: Stage 1 (platform abstraction)
|
||||||
|
**Enables**: Stage 3 (multi-worker), Stage 4 (feature parity)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
A working browser runtime on a single dedicated Web Worker with a JS API that supports spawning arbitrary (pre-registered) actor types, sending messages, receiving results, and auto-scheduled ticking.
|
||||||
|
|
||||||
|
## What's Built
|
||||||
|
|
||||||
|
### 1. New Crate: `crates/wasm-browser/`
|
||||||
|
|
||||||
|
Replaces the PoC `crates/wasm/`. Structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
crates/wasm-browser/
|
||||||
|
├── Cargo.toml
|
||||||
|
├── src/
|
||||||
|
│ ├── lib.rs # wasm-bindgen entry point
|
||||||
|
│ ├── runtime.rs # BrowserRuntime wrapping swactor::Runtime
|
||||||
|
│ ├── worker_glue.rs # Web Worker spawn/communication glue
|
||||||
|
│ └── scheduling.rs # Auto-tick scheduling (setTimeout loop)
|
||||||
|
├── js/
|
||||||
|
│ ├── worker.js # Web Worker bootstrap script
|
||||||
|
│ └── index.js # Main thread API wrapper (optional)
|
||||||
|
└── tests/
|
||||||
|
└── browser.rs # wasm-pack test suite
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. BrowserRuntime (wasm-bindgen API)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct BrowserRuntime { ... }
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl BrowserRuntime {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(config: JsValue) -> Self;
|
||||||
|
|
||||||
|
/// Spawn an actor by type name. Returns an opaque handle.
|
||||||
|
pub fn spawn(&mut self, type_name: &str, init: JsValue) -> JsValue;
|
||||||
|
|
||||||
|
/// Send a message to an actor.
|
||||||
|
pub fn send(&self, addr: JsValue, msg: JsValue) -> bool;
|
||||||
|
|
||||||
|
/// Drive one tick manually.
|
||||||
|
pub fn tick(&self);
|
||||||
|
|
||||||
|
/// Start auto-scheduling. Calls tick() in a tight setTimeout(0) loop.
|
||||||
|
pub fn start(&self);
|
||||||
|
|
||||||
|
/// Stop auto-scheduling.
|
||||||
|
pub fn stop(&self);
|
||||||
|
|
||||||
|
/// Poll for results from a JS-visible inbox.
|
||||||
|
pub fn try_recv(&self) -> JsValue;
|
||||||
|
|
||||||
|
/// Runtime stats snapshot.
|
||||||
|
pub fn stats(&self) -> JsValue;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Actor Registration
|
||||||
|
|
||||||
|
Since Rust generics can't be dynamically dispatched from JS, actor types are registered at compile time:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// In the user's wasm crate that depends on wasm-browser:
|
||||||
|
register_actors! {
|
||||||
|
"counter" => Counter,
|
||||||
|
"relay" => Relay,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This macro generates a factory map that `BrowserRuntime::spawn` indexes by string name. Each entry knows how to deserialize `JsValue` init args into the actor's constructor.
|
||||||
|
|
||||||
|
### 4. Auto-Scheduling
|
||||||
|
|
||||||
|
The runtime self-drives via a `setTimeout(0)` loop:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function tickLoop() {
|
||||||
|
runtime.tick();
|
||||||
|
if (runtime.is_running()) {
|
||||||
|
setTimeout(tickLoop, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs as fast as the browser allows (~4ms between ticks in most browsers, faster in Web Workers). The Rust side just calls `tick()` — no async runtime needed.
|
||||||
|
|
||||||
|
### 5. Message Serialization
|
||||||
|
|
||||||
|
JS ↔ Wasm boundary requires serialization. Options:
|
||||||
|
- **serde-wasm-bindgen**: Serialize Rust types to/from JsValue via serde. Zero-copy for simple types.
|
||||||
|
- **Manual**: Convert JsValue to bytes, route as `ByteMessage`.
|
||||||
|
|
||||||
|
For Stage 2, use `serde-wasm-bindgen` for typed messages. Actor `Incoming` types must implement `serde::Deserialize`.
|
||||||
|
|
||||||
|
### 6. Single Worker Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐ postMessage ┌──────────────────────┐
|
||||||
|
│ Main Thread │ ◄──────────────────────► │ Web Worker │
|
||||||
|
│ │ │ │
|
||||||
|
│ JS application │ "spawn", "send", │ BrowserRuntime │
|
||||||
|
│ calls API methods │ "tick", "recv" │ swactor::Runtime │
|
||||||
|
│ │ │ (1 worker, tick()) │
|
||||||
|
└─────────────────────┘ └──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The Web Worker runs the swactor runtime. The main thread sends commands via `postMessage`. This keeps the UI thread free.
|
||||||
|
|
||||||
|
Alternative: run everything on the main thread (simpler, but blocks UI during tick). Support both modes — the user picks.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. `wasm-pack build --target web` succeeds
|
||||||
|
2. `wasm-pack test --headless --chrome` passes
|
||||||
|
3. Manual test: HTML page spawns actors, sends messages, receives results
|
||||||
|
4. Auto-scheduling: actors process messages continuously without manual tick calls
|
||||||
|
5. Performance: measure ticks/sec, compare to native single-threaded
|
||||||
|
|
||||||
|
## Estimated Scope
|
||||||
|
|
||||||
|
~300-500 lines of Rust + ~50 lines of JS glue.
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
# Stage 3 — Multi-Worker Parallelism
|
||||||
|
|
||||||
|
**Priority**: P1
|
||||||
|
**Depends on**: Stage 1 (platform abstraction), Stage 2 (single-worker browser)
|
||||||
|
**Enables**: Stage 5 (transport)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Spawn N Web Workers sharing the same swactor runtime via SharedArrayBuffer. Actors distributed across workers for true multi-core parallelism. Worker count configurable, defaults to `navigator.hardwareConcurrency`.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ Main Thread │ postMessage API
|
||||||
|
│ (JS app) │◄─────────────────────┐
|
||||||
|
└──────┬───────┘ │
|
||||||
|
│ spawn workers │
|
||||||
|
▼ │
|
||||||
|
┌──────────────┐ SharedArrayBuffer ┌──────────────┐
|
||||||
|
│ Web Worker 0 │◄───────────────────►│ Web Worker 1 │
|
||||||
|
│ swactor │ (InboxRegistry, │ swactor │
|
||||||
|
│ Worker #0 │ AddressMap, │ Worker #1 │
|
||||||
|
│ tick loop │ transfer queues, │ tick loop │
|
||||||
|
└──────────────┘ atomics) └──────────────┘
|
||||||
|
...
|
||||||
|
┌──────────────┐
|
||||||
|
│ Web Worker N │
|
||||||
|
│ swactor │
|
||||||
|
│ Worker #N │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Challenge: Web Worker ↔ SharedArrayBuffer
|
||||||
|
|
||||||
|
Web Workers can share `SharedArrayBuffer` instances. The swactor `Runtime` struct contains `Arc`-wrapped shared state (InboxRegistry, AddressMap, etc.). On native, this memory is shared via the process address space. On wasm with SharedArrayBuffer, it's shared via the underlying wasm linear memory.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Main thread** creates the `Runtime` (allocates shared structures in wasm linear memory)
|
||||||
|
2. **Main thread** spawns N Web Workers, each loading the same `.wasm` module with `shared: true` memory
|
||||||
|
3. Each Web Worker receives a pointer (offset) to the `Runtime` shared state
|
||||||
|
4. Each Worker runs `worker.run(&tc, &is_running)` — the same tick loop as native
|
||||||
|
5. Crossbeam queues, atomics, Mutex/RwLock all work because the underlying memory is shared
|
||||||
|
|
||||||
|
### wasm-bindgen + Web Workers
|
||||||
|
|
||||||
|
The `web-sys::Worker` API creates workers. Each worker loads the same wasm module:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// worker.js (loaded by each Web Worker)
|
||||||
|
import init, { worker_entry } from './pkg/wasm_browser.js';
|
||||||
|
|
||||||
|
self.onmessage = async (e) => {
|
||||||
|
const { module, memory, worker_id, runtime_ptr } = e.data;
|
||||||
|
await init({ module, memory }); // shared memory!
|
||||||
|
worker_entry(worker_id, runtime_ptr);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The Rust side:
|
||||||
|
```rust
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn worker_entry(worker_id: usize, runtime_ptr: u32) {
|
||||||
|
// Reconstruct the shared Runtime reference from the raw pointer
|
||||||
|
// Run the tick loop for this worker
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Sharing
|
||||||
|
|
||||||
|
With `wasm-threads`, the wasm linear memory is backed by a `SharedArrayBuffer`. All workers see the same memory. `Arc` increments are atomic operations on shared memory. Crossbeam queues use atomic compare-and-swap on shared memory. This is identical to how it works with OS threads.
|
||||||
|
|
||||||
|
**Critical**: The wasm module must be compiled with `--shared-memory` and the `atomics` feature. The `Memory` import must use `shared: true`.
|
||||||
|
|
||||||
|
## Park / Unpark
|
||||||
|
|
||||||
|
The `ParkHandle` from Stage 1 uses `memory_atomic_wait32` / `memory_atomic_notify` — the wasm equivalents of futex. These work across Web Workers sharing the same memory.
|
||||||
|
|
||||||
|
- `park_timeout_us(micros)` → `Atomics.wait(ptr, expected, timeout)` — blocks the Worker thread
|
||||||
|
- `unpark(handle)` → `Atomics.notify(ptr, 1)` — wakes one waiting Worker
|
||||||
|
|
||||||
|
This gives the same backoff behavior as native: hot spin → yield → sleep with exponential backoff.
|
||||||
|
|
||||||
|
## Worker Spawning
|
||||||
|
|
||||||
|
Replace `std::thread::Builder::new().spawn()` in `Runtime::run()`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn run(self) -> Result<BrowserRuntimeHandle, Error> {
|
||||||
|
let num_workers = self.config.num_threads.max(1);
|
||||||
|
let rt = Arc::new(self);
|
||||||
|
|
||||||
|
for i in 0..num_workers {
|
||||||
|
let worker = web_sys::Worker::new("./worker.js")?;
|
||||||
|
worker.post_message(&JsValue::from(/* module, memory, worker_id, ptr */));
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Placement
|
||||||
|
|
||||||
|
The existing `Placement` strategy (load-aware round-robin) works unchanged — it reads `WorkerStats` atomics to pick the least-loaded worker. On wasm, these atomics are in SharedArrayBuffer, readable from any worker.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Spawn runtime with `num_threads: 4`, verify 4 Web Workers created
|
||||||
|
2. Spawn actors, verify they're distributed across workers (check stats per-worker actor count)
|
||||||
|
3. Cross-worker message delivery works (actor on Worker 0 sends to actor on Worker 1)
|
||||||
|
4. Backoff/parking works (idle workers sleep, wake on new messages)
|
||||||
|
5. Throughput scales with worker count (benchmark: N workers vs 1 worker)
|
||||||
|
6. Shutdown: all workers terminate cleanly when `is_running` set to false
|
||||||
|
|
||||||
|
## Estimated Scope
|
||||||
|
|
||||||
|
~200-300 lines Rust + ~30 lines JS worker bootstrap. Most complexity is in the Web Worker ↔ shared memory plumbing, not the Rust logic (which is the same as native).
|
||||||
109
big-feature-phase/notes/feature-stages/04-feature-parity.md
Normal file
109
big-feature-phase/notes/feature-stages/04-feature-parity.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Stage 4 — Feature Parity
|
||||||
|
|
||||||
|
**Priority**: P1
|
||||||
|
**Depends on**: Stage 2 (single-worker) or Stage 3 (multi-worker)
|
||||||
|
**Enables**: Stage 5 (transport), Stage 6 (DX)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
All swactor features that are feasible in a browser environment work and are tested: actor watching, swactor-std extensions (naming, groups, monitoring), stats introspection.
|
||||||
|
|
||||||
|
## Features to Enable
|
||||||
|
|
||||||
|
### 1. Actor Watching / Death Notifications
|
||||||
|
|
||||||
|
**Status**: Already in core (`src/runtime.rs`, `src/worker.rs`)
|
||||||
|
|
||||||
|
Components:
|
||||||
|
- `ExitReason` enum — normal, panic, stopped
|
||||||
|
- `ActorExited` message — delivered to watchers
|
||||||
|
- `on_actor_exit()` default method on `ActorInterface`
|
||||||
|
- `WatchRegistry` — tracks who watches whom
|
||||||
|
- Phase 5b in `tick_once` — delivers death notifications
|
||||||
|
|
||||||
|
**Wasm concern**: `WatchRegistry` is behind `Arc<Mutex<...>>`. With wasm-threads, `Mutex` works. The catch_unwind panic-safety model works identically in wasm.
|
||||||
|
|
||||||
|
**Work needed**: Compile and test. Write wasm-specific tests for:
|
||||||
|
- Actor dies → watchers notified
|
||||||
|
- Watcher on different Web Worker receives notification (cross-worker)
|
||||||
|
- Panic in wasm actor → poisoned, watchers notified
|
||||||
|
|
||||||
|
### 2. swactor-std Extension
|
||||||
|
|
||||||
|
**Status**: Complete in `crates/std/`
|
||||||
|
|
||||||
|
Components:
|
||||||
|
- `StdExtension` — wraps NameRegistry + MonitorRegistry + GroupRegistry
|
||||||
|
- `CtxMonitoring` — watch/unwatch actors
|
||||||
|
- `CtxNaming` — register/resolve actor names
|
||||||
|
- `CtxGroups` — join/leave groups, broadcast
|
||||||
|
- `RuntimeNaming` — resolve names from runtime handle
|
||||||
|
- `RuntimeGroups` — list groups, broadcast from outside
|
||||||
|
- Supervisor — restart policies
|
||||||
|
|
||||||
|
**Wasm concern**: All use `Arc`, `Mutex`, `HashMap` — standard types that work with wasm-threads. No OS-specific dependencies.
|
||||||
|
|
||||||
|
**Work needed**:
|
||||||
|
- Add `crates/std/` to wasm build verification
|
||||||
|
- Test naming: register name → resolve from another actor on different worker
|
||||||
|
- Test groups: broadcast reaches all group members across workers
|
||||||
|
- Test supervisor: child dies → supervisor restarts (factory-based)
|
||||||
|
|
||||||
|
### 3. Stats and Introspection
|
||||||
|
|
||||||
|
**Status**: In core (`src/stats.rs`)
|
||||||
|
|
||||||
|
Components:
|
||||||
|
- `WorkerStats` — per-worker atomic counters (actors, depth, ticks, messages)
|
||||||
|
- `RuntimeStats` — aggregated snapshot
|
||||||
|
- `StatsHook` trait — called each tick with stats
|
||||||
|
|
||||||
|
**Wasm concern**: Atomic counters work with wasm-threads. `StatsHook` is called in the tick loop — works.
|
||||||
|
|
||||||
|
**Work needed**:
|
||||||
|
- Expose `RuntimeStats` to JS via `serde-wasm-bindgen` (JSON-serializable snapshot)
|
||||||
|
- Optional: periodic stats push to main thread via `postMessage`
|
||||||
|
- Test: spawn actors across workers, verify stats reflect correct counts
|
||||||
|
|
||||||
|
### 4. Runtime Extensions
|
||||||
|
|
||||||
|
**Status**: In core (`src/extension.rs`)
|
||||||
|
|
||||||
|
The `RuntimeExtension` trait (`on_actor_death`, `cleanup_dead`, `as_any`) is called during phase 7 of tick_once. It uses `Arc<dyn RuntimeExtension>` — works with wasm-threads.
|
||||||
|
|
||||||
|
**Work needed**: Verify `StdExtension` as a `RuntimeExtension` compiles and works in wasm. Test the full lifecycle: actor death → extension notified → cleanup runs.
|
||||||
|
|
||||||
|
## JS API Additions
|
||||||
|
|
||||||
|
Extend the `BrowserRuntime` wasm-bindgen API:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl BrowserRuntime {
|
||||||
|
// Actor watching
|
||||||
|
pub fn watch(&self, watcher: JsValue, target: JsValue) -> bool;
|
||||||
|
|
||||||
|
// Naming (if StdExtension enabled)
|
||||||
|
pub fn register_name(&self, name: &str, addr: JsValue) -> bool;
|
||||||
|
pub fn resolve_name(&self, name: &str) -> JsValue;
|
||||||
|
|
||||||
|
// Groups
|
||||||
|
pub fn join_group(&self, group: &str, addr: JsValue) -> bool;
|
||||||
|
pub fn broadcast_group(&self, group: &str, msg: JsValue) -> bool;
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
pub fn stats(&self) -> JsValue; // JSON snapshot of RuntimeStats
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. All existing native tests for watching/std pass on wasm target
|
||||||
|
2. Cross-worker watching: actor on Worker 0 watches actor on Worker 1, Worker 1 actor dies → notification arrives
|
||||||
|
3. Naming works across workers: register on Worker 0, resolve on Worker 1
|
||||||
|
4. Group broadcast reaches actors on all workers
|
||||||
|
5. Stats counters are accurate across workers (compare sum to expected)
|
||||||
|
6. `cargo test` still passes on native (no regressions)
|
||||||
|
|
||||||
|
## Estimated Scope
|
||||||
|
|
||||||
|
~100-200 lines of new wasm-bindgen API surface + ~200 lines of wasm tests. Core logic should work as-is once it compiles.
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Stage 5 — Transport Foundation
|
||||||
|
|
||||||
|
**Priority**: P2
|
||||||
|
**Depends on**: Stage 3 (multi-worker), Stage 4 (feature parity)
|
||||||
|
**Enables**: Browser nodes joining distributed swactor clusters
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Browser nodes connect to native swactor clusters via WebSocket. A browser can spawn actors that communicate with actors on server nodes. Foundation for future STUN/TURN (WebRTC DataChannel) for browser-to-browser direct connections.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐ WebSocket ┌──────────────────┐
|
||||||
|
│ Browser Node │ ◄────────────────────────► │ Server Node │
|
||||||
|
│ (wasm runtime) │ │ (native runtime)│
|
||||||
|
│ │ swactor wire protocol │ │
|
||||||
|
│ Actor A ──────►─┤───── msg for Actor B ─────►├──► Actor B │
|
||||||
|
│ │ │ │
|
||||||
|
│ Actor C ◄───────┤◄──── msg for Actor C ─────┤───── Actor D │
|
||||||
|
└──────────────────┘ └──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Existing Transport Infrastructure
|
||||||
|
|
||||||
|
swactor already has a transport layer (feature-gated under `transport`):
|
||||||
|
|
||||||
|
- `src/transport.rs` — `TransportRouter`, `CodecRegistry`, remote message routing
|
||||||
|
- `crates/distribution/` — SWIM protocol, gossip, cluster membership
|
||||||
|
- `crates/distribution/src/driver.rs` — `NodeDriver` bridges `DistributedNode` ↔ TCP
|
||||||
|
- Wire protocol: Ping/Ack/PingReq with piggyback bytes
|
||||||
|
|
||||||
|
The browser transport needs to implement the same wire protocol over WebSocket instead of raw TCP.
|
||||||
|
|
||||||
|
## What's Built
|
||||||
|
|
||||||
|
### 1. WebSocket Transport Adapter
|
||||||
|
|
||||||
|
A new module in `crates/wasm-browser/` (not in core):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct WebSocketTransport {
|
||||||
|
ws: web_sys::WebSocket,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportAdapter for WebSocketTransport {
|
||||||
|
fn send(&self, dest: SocketAddr, data: &[u8]) -> Result<(), Error>;
|
||||||
|
fn recv(&self) -> Option<(SocketAddr, Vec<u8>)>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses `web-sys::WebSocket` for the browser side. The server side uses a WebSocket server (e.g., `tokio-tungstenite`) that bridges to the existing TCP transport.
|
||||||
|
|
||||||
|
### 2. WebSocket ↔ TCP Bridge (Server Side)
|
||||||
|
|
||||||
|
A thin relay server that accepts WebSocket connections from browsers and translates to/from the TCP wire protocol:
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser ──WebSocket──► Bridge Server ──TCP──► swactor-node
|
||||||
|
```
|
||||||
|
|
||||||
|
This bridge is a separate binary/service, not part of the runtime. It's a protocol translator.
|
||||||
|
|
||||||
|
### 3. Browser Node Identity
|
||||||
|
|
||||||
|
Browser nodes need:
|
||||||
|
- A unique node ID (derived from random or assigned by the cluster)
|
||||||
|
- An address for the cluster to route messages to (the WebSocket endpoint)
|
||||||
|
- Membership in the SWIM protocol (lightweight — browsers are "client" members that don't participate in failure detection)
|
||||||
|
|
||||||
|
### 4. Cluster Registry Integration
|
||||||
|
|
||||||
|
The existing ClusterRegistry (LWW-Register CRDT in `crates/distribution/src/registry.rs`) should work from browsers:
|
||||||
|
- `register_name` / `resolve_name` / `registry_events` — all work over the wire
|
||||||
|
- Piggyback payloads carry registry updates through the WebSocket connection
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
| Decision | Choice | Rationale |
|
||||||
|
|----------|--------|-----------|
|
||||||
|
| Transport protocol | WebSocket (binary frames) | Universal browser support, bidirectional, binary-capable |
|
||||||
|
| Membership role | Client member (no failure detection) | Browsers are ephemeral; full SWIM overhead not justified |
|
||||||
|
| Bridge architecture | Separate relay server | Keeps swactor-node unchanged; bridge handles WebSocket↔TCP |
|
||||||
|
| Wire format | Same as TCP transport | No translation needed beyond framing (WebSocket frames ↔ TCP stream) |
|
||||||
|
|
||||||
|
## STUN/TURN Foundation (Future)
|
||||||
|
|
||||||
|
This stage establishes the transport abstraction. Stage 5 itself is WebSocket only. Future work:
|
||||||
|
|
||||||
|
- **WebRTC DataChannel** — direct browser-to-browser, requires STUN/TURN for NAT traversal
|
||||||
|
- The `TransportAdapter` trait from this stage will have a WebRTC implementation
|
||||||
|
- STUN/TURN server infrastructure is out of scope for this feature phase
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. Browser node connects to server cluster via WebSocket
|
||||||
|
2. Actor on browser sends message to actor on server → received
|
||||||
|
3. Actor on server sends message to actor on browser → received
|
||||||
|
4. Browser appears in cluster membership (visible in dashboard)
|
||||||
|
5. ClusterRegistry: name registered on server → resolvable from browser
|
||||||
|
6. Browser disconnects → cluster detects and removes membership
|
||||||
|
7. Reconnection: browser reconnects → re-joins cluster, actor addresses still valid
|
||||||
|
|
||||||
|
## Estimated Scope
|
||||||
|
|
||||||
|
~500-800 lines for WebSocket transport adapter + bridge server. Builds heavily on existing distribution infrastructure.
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Stage 6 — Developer Experience
|
||||||
|
|
||||||
|
**Priority**: P3
|
||||||
|
**Depends on**: Stage 2 (single-worker browser), Stage 4 (feature parity)
|
||||||
|
**Enables**: Adoption, ecosystem growth
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make it easy for Rust developers to build browser applications with swactor. TypeScript type safety, build tooling, and debugging support.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. TypeScript Type Generation
|
||||||
|
|
||||||
|
Derive TypeScript interfaces from Rust actor message types. When an actor defines:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ChatMessage {
|
||||||
|
pub from: String,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate:
|
||||||
|
```typescript
|
||||||
|
export interface ChatMessage {
|
||||||
|
from: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Approach**: Use `ts-rs` crate or a custom proc macro that emits `.d.ts` files during `wasm-pack build`. This gives TypeScript consumers compile-time type checking for messages.
|
||||||
|
|
||||||
|
### 2. Build Tooling
|
||||||
|
|
||||||
|
A `swactor-build` CLI or build script that wraps:
|
||||||
|
```bash
|
||||||
|
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \
|
||||||
|
cargo +nightly build --target wasm32-unknown-unknown \
|
||||||
|
-Z build-std=std,panic_abort \
|
||||||
|
--release
|
||||||
|
wasm-bindgen --target web --out-dir pkg/ ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Into:
|
||||||
|
```bash
|
||||||
|
swactor-build --target browser
|
||||||
|
```
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Detects nightly toolchain, installs if missing
|
||||||
|
- Sets correct RUSTFLAGS for wasm-threads
|
||||||
|
- Runs wasm-bindgen with correct target
|
||||||
|
- Copies worker.js bootstrap into output
|
||||||
|
- Generates example HTML with correct COOP/COEP headers
|
||||||
|
|
||||||
|
### 3. Example Project Template
|
||||||
|
|
||||||
|
A `cargo generate` template or example project:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-swactor-app/
|
||||||
|
├── Cargo.toml
|
||||||
|
├── src/
|
||||||
|
│ └── lib.rs # Define actors, register them
|
||||||
|
├── web/
|
||||||
|
│ ├── index.html # With COOP/COEP headers
|
||||||
|
│ ├── main.js # Import wasm, create runtime, interact
|
||||||
|
│ └── worker.js # Web Worker bootstrap (auto-generated)
|
||||||
|
└── tests/
|
||||||
|
└── browser.rs # wasm-pack tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Browser Dev Tools Integration
|
||||||
|
|
||||||
|
Expose runtime internals for debugging:
|
||||||
|
|
||||||
|
- **Actor Inspector**: List all actors, their types, mailbox depths, message counts
|
||||||
|
- **Message Tracer**: Log messages between actors (opt-in, performance impact)
|
||||||
|
- **Performance Monitor**: Ticks/sec, messages/sec, worker utilization
|
||||||
|
|
||||||
|
Implementation: A `console`-based logger that uses `web-sys::console` to output structured data. Optionally integrates with browser DevTools via custom formatters or a small React/Preact inspector panel.
|
||||||
|
|
||||||
|
### 5. Documentation
|
||||||
|
|
||||||
|
- Getting started guide
|
||||||
|
- Architecture overview for browser runtime
|
||||||
|
- Migration guide from native → browser (what works, what doesn't)
|
||||||
|
- API reference (generated from Rust doc comments)
|
||||||
|
- Example: Chat application with multiple browser tabs
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. TypeScript types match Rust types (compile TS project against generated `.d.ts`)
|
||||||
|
2. Build tool produces working wasm output from example project
|
||||||
|
3. Template project builds and runs out of the box
|
||||||
|
4. Dev tools show actor state in browser console
|
||||||
|
|
||||||
|
## Estimated Scope
|
||||||
|
|
||||||
|
Variable — this stage is a collection of independent DX improvements. Each can be implemented and shipped independently. Total: ~500-1000 lines across Rust, JS, and documentation.
|
||||||
4
big-feature-phase/notes/history.md
Normal file
4
big-feature-phase/notes/history.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Cycle History (append-only)
|
||||||
|
|
||||||
|
## Cycle 0 — Research (complete)
|
||||||
|
Investigated Lunatic, wasmCloud, Actix-wasm attempts. Analyzed core swactor platform deps: 4 blockers (thread spawn, park/unpark, yield, Instant). User confirmed: performance-first, SharedArrayBuffer+wasm-threads, Rust-only actors, all feasible features, future STUN/TURN. Produced constraints.md, research_synthesis.md, 6 feature-stage docs. Next: Stage 1 platform abstraction.
|
||||||
87
big-feature-phase/notes/research_synthesis.md
Normal file
87
big-feature-phase/notes/research_synthesis.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Research Synthesis — In-Browser Swactor Runtime
|
||||||
|
|
||||||
|
## Ecosystem Landscape
|
||||||
|
|
||||||
|
No established Rust actor framework runs natively in browsers. The closest projects:
|
||||||
|
|
||||||
|
- **Lunatic** — Erlang-inspired Wasm actor runtime using wasmtime (server-side, not browser). Uses preemptive scheduling and work-stealing. Not applicable to browser constraints.
|
||||||
|
- **wasmCloud** — CNCF distributed actor platform. Single-threaded actors, NATS-backed lattice. Cloud/edge focus, no browser target.
|
||||||
|
- **Actix** — Tokio-dependent, network stack doesn't compile for wasm32. Community attempts to port failed due to `net2`/tokio dependencies.
|
||||||
|
|
||||||
|
**Implication**: swactor would be the first Rust actor runtime with true multi-threaded browser execution via wasm-threads. This is a differentiated position.
|
||||||
|
|
||||||
|
## Existing Work in This Codebase
|
||||||
|
|
||||||
|
| Component | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| `crates/wasm/` | Basic PoC | Hardcoded Counter/Relay actors, manual tick, u32-only messages |
|
||||||
|
| `no_random` feature | Working | Deterministic address generation without `getrandom` |
|
||||||
|
| `tick()` method | Working | Single-threaded tick for manual driving |
|
||||||
|
| Actor watching | In core | `ExitReason`, `ActorExited`, `on_actor_exit`, `WatchRegistry` |
|
||||||
|
| swactor-std | Complete | StdExtension, naming, groups, monitoring, supervisor |
|
||||||
|
|
||||||
|
## Platform Dependencies Analysis
|
||||||
|
|
||||||
|
### Works as-is with wasm-threads
|
||||||
|
- `crossbeam-queue` (ArrayQueue, SegQueue) — uses `core::sync::atomic`
|
||||||
|
- `std::sync::{Mutex, RwLock}` — stdlib uses futex on wasm with atomics
|
||||||
|
- `std::sync::atomic::*` — maps to wasm atomic instructions
|
||||||
|
- `Arc<T>` — works with atomics
|
||||||
|
- `std::sync::OnceLock` — works with atomics
|
||||||
|
|
||||||
|
### Requires platform abstraction (4 items)
|
||||||
|
1. `std::thread::spawn` → Web Worker via `web-sys::Worker`
|
||||||
|
2. `thread::park_timeout` / `Thread::unpark` → `Atomics.wait` / `Atomics.notify`
|
||||||
|
3. `thread::yield_now` → no-op (or `Atomics.wait(0)` as hint)
|
||||||
|
4. `std::time::Instant` → `web_time::Instant` (drop-in crate)
|
||||||
|
|
||||||
|
## Priority Ranking
|
||||||
|
|
||||||
|
### P0 — Must Have (enables everything else)
|
||||||
|
|
||||||
|
1. **Platform abstraction layer** — cfg-gated replacements for thread spawn, park/unpark, yield, Instant. Core swactor compiles for wasm32 with atomics.
|
||||||
|
2. **Single-worker browser runtime** — Prove the runtime works in a browser. One Web Worker, auto-scheduled tick loop, generic JS API for spawn/send/recv.
|
||||||
|
3. **Multi-worker parallelism** — N Web Workers sharing runtime state via SharedArrayBuffer. Full utilization of browser CPU cores.
|
||||||
|
|
||||||
|
### P1 — Should Have (full actor system)
|
||||||
|
|
||||||
|
4. **Actor watching in browser** — Death notifications, exit reasons. Already in core, just needs to compile and pass wasm tests.
|
||||||
|
5. **swactor-std in browser** — Naming, groups, monitoring extensions. Compile and test for wasm32.
|
||||||
|
6. **Stats and introspection** — Runtime stats accessible from JS. Worker info, actor counts, message throughput.
|
||||||
|
|
||||||
|
### P2 — Important (distributed peer)
|
||||||
|
|
||||||
|
7. **WebSocket transport** — Adapter implementing swactor's transport traits over WebSocket. Browser node joins a distributed cluster.
|
||||||
|
8. **Browser-to-browser transport foundation** — WebRTC DataChannel scaffolding for future STUN/TURN.
|
||||||
|
|
||||||
|
### P3 — Nice to Have (developer experience)
|
||||||
|
|
||||||
|
9. **TypeScript type generation** — Derive TS interfaces from Rust actor message types.
|
||||||
|
10. **Build tooling** — wasm-pack wrapper script, example project template, CI configuration.
|
||||||
|
11. **Browser dev tools** — Actor inspector, message flow visualization, performance profiling.
|
||||||
|
|
||||||
|
### P4 — Future (out of scope for this feature phase)
|
||||||
|
|
||||||
|
12. **STUN/TURN integration** — Full NAT traversal for peer-to-peer browser connections.
|
||||||
|
13. **Hot code reload** — Swap actor implementations without restarting the runtime.
|
||||||
|
14. **Wasm component model** — Migrate from wasm-bindgen to component model when stabilized.
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
| Decision | Choice | Rationale |
|
||||||
|
|----------|--------|-----------|
|
||||||
|
| Threading model | SharedArrayBuffer + wasm-threads | swactor's shared-memory architecture (Arc, crossbeam queues, atomics) maps directly. postMessage isolation would require a rewrite. |
|
||||||
|
| Scheduling | `setTimeout(0)` tight loop | `requestAnimationFrame` caps at 60Hz. setTimeout(0) gives ~4ms resolution, sufficient for actor ticks. For rendering-coupled actors, RAF can be opt-in. |
|
||||||
|
| Actor definition | Rust only | Keeps the type system intact. JS actors would require dynamic dispatch and lose compile-time guarantees. |
|
||||||
|
| Platform abstraction approach | cfg-gated type aliases + inline functions | Minimal invasion. No trait-based HAL, no new modules. Just swap `std::time::Instant` → `web_time::Instant` etc. |
|
||||||
|
| Browser crate location | New `crates/wasm-browser/` | Clean separation from the existing PoC. The old `crates/wasm/` can be deprecated or kept as a minimal example. |
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
| Risk | Likelihood | Impact | Mitigation |
|
||||||
|
|------|-----------|--------|------------|
|
||||||
|
| crossbeam-queue doesn't compile for wasm32+atomics | Low | High | Test early in Stage 1. Fallback: thin wrapper over `VecDeque` behind cfg. |
|
||||||
|
| COOP/COEP headers break third-party integrations | Medium | Medium | Document clearly. This is the standard trade-off for SharedArrayBuffer apps. |
|
||||||
|
| Nightly Rust + build-std is unstable | Medium | Medium | Pin to a known-good nightly. Revisit when wasm-threads stabilizes. |
|
||||||
|
| Web Worker spawn latency | Low | Low | Spawn workers eagerly at runtime init, not per-actor. |
|
||||||
|
| Browser compatibility gaps | Low | Medium | Target Chrome/Firefox/Safari latest. All support SharedArrayBuffer since 2021. |
|
||||||
46
big-feature-phase/notes/state.md
Normal file
46
big-feature-phase/notes/state.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
## Current Task
|
||||||
|
Cycle 0 — Research
|
||||||
|
Step: Complete
|
||||||
|
Attempt: 1 of 3
|
||||||
|
|
||||||
|
## Key Files (read these first on resume)
|
||||||
|
- `big-feature-phase/TASK.md` — workflow rules
|
||||||
|
- `big-feature-phase/notes/constraints.md` — guardrails for in-browser runtime
|
||||||
|
- `big-feature-phase/notes/research_synthesis.md` — P0-P4 priority ranking
|
||||||
|
- `big-feature-phase/notes/feature-stages/01-platform-abstraction.md` — Stage 1 spec
|
||||||
|
- `src/runtime.rs` — thread spawn (line 351), park handles (line 110), Instant (line 205)
|
||||||
|
- `src/worker.rs` — backoff loop (lines 495-523), Instant timing (9 calls)
|
||||||
|
- `src/delivery.rs` — TickContext.worker_threads (line 246)
|
||||||
|
- `src/channel.rs` — crossbeam HybridChannel (validate wasm compilation)
|
||||||
|
- `crates/wasm/src/lib.rs` — existing PoC browser runtime
|
||||||
|
|
||||||
|
## Last Action & Result
|
||||||
|
Completed Cycle 0 research phase:
|
||||||
|
- Investigated similar projects (Lunatic, wasmCloud, Actix wasm attempts)
|
||||||
|
- Analyzed all platform-specific code in core swactor (4 blockers found)
|
||||||
|
- User confirmed: Rust-only actors, performance-first, Web Workers, all features, full peer
|
||||||
|
- Key decision: SharedArrayBuffer + wasm-threads (not postMessage isolation)
|
||||||
|
- Wrote constraints.md, research_synthesis.md, 6 feature-stage docs
|
||||||
|
|
||||||
|
## Next Action
|
||||||
|
Begin Stage 1 (Platform Abstraction Layer) — `notes/feature-stages/01-platform-abstraction.md`:
|
||||||
|
1. Add `web-time` dependency to core Cargo.toml
|
||||||
|
2. Replace `std::time::Instant` with `web_time::Instant` in runtime.rs and worker.rs
|
||||||
|
3. Implement `ParkHandle` abstraction with cfg gates
|
||||||
|
4. Gate `Runtime::run()` thread spawning for non-wasm
|
||||||
|
5. Validate crossbeam-queue compiles for wasm32 with atomics
|
||||||
|
6. Add `wasm` feature flag
|
||||||
|
|
||||||
|
## Completed This Session
|
||||||
|
- [x] Read TASK.md, state.md, history.md (context restart checklist)
|
||||||
|
- [x] Investigated similar codebases (Lunatic, wasmCloud, Actix-wasm)
|
||||||
|
- [x] Analyzed platform dependencies in core swactor
|
||||||
|
- [x] Asked and resolved design questions with user
|
||||||
|
- [x] Wrote `notes/constraints.md`
|
||||||
|
- [x] Wrote `notes/research_synthesis.md`
|
||||||
|
- [x] Created `notes/feature-stages/` with 6 stage docs
|
||||||
|
|
||||||
|
## Open Questions / Blockers
|
||||||
|
- Need to verify crossbeam-queue compiles for wasm32 with atomics (Stage 1 task)
|
||||||
|
- Need to find/verify correct `core::arch::wasm32` APIs for memory_atomic_wait/notify
|
||||||
|
- Nightly Rust toolchain + wasm32 target needs to be set up in CI
|
||||||
Loading…
Reference in a new issue