use crate::{error, info, warn};
use rong::{
JSContext, JSFunc, JSObject, JSResult, JSRuntimeService, RongJSError, error::HostError,
};
use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum Scope {
App,
Page(String),
}
#[derive(Clone, Debug)]
pub(crate) struct AppBusEvent {
pub scope: Scope,
pub event_name: String,
pub payload_json: Option<String>,
}
pub(crate) struct EventBusRegistry {
handlers: RefCell<HashMap<Scope, Vec<HandlerEntry>>>,
}
#[derive(Clone)]
struct HandlerEntry {
event_name: String,
callback: JSFunc,
}
impl JSRuntimeService for EventBusRegistry {}
impl Default for EventBusRegistry {
fn default() -> Self {
Self {
handlers: RefCell::new(HashMap::new()),
}
}
}
pub(crate) fn init(ctx: &JSContext) {
ctx.runtime().get_or_init_service::<EventBusRegistry>();
}
pub(crate) fn clear_page(ctx: &JSContext, page_path: &str) {
let registry = ctx.runtime().get_or_init_service::<EventBusRegistry>();
registry
.handlers
.borrow_mut()
.retain(|scope, _| match scope {
Scope::Page(path) => path != page_path,
_ => true,
});
}
pub fn register_app_handler(ctx: &JSContext, event_name: &str, callback: JSFunc) -> JSResult<()> {
if event_name.trim().is_empty() {
return Err(RongJSError::from(HostError::new(
rong::error::E_INTERNAL,
"event_name is required",
)));
}
let entry = HandlerEntry {
event_name: event_name.to_string(),
callback,
};
let registry = ctx.runtime().get_or_init_service::<EventBusRegistry>();
registry
.handlers
.borrow_mut()
.entry(Scope::App)
.or_default()
.push(entry);
Ok(())
}
pub fn unregister_app_handler(
ctx: &JSContext,
event_name: &str,
callback: Option<JSFunc>,
) -> usize {
if event_name.trim().is_empty() {
return 0;
}
let registry = ctx.runtime().get_or_init_service::<EventBusRegistry>();
let mut remaining = 0usize;
registry.handlers.borrow_mut().retain(|scope, entries| {
if !matches!(scope, Scope::App) {
return true;
}
if let Some(ref cb) = callback {
entries.retain(|h| h.event_name != event_name || h.callback != *cb);
} else {
entries.retain(|h| h.event_name != event_name);
}
remaining += entries
.iter()
.filter(|h| h.event_name == event_name)
.count();
!entries.is_empty()
});
remaining
}
pub fn register_page_handler(
ctx: &JSContext,
page_path: &str,
event_name: &str,
callback: JSFunc,
) -> JSResult<()> {
if event_name.trim().is_empty() {
return Err(RongJSError::from(HostError::new(
rong::error::E_INTERNAL,
"event_name is required",
)));
}
if page_path.trim().is_empty() {
return Err(RongJSError::from(HostError::new(
rong::error::E_INTERNAL,
"page_path is required",
)));
}
let entry = HandlerEntry {
event_name: event_name.to_string(),
callback,
};
let registry = ctx.runtime().get_or_init_service::<EventBusRegistry>();
registry
.handlers
.borrow_mut()
.entry(Scope::Page(page_path.to_string()))
.or_default()
.push(entry);
Ok(())
}
pub fn unregister_page_handler(ctx: &JSContext, page_path: &str, event_name: &str) {
if page_path.trim().is_empty() || event_name.trim().is_empty() {
return;
}
let registry = ctx.runtime().get_or_init_service::<EventBusRegistry>();
registry.handlers.borrow_mut().retain(|scope, entries| {
if let Scope::Page(path) = scope {
if path == page_path {
entries.retain(|h| h.event_name != event_name);
return !entries.is_empty();
}
}
true
});
}
pub(crate) async fn dispatch_app_bus_event(ctx: &JSContext, event: &AppBusEvent) -> JSResult<()> {
match &event.scope {
Scope::App => {
emit_to_handlers(
ctx,
Scope::App,
&event.event_name,
event.payload_json.as_deref(),
)
.await
}
Scope::Page(path) => {
emit_to_handlers(
ctx,
Scope::Page(path.clone()),
&event.event_name,
event.payload_json.as_deref(),
)
.await
}
}
}
async fn emit_to_handlers(
ctx: &JSContext,
scope: Scope,
event_name: &str,
payload_json: Option<&str>,
) -> JSResult<()> {
let registry = ctx.runtime().get_or_init_service::<EventBusRegistry>();
let handlers = {
let map = registry.handlers.borrow();
map.get(&scope).cloned().unwrap_or_default()
};
if handlers.is_empty() {
return Ok(());
}
info!(
"Dispatching {} scope={:?} handlers={}",
event_name,
scope,
handlers.len()
);
let payload_base = if let Some(json) = payload_json {
JSObject::from_json_string(ctx, json).unwrap_or_else(|_| JSObject::new(ctx))
} else {
JSObject::new(ctx)
};
for handler in handlers.into_iter().filter(|h| h.event_name == event_name) {
let payload = payload_base.clone();
let _ = handler.callback.call_async::<_, ()>(None, (payload,)).await;
}
Ok(())
}
pub fn publish_app_event(appid: &str, event_name: &str, payload_json: Option<String>) -> bool {
let Some(lxapp) = crate::try_get(appid) else {
warn!("publish_app_event: unknown appid {}", appid);
return false;
};
let event = AppBusEvent {
scope: Scope::App,
event_name: event_name.to_string(),
payload_json,
};
if let Err(e) = lxapp.executor.dispatch_app_bus_event(lxapp.clone(), event) {
error!("Failed to dispatch app event: {}", e).with_appid(appid.to_string());
false
} else {
true
}
}
pub fn publish_page_event(
appid: &str,
page_path: &str,
event_name: &str,
payload_json: Option<String>,
) -> bool {
if page_path.trim().is_empty() {
warn!("publish_page_event: missing page_path");
return false;
}
let Some(lxapp) = crate::try_get(appid) else {
warn!("publish_page_event: unknown appid {}", appid);
return false;
};
let event = AppBusEvent {
scope: Scope::Page(page_path.to_string()),
event_name: event_name.to_string(),
payload_json,
};
if let Err(e) = lxapp.executor.dispatch_app_bus_event(lxapp.clone(), event) {
error!("Failed to dispatch page event: {}", e).with_appid(appid.to_string());
false
} else {
true
}
}