///|
struct Extension {
contract : @proton_contract.ExtensionContract
command_routes : Array[@proton_contract.ContractRoute]
register : (@proton_command.CommandRegistrar) -> Unit raise
scripts : Array[String]
dependencies : Array[String]
resolve_permission : (Json, String) -> Json raise @proton_command.PermissionScopeValidationError
on_destroy : () -> Unit raise
}
///|
struct Extensions {
items : Array[Extension]
}
///|
/// Builds an installable extension from typed command descriptors.
///
/// Validation is deferred until application configuration so invalid static
/// extension metadata follows the facade's ordinary adaptation error path.
pub fn typed(
contract : @proton_contract.ExtensionContract,
command_routes : Array[@proton_contract.ContractRoute],
register : (@proton_command.CommandRegistrar) -> Unit raise,
scripts? : Array[String] = [],
dependencies? : Array[String] = [],
resolve_permission? : (Json, String) -> Json raise @proton_command.PermissionScopeValidationError = fn(
scope,
_base_path,
) -> Json raise @proton_command.PermissionScopeValidationError {
guard scope is Object(fields) else {
raise @proton_command.PermissionScopeValidationError::Invalid(
detail="extension " +
contract.id() +
" permission scope must be an object",
)
}
for _, _ in fields {
raise @proton_command.PermissionScopeValidationError::Invalid(
detail="extension " +
contract.id() +
" does not accept a non-empty permission scope",
)
}
scope
},
on_destroy? : () -> Unit raise = fn() { },
) -> Extension {
Extension::{
contract,
command_routes: command_routes.map(route => route),
register,
scripts: scripts.map(script => script),
dependencies: dependencies.map(dependency => dependency),
resolve_permission,
on_destroy,
}
}
///|
pub fn Extensions::new(items : Array[Extension]) -> Extensions {
Extensions::{ items: copy_extensions(items) }
}
///|
pub fn extensions(items : Array[Extension]) -> Extensions {
Extensions::new(items)
}
///|
pub fn Extension::id(self : Extension) -> String {
self.contract.id()
}
///|
pub fn Extension::command_spec(
self : Extension,
) -> @proton_command.AppCommandExtensionSpec raise ExtensionAdapterError {
@proton_command.AppCommandExtensionSpec::typed(
self.contract,
self.command_routes,
self.register,
scripts=self.scripts,
dependencies=self.dependencies,
resolve_permission=self.resolve_permission,
on_destroy=self.on_destroy,
) catch {
error =>
raise TypedSpecInvalid(
extension_id=self.contract.id(),
detail=error.message(),
)
}
}
///|
pub fn Extensions::items(self : Extensions) -> Array[Extension] {
copy_extensions(self.items)
}
///|
fn copy_extensions(items : Array[Extension]) -> Array[Extension] {
items.map(fn(item) { item })
}
///|
/// Builds the standard JavaScript event-poller script for an extension namespace.
///
/// Extensions that pump native events through a `drain*` command install this
/// script so the renderer polls the host on a fixed interval. The script is
/// idempotent: it exits early if the namespace or its drain method is missing,
/// or if a poller is already registered.
pub fn poller_script(
js_namespace~ : String,
drain_method~ : String,
poller_field~ : String,
) -> String {
let ns = js_namespace
"(() => {\n" +
" const root = window.__MoonBit__;\n" +
" const " +
ns +
" = root && root." +
ns +
";\n" +
" if (!" +
ns +
" || typeof " +
ns +
"." +
drain_method +
" !== \"function\") {\n" +
" return;\n" +
" }\n" +
" if (" +
ns +
"." +
poller_field +
") {\n" +
" return;\n" +
" }\n" +
" " +
ns +
"." +
poller_field +
" = window.setInterval(() => {\n" +
" " +
ns +
"." +
drain_method +
"({}).catch(() => {});\n" +
" }, 100);\n" +
"})();"
}