feat: skeleton for in browser swactor engine #35
9 changed files with 150 additions and 37 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
82
docs/development_history/in-browser/PLATFORM_ABSTRACTION.md
Normal file
82
docs/development_history/in-browser/PLATFORM_ABSTRACTION.md
Normal file
|
|
@ -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<RuntimeHandle, Error> { ... }
|
||||
```
|
||||
|
||||
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<Thread>` — 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<R: Message> Ask<R> {
|
|||
}
|
||||
|
||||
/// 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<Runtime>,
|
||||
threads: Vec<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[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<RuntimeHandle, Error> {
|
||||
self.is_running.store(true, Ordering::Release);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue