strata_js
Zero-V8 Hybrid JavaScript Runtime, Dynamic Typing, and Promise Event Loop in Safe Rust.
The strata_js crate provides a zero-dependency, pure-Rust execution layer that ports modern JavaScript frontend paradigms, dynamic typing (JsValue), array prototype methods, cooperative microtask scheduling, and standard Web APIs directly into safe Rust. It enables developers to write web-ergonomic code without bundling V8, WebKit, or JavaScriptCore.
Architecture Overview
Embedding standard JavaScript engines (such as Google V8 or JavaScriptCore) into cross-platform desktop or cloud infrastructure binaries incurs severe costs:
- Massive Binary Bloat: V8 adds 30MB to 50MB of compiled binary weight.
- JIT & Security Restrictions: iOS and various hardened server environments forbid executable memory (
W^X/ W^X violation on unsigned JIT pages). - Garbage Collection Pauses: Non-deterministic stop-the-world GC cycles induce frame stutter and latency spikes.
- Foreign Function Interface (FFI) Overhead: Marshaling data between Rust and V8 handles across the C-FFI barrier dominates CPU profiles.
strata_js implements a zero-V8 architecture:
- Tagged Union Dynamic Typing (
JsValue): Native Rust representations for JavaScript types with zero GC overhead. - Deterministic Microtask Scheduler: A thread-local queue executing Promise resolution callbacks cooperatively.
- Native Rust
FutureIntegration:JsPromise<T>directly implementsstd::future::Future, enabling seamless interop with Rust async/await runtimes.
flowchart TD
subgraph ExecutionPlane["strata_js Execution Engine"]
JsValueEnum["Tagged Union JsValue\n(Undefined | Null | Bool | Number | String | Array | Object)"]
PrototypeOps["Functional Array Prototype Operations\n(map, filter, reduce, find, slice, flat)"]
JsonEngine["Zero-Dependency JSON Parser & Stringifier"]
end
subgraph MicrotaskEventLoop["Microtask Event Loop"]
MicroQueue["MICROTASK_QUEUE (VecDeque)"]
PromiseState["JsPromise<T>\n(Pending | Fulfilled | Rejected)"]
DrainLoop["drain_microtasks()"]
end
subgraph RustAsyncInterOp["Rust Async Interoperability"]
RustFuture["impl Future<Output = Result<T, JsValue>>"]
AsyncAwait[".await syntax in native Rust async functions"]
end
JsValueEnum --> PrototypeOps
JsValueEnum --> JsonEngine
PromiseState --> MicroQueue
DrainLoop --> MicroQueue
PromiseState --> RustFuture
RustFuture --> AsyncAwaitDynamic Typing (JsValue)
The core of strata_js is the JsValue tagged union:
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub enum JsValue {
Undefined,
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<JsValue>),
Object(BTreeMap<String, JsValue>),
}Type Checking and Inspections
JsValue accurately replicates JavaScript specification behaviors, including historical language quirks:
use strata_js::prelude::*;
assert_eq!(JsValue::Undefined.js_typeof(), "undefined");
assert_eq!(JsValue::Null.js_typeof(), "object"); // Canonical JS specification quirk
assert_eq!(JsValue::Bool(true).js_typeof(), "boolean");
assert_eq!(JsValue::Number(42.0).js_typeof(), "number");
assert_eq!(JsValue::String("hello".into()).js_typeof(), "string");
assert_eq!(JsValue::Array(vec![]).js_typeof(), "object");
assert_eq!(JsValue::Object(BTreeMap::new()).js_typeof(), "object");Truthiness Evaluation (is_truthy)
Accurately adheres to ECMAScript truthy/falsy conventions:
- Falsy:
Undefined,Null,Bool(false),Number(0.0),Number(NaN),String(""). - Truthy:
Bool(true), non-zero/non-NaN numbers, non-empty strings, allArrays (even empty[]), allObjects (even empty{}).
use strata_js::prelude::*;
assert!(!JsValue::Undefined.is_truthy());
assert!(!JsValue::Null.is_truthy());
assert!(!JsValue::Number(0.0).is_truthy());
assert!(!JsValue::Number(f64::NAN).is_truthy());
assert!(!JsValue::String("".into()).is_truthy());
assert!(JsValue::Bool(true).is_truthy());
assert!(JsValue::Number(-1.0).is_truthy());
assert!(JsValue::String("0".into()).is_truthy());
assert!(JsValue::Array(vec![]).is_truthy()); // In JS, [] is truthyEquality Semantics (strict_eq vs loose_eq)
strata_js provides explicit implementations for both JavaScript strict equality (===) and abstract loose equality (==).
flowchart TD
Compare["Compare: a.loose_eq(b)"] --> CheckSameType{"Same Type?"}
CheckSameType -->|Yes| StrictComp["Delegate to strict_eq (===)"]
CheckSameType -->|No| CheckNullUndefined{"null == undefined?"}
CheckNullUndefined -->|Yes| TrueRet["Return true"]
CheckNullUndefined -->|No| CheckNumStr{"Number == String?"}
CheckNumStr -->|Yes| ParseStr["Parse String as f64 -> compare"]
CheckNumStr -->|No| CheckBool{"Either is Boolean?"}
CheckBool -->|Yes| CastBool["Coerce Bool to 1.0 or 0.0 -> retry loose_eq"]
CheckBool -->|No| CheckArray{"Array == String/Number?"}
CheckArray -->|Yes| JoinArr["Unwrap/Join Array by ',' -> retry loose_eq"]
CheckArray -->|No| FalseRet["Return false"]Strict Equality (strict_eq / ===)
- Different types return
false. NaN === NaNreturnsfalse(standard IEEE 754 invariance).- Arrays and Objects undergo deep structural equality.
Abstract Loose Equality (loose_eq / ==)
null == undefinedevaluates totrue.- Compares numbers and strings by parsing the string to
f64. - Booleans are coerced to
1.0(true) or0.0(false). - Arrays are joined with commas when compared against primitives.
use strata_js::prelude::*;
// Strict Equality (===)
assert!(JsValue::Null.strict_eq(&JsValue::Null));
assert!(!JsValue::Null.strict_eq(&JsValue::Undefined));
assert!(!JsValue::Number(f64::NAN).strict_eq(&JsValue::Number(f64::NAN)));
// Loose Equality (==)
assert!(JsValue::Null.loose_eq(&JsValue::Undefined));
assert!(JsValue::Number(42.0).loose_eq(&JsValue::String("42".into())));
assert!(JsValue::Bool(true).loose_eq(&JsValue::Number(1.0)));
assert!(JsValue::Bool(true).loose_eq(&JsValue::String("1".into())));JavaScript Array Prototype Methods
All common JavaScript array prototype functions are implemented directly on JsValue:
use strata_js::prelude::*;
let numbers = JsValue::Array(vec![
JsValue::Number(1.0),
JsValue::Number(2.0),
JsValue::Number(3.0),
JsValue::Number(4.0),
JsValue::Number(5.0),
]);
// Array.prototype.map
let doubled = numbers.map(|x| JsValue::Number(x.as_f64().unwrap() * 2.0));
// Array.prototype.filter
let evens = numbers.filter(|x| x.as_f64().unwrap() % 2.0 == 0.0);
// Array.prototype.reduce
let sum = numbers.reduce(|acc, item| {
JsValue::Number(acc.as_f64().unwrap() + item.as_f64().unwrap())
}, JsValue::Number(0.0));
assert_eq!(sum.as_f64(), Some(15.0));
// Array.prototype.find and findIndex
let found = numbers.find(|x| x.as_f64() == Some(3.0));
assert_eq!(found, Some(&JsValue::Number(3.0)));
let found_idx = numbers.find_index(|x| x.as_f64() == Some(3.0));
assert_eq!(found_idx, Some(2));
// Array.prototype.slice, join, flat
assert_eq!(numbers.slice(1, Some(3)).len(), 2);
assert_eq!(numbers.join(", "), "1, 2, 3, 4, 5");Static Object Utilities (JsObject)
The JsObject utility mimics static methods on the JavaScript Object constructor:
use strata_js::prelude::*;
use std::collections::BTreeMap;
let mut obj = JsValue::Object(BTreeMap::new());
obj.set_property("cluster_name", "us-east-prod");
obj.set_property("nodes", 64.0);
// Object.keys
let keys: Vec<String> = JsObject::keys(&obj);
assert_eq!(keys, vec!["cluster_name".to_string(), "nodes".to_string()]);
// Object.values
let vals: Vec<JsValue> = JsObject::values(&obj);
// Object.entries
let entries: Vec<(String, JsValue)> = JsObject::entries(&obj);
// Object.assign
let mut target = JsValue::Object(BTreeMap::new());
JsObject::assign(&mut target, &obj);
assert_eq!(target["cluster_name"].as_str(), Some("us-east-prod"));Cooperative Microtask Queue & JsPromise<T>
strata_js implements a cooperative microtask scheduler. Promises resolve their callbacks via the thread-local microtask queue, guaranteeing deterministic scheduling order identical to the JavaScript event loop.
use strata_js::prelude::*;
// 1. Queue a manual microtask
queue_microtask(|| {
println!("Executed inside microtask checkpoint");
});
// 2. Create a Promise with resolve / reject callbacks
let promise: JsPromise<String> = JsPromise::new(|resolve, _reject| {
resolve("Cluster Initialized".to_string());
});
// 3. Chain with then()
let chained = promise
.then(|val| format!("{val} -> Metrics Active"))
.then(|val| format!("{val} -> Ready"));
// 4. Drain microtasks to settle chained callbacks
let tasks_fired = drain_microtasks();
println!("Executed {tasks_fired} microtasks");
assert_eq!(chained.state(), PromiseState::Fulfilled("Cluster Initialized -> Metrics Active -> Ready".to_string()));Promise Combinators (Promise.all & Promise.race)
use strata_js::prelude::*;
let p1 = JsPromise::resolve(10);
let p2 = JsPromise::resolve(20);
let p3 = JsPromise::resolve(30);
// Promise.all: Resolves when all resolve, rejects if any rejects
let all_promise = JsPromise::all(vec![p1, p2, p3]);
drain_microtasks();
assert_eq!(all_promise.state(), PromiseState::Fulfilled(vec![10, 20, 30]));Native Rust Future Implementation
Because JsPromise<T> implements std::future::Future, promises can be directly awaited inside standard Rust asynchronous tasks:
use strata_js::prelude::*;
async fn initialize_subsystems() -> Result<String, JsValue> {
let promise = JsPromise::resolve("Telemetry Online".to_string());
// Direct .await on JsPromise
let result = promise.await?;
Ok(result)
}Zero-Dependency JSON Parser and Serializer
The crate includes an allocation-conscious recursive descent parser for JSON without external dependencies:
use strata_js::prelude::*;
let json_text = r#"{"name": "worker-4", "active": true, "load": 0.42}"#;
// Parse directly to JsValue
let val = parse_json(json_text).expect("Valid JSON");
assert_eq!(val["name"].as_str(), Some("worker-4"));
assert_eq!(val["active"].as_bool(), Some(true));
// Serialize back to JSON
let output_json = stringify_json(&val);High-Resolution Timers (setTimeout & setInterval)
Implements the standard Web Timers specification using a thread-local scheduler with support for deterministic time dilation:
use strata_js::prelude::*;
use std::time::Duration;
use std::rc::Rc;
use std::cell::Cell;
let counter = Rc::new(Cell::new(0));
let counter_clone = Rc::clone(&counter);
let timer_id = set_timeout(move || {
counter_clone.set(counter_clone.get() + 1);
}, 500);
// Advance simulated clock instantly without sleep (essential for instant unit testing)
let fired = advance_timers_by(Duration::from_millis(500));
assert_eq!(fired, 1);
assert_eq!(counter.get(), 1);