use crate::host::{with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static CHANNELS: RefCell<HashMap<String, Value>> = RefCell::new(HashMap::new());
}
pub const METHODS: &[&str] = &[
"channel",
"subscribe",
"unsubscribe",
"hasSubscribers",
"tracingChannel",
"boundedChannel",
];
pub const TRACING_CHANNEL_METHODS: &[&str] = &["subscribe", "unsubscribe", "traceSync"];
const TRACING_SUBS: &[&str] = &["start", "end", "asyncStart", "asyncEnd", "error"];
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
let name = super::arg_str(args, 0);
Some(match method {
"channel" => Ok(get_or_create(&name)),
"subscribe" => {
let ch = get_or_create(&name);
add_sub(&ch, args.get(1).cloned().unwrap_or(Value::Undef));
Ok(Value::Undef)
}
"unsubscribe" => {
let ch = get_or_create(&name);
Ok(Value::Bool(remove_sub(
&ch,
&args.get(1).cloned().unwrap_or(Value::Undef),
)))
}
"hasSubscribers" => Ok(Value::Bool(sub_count(&get_or_create(&name)) > 0)),
"tracingChannel" => Ok(tracing_channel(&name)),
"boundedChannel" => Ok(get_or_create(&name)),
_ => return None,
})
}
pub fn constant(name: &str) -> Option<Value> {
match name {
"Channel" | "TracingChannel" | "BoundedChannel" => {
Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
}
_ => None,
}
}
fn tracing_channel(name: &str) -> Value {
let subs: Vec<(String, Value)> = TRACING_SUBS
.iter()
.map(|s| {
(
(*s).to_string(),
get_or_create(&format!("tracing:{name}:{s}")),
)
})
.collect();
with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("TracingChannel"));
m.insert("@@name".into(), h.new_str(name));
for (k, v) in subs {
m.insert(k, v);
}
h.new_object(m)
})
}
pub fn tracing_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"subscribe" | "unsubscribe" => {
let handlers = args.first().cloned().unwrap_or(Value::Undef);
for sub in TRACING_SUBS {
let cb = with_host(|h| match h.get(&handlers) {
Some(JsObj::Object(p)) => p.get(*sub).cloned(),
_ => None,
});
let (Some(cb), Some(ch)) = (cb, sub_channel(recv, sub)) else {
continue;
};
if method == "subscribe" {
add_sub(&ch, cb);
} else {
remove_sub(&ch, &cb);
}
}
Ok(Value::Undef)
}
"traceSync" => {
let fn_v = args.first().cloned().unwrap_or(Value::Undef);
let ctx = args.get(1).cloned().unwrap_or(Value::Undef);
let this = args.get(2).cloned();
let call_args: Vec<Value> = args.iter().skip(3).cloned().collect();
if let Some(start) = sub_channel(recv, "start") {
publish(&start, ctx.clone())?;
}
match crate::host::invoke(&fn_v, call_args, this) {
Ok(v) => {
if let Some(end) = sub_channel(recv, "end") {
publish(&end, ctx)?;
}
Ok(v)
}
Err(e) => {
if let Some(err_ch) = sub_channel(recv, "error") {
let _ = publish(&err_ch, ctx.clone());
}
if let Some(end) = sub_channel(recv, "end") {
let _ = publish(&end, ctx);
}
Err(e)
}
}
}
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
fn sub_channel(recv: &Value, sub: &str) -> Option<Value> {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get(sub).cloned(),
_ => None,
})
}
pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"subscribe" => {
add_sub(recv, args.first().cloned().unwrap_or(Value::Undef));
Ok(Value::Undef)
}
"unsubscribe" => Ok(Value::Bool(remove_sub(
recv,
&args.first().cloned().unwrap_or(Value::Undef),
))),
"publish" => publish(recv, args.first().cloned().unwrap_or(Value::Undef)),
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
fn get_or_create(name: &str) -> Value {
if let Some(ch) = CHANNELS.with(|c| c.borrow().get(name).cloned()) {
return ch;
}
let ch = with_host(|h| {
let subs = h.new_array(Vec::new());
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("Channel"));
m.insert("@@name".into(), h.new_str(name));
m.insert("@@subs".into(), subs);
m.insert("name".into(), h.new_str(name));
m.insert("hasSubscribers".into(), Value::Bool(false));
h.new_object(m)
});
CHANNELS.with(|c| c.borrow_mut().insert(name.to_string(), ch.clone()));
ch
}
fn subs_array(ch: &Value) -> Option<Value> {
with_host(|h| match h.get(ch) {
Some(JsObj::Object(p)) => p.get("@@subs").cloned(),
_ => None,
})
}
fn sub_count(ch: &Value) -> usize {
match subs_array(ch) {
Some(arr) => with_host(|h| match h.get(&arr) {
Some(JsObj::Array(items)) => items.len(),
_ => 0,
}),
None => 0,
}
}
fn add_sub(ch: &Value, cb: Value) {
if let Some(arr) = subs_array(ch) {
with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
items.push(cb);
}
});
refresh_has(ch);
}
}
fn remove_sub(ch: &Value, cb: &Value) -> bool {
let removed = match subs_array(ch) {
Some(arr) => with_host(|h| {
if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
if let Some(i) = items.iter().position(|x| same_ref(x, cb)) {
items.remove(i);
return true;
}
}
false
}),
None => false,
};
if removed {
refresh_has(ch);
}
removed
}
fn publish(ch: &Value, msg: Value) -> Result<Value, String> {
let subs: Vec<Value> = with_host(|h| match h.get(ch) {
Some(JsObj::Object(p)) => match p.get("@@subs").and_then(|a| h.get(a)) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
},
_ => Vec::new(),
});
let name_val = with_host(|h| match h.get(ch) {
Some(JsObj::Object(p)) => p.get("@@name").cloned().unwrap_or(Value::Undef),
_ => Value::Undef,
});
for cb in subs {
crate::host::invoke(&cb, vec![msg.clone(), name_val.clone()], None)?;
}
Ok(Value::Undef)
}
fn refresh_has(ch: &Value) {
let has = sub_count(ch) > 0;
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(ch) {
p.insert("hasSubscribers".into(), Value::Bool(has));
}
});
}
fn same_ref(a: &Value, b: &Value) -> bool {
matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
}