use crate::AppSessionClass;
use rong::{JSContext, JSResult};
#[cfg(feature = "js-appservice")]
use std::cell::Cell;
#[cfg(feature = "js-appservice")]
use std::sync::{Mutex, OnceLock};
pub trait LxLogicExtension: Send + Sync {
fn init(&self, ctx: &JSContext) -> JSResult<()>;
}
type BoxedExtension = Box<dyn LxLogicExtension>;
#[derive(Clone, Copy)]
enum ExtensionAudience {
StandardOnly,
HostAssigned,
}
struct RegisteredExtension {
extension: BoxedExtension,
audience: ExtensionAudience,
}
fn audience_allows(audience: ExtensionAudience, class: AppSessionClass) -> bool {
matches!(audience, ExtensionAudience::HostAssigned) || class == AppSessionClass::StandardApp
}
#[cfg(feature = "js-appservice")]
static EXTENSIONS: OnceLock<Mutex<Vec<RegisteredExtension>>> = OnceLock::new();
#[cfg(feature = "js-appservice")]
thread_local! {
static HOST_REGISTRATION_PHASE: Cell<bool> = const { Cell::new(false) };
}
#[cfg(feature = "js-appservice")]
pub fn register_logic_extension(extension: BoxedExtension) {
let audience = HOST_REGISTRATION_PHASE.with(|phase| {
if phase.get() {
ExtensionAudience::HostAssigned
} else {
ExtensionAudience::StandardOnly
}
});
register(extension, audience);
}
#[cfg(feature = "js-appservice")]
fn register(extension: BoxedExtension, audience: ExtensionAudience) {
let extensions_mutex = EXTENSIONS.get_or_init(|| Mutex::new(Vec::new()));
let mut extensions = extensions_mutex
.lock()
.expect("Extension registry mutex is poisoned");
extensions.push(RegisteredExtension {
extension,
audience,
});
}
#[cfg(feature = "js-appservice")]
#[unsafe(export_name = "lingxia_lxapp_register_framework_logic_extension_v1")]
pub(crate) extern "Rust" fn register_framework_logic_extension(extension: BoxedExtension) {
register(extension, ExtensionAudience::HostAssigned);
}
#[cfg(feature = "js-appservice")]
#[unsafe(export_name = "lingxia_lxapp_run_host_extension_registration_v1")]
pub(crate) extern "Rust" fn run_host_extension_registration(action: Box<dyn FnOnce()>) {
struct Reset;
impl Drop for Reset {
fn drop(&mut self) {
HOST_REGISTRATION_PHASE.with(|phase| phase.set(false));
}
}
HOST_REGISTRATION_PHASE.with(|phase| {
assert!(!phase.replace(true), "nested host extension registration");
});
let _reset = Reset;
action();
}
#[cfg(feature = "js-appservice")]
pub(crate) fn with_registered_extensions<F, R>(class: AppSessionClass, f: F) -> Option<R>
where
F: FnOnce(Vec<&dyn LxLogicExtension>) -> R,
{
EXTENSIONS.get().map(|extensions_mutex| {
let extensions = extensions_mutex
.lock()
.expect("Extension registry mutex is poisoned");
let selected = extensions
.iter()
.filter(|registered| audience_allows(registered.audience, class))
.map(|registered| registered.extension.as_ref())
.collect();
f(selected)
})
}
#[cfg(not(feature = "js-appservice"))]
pub(crate) fn with_registered_extensions<F, R>(_class: AppSessionClass, _f: F) -> Option<R>
where
F: FnOnce(Vec<&dyn LxLogicExtension>) -> R,
{
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ordinary_extensions_cannot_enter_control_contexts() {
assert!(audience_allows(
ExtensionAudience::StandardOnly,
AppSessionClass::StandardApp
));
assert!(!audience_allows(
ExtensionAudience::StandardOnly,
AppSessionClass::ControlApp
));
assert!(audience_allows(
ExtensionAudience::HostAssigned,
AppSessionClass::ControlApp
));
assert!(!audience_allows(
ExtensionAudience::StandardOnly,
AppSessionClass::ControlSurface
));
assert!(audience_allows(
ExtensionAudience::HostAssigned,
AppSessionClass::ControlSurface
));
}
}