diff --git a/Cargo.lock b/Cargo.lock index 1e99605..52eef1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2723,6 +2723,7 @@ dependencies = [ "serde", "swactor-std", "tracing", + "web-time", ] [[package]] @@ -3735,6 +3736,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 19c2995..2634ddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,11 +22,13 @@ serde = ["dep:serde"] tracing = ["dep:tracing"] no_random = [] # compile without access to a source of randomness transport = [] # transport-agnostic messaging (no mandatory deps; codec is user-provided) +wasm = ["no_random", "dep:web-time"] # browser/wasm32 target support [dependencies] getrandom = { version = "0.2", optional = true } serde = { version = "1", features = ["derive"], optional = true } tracing = { version = "0.1", optional = true } +web-time = { version = "0.2", optional = true } crossbeam-queue = "0.3.12" crossbeam-utils = "0.8.21" diff --git a/big-feature-phase/notes/history.md b/big-feature-phase/notes/history.md index 67e6a3f..abc909b 100644 --- a/big-feature-phase/notes/history.md +++ b/big-feature-phase/notes/history.md @@ -2,3 +2,6 @@ ## 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. + +## Stage 1 — Platform Abstraction (complete) +Added `wasm` feature + `web-time` dep. Replaced `std::time::Instant` → `crate::Instant` (cfg-gated re-export). Gated `Runtime::run()` and `RuntimeHandle` for non-wasm. Key finding: only `thread::spawn` needed gating — park/unpark/yield/Mutex/RwLock/atomics/crossbeam all work on wasm32 with atomics. Cleaned unused Mutex import in worker.rs. Updated crates/wasm/ to use `wasm` feature. All native tests pass, wasm32 compilation succeeds. Files changed: Cargo.toml, src/lib.rs, src/runtime.rs, src/worker.rs, crates/wasm/Cargo.toml. diff --git a/big-feature-phase/notes/state.md b/big-feature-phase/notes/state.md index e0f952a..df07bba 100644 --- a/big-feature-phase/notes/state.md +++ b/big-feature-phase/notes/state.md @@ -1,46 +1,47 @@ ## Current Task -Cycle 0 — Research +Stage 1 — Platform Abstraction Layer 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 +- `big-feature-phase/notes/constraints.md` — guardrails +- `big-feature-phase/notes/feature-stages/02-single-worker-browser.md` — Stage 2 spec +- `Cargo.toml` — `wasm` feature flag, `web-time` dep (lines 23, 30) +- `src/lib.rs` — platform-aware `Instant` re-export (lines 20-24) +- `src/runtime.rs` — cfg-gated `run()` (line 340) and `RuntimeHandle` (line 73) +- `crates/wasm/Cargo.toml` — now uses `features = ["wasm"]` +- `docs/development_history/in-browser/PLATFORM_ABSTRACTION.md` — what was done ## 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 +Completed Stage 1 (Platform Abstraction Layer): +- Added `web-time` dep + `wasm` feature (`no_random` + `web-time`) +- Replaced `std::time::Instant` → `crate::Instant` in runtime.rs, worker.rs +- cfg-gated `Runtime::run()` and `RuntimeHandle` for `not(target_arch = "wasm32")` +- Removed unused `Mutex` import from worker.rs +- Updated `crates/wasm/` to use `wasm` feature +- Key finding: most std::sync/thread primitives work on wasm32 with atomics; only `thread::spawn` needed gating +- All native tests pass, wasm32 compilation succeeds ## 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 +Begin Stage 2 (Single-Worker Browser Runtime) — `notes/feature-stages/02-single-worker-browser.md`: +1. Create `crates/wasm-browser/` crate structure +2. Implement `BrowserRuntime` wasm-bindgen API (spawn, send, tick, try_recv, stats) +3. Actor registration macro/pattern for JS-accessible spawning +4. Auto-scheduling via setTimeout(0) loop +5. Message serialization across JS↔Wasm boundary +6. Tests (wasm-pack test or Node.js) ## 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 +- [x] Cycle 0 research artifacts (constraints.md, research_synthesis.md, 6 stage docs) +- [x] Stage 1: `web-time` dep + `wasm` feature flag in Cargo.toml +- [x] Stage 1: Platform-aware `Instant` re-export in src/lib.rs +- [x] Stage 1: cfg-gated `Runtime::run()` and `RuntimeHandle` in src/runtime.rs +- [x] Stage 1: Updated crates/wasm/ to use `wasm` feature +- [x] Stage 1: Validated wasm32 compilation and native tests +- [x] Stage 1: Development history doc ## 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 +- Stage 2: Need to decide on message serialization (serde-wasm-bindgen vs raw bytes) +- Stage 2: Actor registration pattern — macro vs manual factory map +- Stage 3: Web Worker thread state initialization needs investigation (does std::thread::current() work in a Web Worker context?) diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml index f4fda2e..445b85c 100644 --- a/crates/wasm/Cargo.toml +++ b/crates/wasm/Cargo.toml @@ -7,5 +7,5 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -swactor = { path = "../..", default-features = false, features = ["no_random"] } +swactor = { path = "../..", default-features = false, features = ["wasm"] } wasm-bindgen = "0.2" diff --git a/docs/development_history/in-browser/PLATFORM_ABSTRACTION.md b/docs/development_history/in-browser/PLATFORM_ABSTRACTION.md new file mode 100644 index 0000000..30738ed --- /dev/null +++ b/docs/development_history/in-browser/PLATFORM_ABSTRACTION.md @@ -0,0 +1,82 @@ +# Platform Abstraction Layer — Development History + +> Stage 1 of the in-browser swactor runtime. Makes core swactor compile for +> `wasm32-unknown-unknown` without behavioral changes on native targets. + +--- + +## Changes + +### 1. `web-time` dependency + `wasm` feature flag + +**File**: `Cargo.toml` + +Added `web-time` as an optional dependency and a `wasm` feature that bundles +`no_random` + `web-time`: + +```toml +wasm = ["no_random", "dep:web-time"] +web-time = { version = "0.2", optional = true } +``` + +`web-time` is a drop-in replacement for `std::time::Instant`: +- Native: re-exports `std::time::Instant` (zero-cost) +- wasm32: uses `performance.now()` via `js-sys` + +### 2. Platform-aware `Instant` re-export + +**File**: `src/lib.rs` + +```rust +#[cfg(feature = "wasm")] +pub(crate) use web_time::Instant; +#[cfg(not(feature = "wasm"))] +pub(crate) use std::time::Instant; +``` + +All modules (`runtime.rs`, `worker.rs`) now use `crate::Instant` instead of +`std::time::Instant`. Single point of truth — no cfg noise in consumer code. + +### 3. cfg-gated `Runtime::run()` and `RuntimeHandle` + +**File**: `src/runtime.rs` + +`Runtime::run()` calls `std::thread::spawn()` which is not available on wasm32. +Both `run()` and `RuntimeHandle` (which holds `JoinHandle<()>`) are gated: + +```rust +#[cfg(not(target_arch = "wasm32"))] +pub fn run(self) -> Result { ... } +``` + +On wasm32, the browser crate will provide its own `run()` via Web Workers. +`tick()` remains available on all platforms for single-threaded driving. + +### 4. Updated `crates/wasm/` to use `wasm` feature + +**File**: `crates/wasm/Cargo.toml` + +Changed from `features = ["no_random"]` to `features = ["wasm"]` to pick up +the `web-time` Instant on wasm32. + +## What Did NOT Need Abstraction + +Key discovery: on wasm32 with the `+atomics` target feature, most of +`std::sync` and `std::thread` works: + +- `OnceLock` — compiles and works (futex-based) +- `Thread::unpark()` — works (futex → `memory.atomic.notify`) +- `thread::park_timeout()` — works (futex → `memory.atomic.wait32`) +- `thread::yield_now()` — works (no-op on wasm) +- `Mutex`, `RwLock` — work (futex-based) +- `crossbeam-queue` — works (uses `core::sync::atomic`) +- `AtomicBool/Usize/U64` — work (wasm atomic instructions) + +Only `std::thread::spawn()` and `JoinHandle` are not functional on wasm32. + +## Verification + +- `cargo test` — all native tests pass (no regressions) +- `cargo test --features wasm` — all native tests pass with wasm feature +- `cargo build --target wasm32-unknown-unknown --features wasm --no-default-features` — compiles +- `cargo build --target wasm32-unknown-unknown -p wasm` — existing PoC crate compiles diff --git a/src/lib.rs b/src/lib.rs index 146ff90..00d9f72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,13 @@ pub mod runtime; #[cfg(feature = "transport")] pub mod transport; +// Platform-aware Instant: web_time on wasm, std::time on native. +// web_time is a no-op re-export of std::time::Instant on non-wasm targets. +#[cfg(feature = "wasm")] +pub(crate) use web_time::Instant; +#[cfg(not(feature = "wasm"))] +pub(crate) use std::time::Instant; + #[cfg(feature = "getrandom")] pub(crate) fn get_random(buf: &mut [u8]) { getrandom::getrandom(buf).unwrap() diff --git a/src/runtime.rs b/src/runtime.rs index dd65928..a412f91 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -2,8 +2,10 @@ use std::any::Any; use std::cell::RefCell; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; -use std::thread::{self, JoinHandle, Thread}; -use std::time::Instant; +#[cfg(not(target_arch = "wasm32"))] +use std::thread::{self, JoinHandle}; +use std::thread::Thread; +use crate::Instant; use crate::actor::{Actor, ActorAddress, ActorExited, ActorInterface, AnyActor, ExitReason, Message, StopSignal, TimerRequest}; use crate::channel::{Receiver, Sender}; @@ -67,11 +69,13 @@ impl Ask { } /// Handle for dealing with a runtime that has started via the `Runtime::run()` method. +#[cfg(not(target_arch = "wasm32"))] pub struct RuntimeHandle { pub runtime: Arc, threads: Vec>, } +#[cfg(not(target_arch = "wasm32"))] impl RuntimeHandle { pub fn join(self) { for handle in self.threads { @@ -333,6 +337,9 @@ impl Runtime { /// /// Works in both single-threaded and multi-threaded configurations. /// In single-threaded mode, one background thread is spawned. + /// + /// Not available on wasm32 — use the browser crate's Web Worker-based run instead. + #[cfg(not(target_arch = "wasm32"))] pub fn run(self) -> Result { self.is_running.store(true, Ordering::Release); diff --git a/src/worker.rs b/src/worker.rs index 901fc99..446dc66 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -2,9 +2,9 @@ use std::any::Any; use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::thread; -use std::time::Instant; +use crate::Instant; use crate::actor::{ActorAddress, ActorExited, AnyActor, CloneMsg, ContextInner, Ctx, ExitReason, StopReason, StopSignal, TimerRequest}; use crate::channel::Receiver;