use crate::host::{with_host, IoTask, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Mutex, OnceLock};
pub const METHODS: &[&str] = &[
"getEnvironmentData",
"setEnvironmentData",
"receiveMessageOnPort",
"markAsUntransferable",
"isMarkedAsUntransferable",
"markAsUncloneable",
"moveMessagePortToContext",
];
pub const BROADCAST_CHANNEL_METHODS: &[&str] = &[
"postMessage",
"close",
"ref",
"unref",
"addEventListener",
"removeEventListener",
];
pub const WORKER_METHODS: &[&str] = &["postMessage", "terminate", "ref", "unref"];
pub const PORT_METHODS: &[&str] = &["postMessage", "close", "start", "ref", "unref"];
static NEXT_THREAD_ID: AtomicU64 = AtomicU64::new(1);
enum WorkerMsg {
Data(String),
Terminate,
}
enum MainEvent {
Online,
Message(String),
Error(String),
Exit(i32),
}
struct WorkerRec {
emitter: Value,
to_worker: Sender<WorkerMsg>,
}
thread_local! {
static WORKERS: RefCell<HashMap<u64, WorkerRec>> = RefCell::new(HashMap::new());
}
struct WorkerCtx {
thread_id: u64,
worker_data_json: String,
main_tx: Sender<IoTask>,
self_id: u64,
rx: Option<Receiver<WorkerMsg>>,
bridge_started: bool,
}
thread_local! {
static WORKER_CTX: RefCell<Option<WorkerCtx>> = const { RefCell::new(None) };
static PARENT_PORT: RefCell<Option<Value>> = const { RefCell::new(None) };
static CHANNEL_PORTS: RefCell<HashMap<u64, Value>> = RefCell::new(HashMap::new());
static CH_PEER: RefCell<HashMap<u64, u64>> = RefCell::new(HashMap::new());
static CH_QUEUE: RefCell<HashMap<u64, VecDeque<String>>> = RefCell::new(HashMap::new());
static CH_STARTED: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
static BCAST: RefCell<HashMap<String, Vec<(u64, Value)>>> = RefCell::new(HashMap::new());
static UNTRANSFERABLE: RefCell<HashSet<u32>> = RefCell::new(HashSet::new());
static UNCLONEABLE: RefCell<HashSet<u32>> = RefCell::new(HashSet::new());
}
fn env_data() -> &'static Mutex<HashMap<String, String>> {
static ENV_DATA: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
ENV_DATA.get_or_init(|| Mutex::new(HashMap::new()))
}
static NEXT_PORT_ID: AtomicU64 = AtomicU64::new(1);
fn serialize(v: &Value) -> Result<String, String> {
let arr = with_host(|h| h.new_array(vec![v.clone()]));
let json = crate::builtins::call_builtin_function("JSON.stringify", vec![arr])?;
Ok(with_host(|h| h.str_of(&json)))
}
fn deserialize(json: &str) -> Result<Value, String> {
let sv = with_host(|h| h.new_str(json.to_string()));
let arr = crate::builtins::call_builtin_function("JSON.parse", vec![sv])?;
Ok(with_host(|h| match h.get(&arr) {
Some(JsObj::Array(items)) => items.first().cloned().unwrap_or(Value::Undef),
_ => Value::Undef,
}))
}
fn arg0(args: &[Value]) -> Value {
args.first().cloned().unwrap_or(Value::Undef)
}
fn get_prop(recv: &Value, key: &str) -> Option<Value> {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get(key).cloned(),
_ => None,
})
}
fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
}
fn emit_event(emitter: &Value, name: &str, mut args: Vec<Value>) -> Result<(), String> {
let mut a = vec![with_host(|h| h.new_str(name))];
a.append(&mut args);
super::events::instance_call(emitter, "emit", a).map(|_| ())
}
fn is_worker_thread() -> bool {
WORKER_CTX.with(|c| c.borrow().is_some())
}
fn current_thread_id() -> u64 {
WORKER_CTX.with(|c| c.borrow().as_ref().map(|x| x.thread_id).unwrap_or(0))
}
fn current_worker_data() -> Value {
let json = WORKER_CTX.with(|c| c.borrow().as_ref().map(|x| x.worker_data_json.clone()));
match json {
Some(j) => deserialize(&j).unwrap_or(Value::Undef),
None => Value::Undef,
}
}
fn ensure_parent_port() -> Value {
if let Some(p) = PARENT_PORT.with(|p| p.borrow().clone()) {
return p;
}
let port = super::net::new_emitter_object("MessagePort", IndexMap::new());
PARENT_PORT.with(|p| *p.borrow_mut() = Some(port.clone()));
port
}
pub fn constant(name: &str) -> Option<Value> {
match name {
"isMainThread" => Some(Value::Bool(!is_worker_thread())),
"threadId" => Some(Value::Float(current_thread_id() as f64)),
"parentPort" => Some(if is_worker_thread() {
ensure_parent_port()
} else {
with_host(|h| h.null())
}),
"workerData" => Some(current_worker_data()),
"Worker" | "MessageChannel" | "BroadcastChannel" => {
Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
}
_ => None,
}
}
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"setEnvironmentData" => {
let key = super::arg_str(args, 0);
match args.get(1) {
Some(v) if !matches!(v, Value::Undef) => match serialize(v) {
Ok(json) => {
if let Ok(mut m) = env_data().lock() {
m.insert(key, json);
}
Ok(Value::Undef)
}
Err(e) => Err(e),
},
_ => {
if let Ok(mut m) = env_data().lock() {
m.remove(&key);
}
Ok(Value::Undef)
}
}
}
"getEnvironmentData" => {
let key = super::arg_str(args, 0);
let json = env_data().lock().ok().and_then(|m| m.get(&key).cloned());
match json {
Some(j) => deserialize(&j),
None => Ok(Value::Undef),
}
}
"receiveMessageOnPort" => Ok(receive_message_on_port(args.first())),
"markAsUntransferable" => {
if let Some(Value::Obj(id)) = args.first() {
UNTRANSFERABLE.with(|s| s.borrow_mut().insert(*id));
}
Ok(Value::Undef)
}
"isMarkedAsUntransferable" => Ok(Value::Bool(matches!(
args.first(),
Some(Value::Obj(id)) if UNTRANSFERABLE.with(|s| s.borrow().contains(id))
))),
"markAsUncloneable" => {
if let Some(Value::Obj(id)) = args.first() {
UNCLONEABLE.with(|s| s.borrow_mut().insert(*id));
}
Ok(Value::Undef)
}
"moveMessagePortToContext" => Ok(arg0(args)),
_ => return None,
})
}
pub fn construct_worker(args: &[Value]) -> Result<Value, String> {
let filename = with_host(|h| h.str_of(&arg0(args)));
let opts = args.get(1).cloned();
let is_eval = opts
.as_ref()
.and_then(|o| get_prop(o, "eval"))
.map(|v| with_host(|h| h.truthy(&v)))
.unwrap_or(false);
let worker_data_json = match opts.as_ref().and_then(|o| get_prop(o, "workerData")) {
Some(v) => serialize(&v)?,
None => serialize(&Value::Undef)?, };
let id = NEXT_THREAD_ID.fetch_add(1, Ordering::SeqCst);
let (to_worker_tx, to_worker_rx) = std::sync::mpsc::channel::<WorkerMsg>();
let main_tx = with_host(|h| h.io_sender());
let mut extra = IndexMap::new();
extra.insert("@@wtid".into(), Value::Float(id as f64));
extra.insert("threadId".into(), Value::Float(id as f64));
let emitter = super::net::new_emitter_object("Worker", extra);
WORKERS.with(|w| {
w.borrow_mut().insert(
id,
WorkerRec {
emitter: emitter.clone(),
to_worker: to_worker_tx,
},
);
});
with_host(|h| h.incr_handle());
let spawn_tx = main_tx.clone();
std::thread::spawn(move || {
worker_thread_main(
id,
filename,
is_eval,
worker_data_json,
spawn_tx,
to_worker_rx,
);
});
Ok(emitter)
}
fn worker_thread_main(
id: u64,
filename: String,
is_eval: bool,
worker_data_json: String,
main_tx: Sender<IoTask>,
rx: Receiver<WorkerMsg>,
) {
WORKER_CTX.with(|c| {
*c.borrow_mut() = Some(WorkerCtx {
thread_id: id,
worker_data_json,
main_tx: main_tx.clone(),
self_id: id,
rx: Some(rx),
bridge_started: false,
});
});
post_to_main(&main_tx, id, MainEvent::Online);
let outcome = if is_eval {
crate::eval_str(&filename)
} else {
crate::eval_file(&filename)
};
match outcome {
Ok(_) => post_to_main(&main_tx, id, MainEvent::Exit(0)),
Err(e) => {
post_to_main(&main_tx, id, MainEvent::Error(e));
post_to_main(&main_tx, id, MainEvent::Exit(1));
}
}
}
fn post_to_main(main_tx: &Sender<IoTask>, id: u64, ev: MainEvent) {
let _ = main_tx.send(Box::new(move || dispatch_main(id, ev)));
}
fn dispatch_main(id: u64, ev: MainEvent) -> Result<(), String> {
let emitter = WORKERS.with(|w| w.borrow().get(&id).map(|r| r.emitter.clone()));
let Some(emitter) = emitter else {
return Ok(());
};
match ev {
MainEvent::Online => emit_event(&emitter, "online", vec![]),
MainEvent::Message(json) => {
let v = deserialize(&json)?;
emit_event(&emitter, "message", vec![v])
}
MainEvent::Error(msg) => {
let err =
crate::builtins::construct_builtin("Error", vec![with_host(|h| h.new_str(msg))])?;
emit_event(&emitter, "error", vec![err])
}
MainEvent::Exit(code) => {
emit_event(&emitter, "exit", vec![Value::Float(code as f64)])?;
WORKERS.with(|w| {
w.borrow_mut().remove(&id);
});
with_host(|h| h.decr_handle());
let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
Ok(())
}
}
}
fn start_parent_bridge() {
let worker_io: Option<Sender<IoTask>> = WORKER_CTX.with(|c| {
let mut cb = c.borrow_mut();
let ctx = cb.as_mut()?;
if ctx.bridge_started {
return None;
}
let rx = ctx.rx.take()?;
ctx.bridge_started = true;
let io = with_host(|h| h.io_sender());
with_host(|h| h.incr_handle());
let io_for_thread = io.clone();
std::thread::spawn(move || {
while let Ok(msg) = rx.recv() {
match msg {
WorkerMsg::Data(json) => {
let _ = io_for_thread.send(Box::new(move || parent_deliver(json)));
}
WorkerMsg::Terminate => {
let _ = io_for_thread.send(Box::new(|| {
with_host(|h| h.decr_handle());
Ok(())
}));
break;
}
}
}
});
Some(io)
});
let _ = worker_io;
}
fn parent_deliver(json: String) -> Result<(), String> {
let port = ensure_parent_port();
let v = deserialize(&json)?;
emit_event(&port, "message", vec![v])
}
const EMITTER_METHODS: &[&str] = super::events::METHODS;
pub fn instance_call(
tag: &str,
recv: &Value,
method: &str,
args: Vec<Value>,
) -> Result<Value, String> {
match tag {
"Worker" => worker_call(recv, method, args),
"MessagePort" => port_call(recv, method, args),
"BroadcastChannel" => broadcast_call(recv, method, args),
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
fn worker_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
if EMITTER_METHODS.contains(&method) {
return super::events::instance_call(recv, method, args);
}
match method {
"postMessage" => {
let json = serialize(&arg0(&args))?;
if let Some(id) = u64_prop(recv, "@@wtid") {
WORKERS.with(|w| {
if let Some(r) = w.borrow().get(&id) {
let _ = r.to_worker.send(WorkerMsg::Data(json));
}
});
}
Ok(Value::Undef)
}
"terminate" => {
if let Some(id) = u64_prop(recv, "@@wtid") {
WORKERS.with(|w| {
if let Some(r) = w.borrow().get(&id) {
let _ = r.to_worker.send(WorkerMsg::Terminate);
}
});
}
Ok(Value::Undef)
}
"ref" | "unref" => Ok(recv.clone()),
_ => Err(crate::host::type_error(&format!(
"worker.{method} is not a function"
))),
}
}
fn port_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
if let Some(pid) = u64_prop(recv, "@@portid") {
return channel_port_call(recv, pid, method, args);
}
if EMITTER_METHODS.contains(&method) {
let r = super::events::instance_call(recv, method, args.clone());
if matches!(
method,
"on" | "addListener" | "prependListener" | "once" | "prependOnceListener"
) {
let ev = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
if ev == "message" {
start_parent_bridge();
}
}
return r;
}
match method {
"postMessage" => {
let json = serialize(&arg0(&args))?;
WORKER_CTX.with(|c| {
if let Some(ctx) = c.borrow().as_ref() {
post_to_main(&ctx.main_tx, ctx.self_id, MainEvent::Message(json));
}
});
Ok(Value::Undef)
}
"start" => {
start_parent_bridge();
Ok(Value::Undef)
}
"close" | "ref" | "unref" => Ok(recv.clone()),
_ => Err(crate::host::type_error(&format!(
"port.{method} is not a function"
))),
}
}
pub fn construct_message_channel(_args: &[Value]) -> Result<Value, String> {
let id1 = NEXT_PORT_ID.fetch_add(1, Ordering::SeqCst);
let id2 = NEXT_PORT_ID.fetch_add(1, Ordering::SeqCst);
let mut e1 = IndexMap::new();
e1.insert("@@portid".into(), Value::Float(id1 as f64));
let port1 = super::net::new_emitter_object("MessagePort", e1);
let mut e2 = IndexMap::new();
e2.insert("@@portid".into(), Value::Float(id2 as f64));
let port2 = super::net::new_emitter_object("MessagePort", e2);
CHANNEL_PORTS.with(|m| {
let mut m = m.borrow_mut();
m.insert(id1, port1.clone());
m.insert(id2, port2.clone());
});
CH_PEER.with(|m| {
let mut m = m.borrow_mut();
m.insert(id1, id2);
m.insert(id2, id1);
});
Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("port1".into(), port1);
m.insert("port2".into(), port2);
h.new_object(m)
}))
}
fn channel_port_call(
recv: &Value,
pid: u64,
method: &str,
args: Vec<Value>,
) -> Result<Value, String> {
if EMITTER_METHODS.contains(&method) {
let r = super::events::instance_call(recv, method, args.clone());
if matches!(
method,
"on" | "addListener" | "prependListener" | "once" | "prependOnceListener"
) {
let ev = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
if ev == "message" {
start_channel_port(pid);
}
}
return r;
}
match method {
"postMessage" => {
let json = serialize(&arg0(&args))?;
channel_post(pid, json);
Ok(Value::Undef)
}
"start" => {
start_channel_port(pid);
Ok(Value::Undef)
}
"close" => {
CH_STARTED.with(|s| {
s.borrow_mut().remove(&pid);
});
Ok(Value::Undef)
}
"ref" | "unref" => Ok(recv.clone()),
_ => Err(crate::host::type_error(&format!(
"port.{method} is not a function"
))),
}
}
fn channel_post(from: u64, json: String) {
let Some(peer) = CH_PEER.with(|m| m.borrow().get(&from).copied()) else {
return;
};
CH_QUEUE.with(|q| q.borrow_mut().entry(peer).or_default().push_back(json));
if CH_STARTED.with(|s| s.borrow().contains(&peer)) {
schedule_channel_delivery(peer);
}
}
fn start_channel_port(pid: u64) {
let newly = CH_STARTED.with(|s| s.borrow_mut().insert(pid));
if !newly {
return;
}
let pending = CH_QUEUE.with(|q| q.borrow().get(&pid).map_or(0, |d| d.len()));
for _ in 0..pending {
schedule_channel_delivery(pid);
}
}
fn schedule_channel_delivery(pid: u64) {
with_host(|h| h.incr_handle());
let io = with_host(|h| h.io_sender());
let _ = io.send(Box::new(move || channel_deliver(pid)));
}
fn channel_deliver(pid: u64) -> Result<(), String> {
let json = CH_QUEUE.with(|q| q.borrow_mut().get_mut(&pid).and_then(|d| d.pop_front()));
if let Some(json) = json {
if let Some(port) = CHANNEL_PORTS.with(|m| m.borrow().get(&pid).cloned()) {
match deserialize(&json) {
Ok(v) => {
if let Err(e) = emit_event(&port, "message", vec![v]) {
eprintln!("{e}");
}
}
Err(e) => eprintln!("{e}"),
}
}
}
with_host(|h| h.decr_handle());
Ok(())
}
fn receive_message_on_port(port: Option<&Value>) -> Value {
let Some(port) = port else {
return Value::Undef;
};
let Some(pid) = u64_prop(port, "@@portid") else {
return Value::Undef;
};
let json = CH_QUEUE.with(|q| q.borrow_mut().get_mut(&pid).and_then(|d| d.pop_front()));
match json {
Some(j) => match deserialize(&j) {
Ok(v) => with_host(|h| {
let mut m = IndexMap::new();
m.insert("message".into(), v);
h.new_object(m)
}),
Err(_) => Value::Undef,
},
None => Value::Undef,
}
}
pub fn construct_broadcast_channel(args: &[Value]) -> Result<Value, String> {
let name = with_host(|h| h.str_of(&arg0(args)));
let id = NEXT_PORT_ID.fetch_add(1, Ordering::SeqCst);
let obj = with_host(|h| {
let listeners = h.new_array(Vec::new());
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("BroadcastChannel"));
m.insert("@@bcid".into(), Value::Float(id as f64));
m.insert("@@bcname".into(), h.new_str(name.clone()));
m.insert("@@listeners".into(), listeners);
m.insert("@@refed".into(), Value::Bool(true));
m.insert("name".into(), h.new_str(name.clone()));
m.insert("onmessage".into(), h.null());
m.insert("onmessageerror".into(), h.null());
h.new_object(m)
});
BCAST.with(|b| {
b.borrow_mut()
.entry(name)
.or_default()
.push((id, obj.clone()))
});
with_host(|h| h.incr_handle());
Ok(obj)
}
fn broadcast_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
match method {
"postMessage" => {
let json = serialize(&arg0(&args))?;
let name = str_prop(recv, "@@bcname");
let self_id = u64_prop(recv, "@@bcid");
let targets: Vec<Value> = BCAST.with(|b| match b.borrow().get(&name) {
Some(list) => list
.iter()
.filter(|(id, _)| Some(*id) != self_id)
.map(|(_, v)| v.clone())
.collect(),
None => Vec::new(),
});
for t in targets {
schedule_broadcast_delivery(t, json.clone());
}
Ok(Value::Undef)
}
"close" => {
let name = str_prop(recv, "@@bcname");
let self_id = u64_prop(recv, "@@bcid");
BCAST.with(|b| {
if let Some(list) = b.borrow_mut().get_mut(&name) {
list.retain(|(id, _)| Some(*id) != self_id);
}
});
release_broadcast_ref(recv);
Ok(Value::Undef)
}
"addEventListener" => {
let ev = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
if ev == "message" {
if let Some(cb) = args.get(1) {
if let Some(arr) = get_prop(recv, "@@listeners") {
with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
items.push(cb.clone());
}
});
}
}
}
Ok(Value::Undef)
}
"removeEventListener" => Ok(Value::Undef),
"ref" => {
let refed = matches!(get_prop(recv, "@@refed"), Some(Value::Bool(true)));
if !refed {
with_host(|h| h.incr_handle());
set_bool(recv, "@@refed", true);
}
Ok(recv.clone())
}
"unref" => {
release_broadcast_ref(recv);
Ok(recv.clone())
}
_ => Err(crate::host::type_error(&format!(
"BroadcastChannel.{method} is not a function"
))),
}
}
fn release_broadcast_ref(recv: &Value) {
if matches!(get_prop(recv, "@@refed"), Some(Value::Bool(true))) {
with_host(|h| h.decr_handle());
set_bool(recv, "@@refed", false);
}
}
fn schedule_broadcast_delivery(target: Value, json: String) {
with_host(|h| h.incr_handle());
let io = with_host(|h| h.io_sender());
let _ = io.send(Box::new(move || broadcast_deliver(target, json)));
}
fn broadcast_deliver(target: Value, json: String) -> Result<(), String> {
let value = match deserialize(&json) {
Ok(v) => v,
Err(e) => {
eprintln!("{e}");
with_host(|h| h.decr_handle());
return Ok(());
}
};
let event = with_host(|h| {
let mut m = IndexMap::new();
m.insert("data".into(), value);
m.insert("type".into(), h.new_str("message"));
h.new_object(m)
});
let onmessage = get_prop(&target, "onmessage");
let mut handlers: Vec<Value> = Vec::new();
if let Some(cb) = onmessage {
if with_host(|h| crate::host::is_callable(h, &cb)) {
handlers.push(cb);
}
}
if let Some(arr) = get_prop(&target, "@@listeners") {
let listeners: Vec<Value> = with_host(|h| match h.get(&arr) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
});
handlers.extend(listeners);
}
for cb in handlers {
if let Err(e) = crate::host::invoke(&cb, vec![event.clone()], None) {
eprintln!("{e}");
}
}
with_host(|h| h.decr_handle());
Ok(())
}
fn str_prop(recv: &Value, key: &str) -> String {
get_prop(recv, key)
.map(|v| with_host(|h| h.str_of(&v)))
.unwrap_or_default()
}
fn set_bool(recv: &Value, key: &str, val: bool) {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert(key.to_string(), Value::Bool(val));
}
});
}