///|
/// Command host for user app code.
///
/// The framework decides how this host is connected to the window runtime.
/// App authors register commands and run the host without depending on the
/// underlying IPC implementation.
struct AppCommandHost {
host : MbtProcessHost
mut closed : Bool
}
///|
/// Scheduler-local notification shared by the runtime pump and command tasks.
/// The revision makes waiting race-free when a notification arrives between a
/// native poll and suspension on the condition variable.
pub struct RuntimeWakeSignal {
mut revision : Int64
condition : @async.CondVar
}
///|
pub fn RuntimeWakeSignal::new() -> RuntimeWakeSignal {
RuntimeWakeSignal::{ revision: 0L, condition: @async.CondVar::Cond() }
}
///|
pub fn RuntimeWakeSignal::revision(self : RuntimeWakeSignal) -> Int64 {
self.revision
}
///|
pub fn RuntimeWakeSignal::notify(self : RuntimeWakeSignal) -> Unit {
self.revision = self.revision + 1L
self.condition.broadcast()
}
///|
pub async fn RuntimeWakeSignal::wait_for_change(
self : RuntimeWakeSignal,
revision : Int64,
) -> Unit {
while self.revision == revision {
self.condition.wait()
}
}
///|
/// Request-scoped context for one bridged app command dispatch.
pub struct AppCommandRequestContext {
window : Int64
window_id : String
source_origin : String
page_instance : String?
permission_extension : String?
permission_scope_value : Json?
tasks : @async.TaskGroup[Unit]
wake_signal : RuntimeWakeSignal
emit_event : async (@proton_contract.ContractRoute, String, Json) -> Unit noraise
}
///|
#doc(hidden)
pub fn AppCommandRequestContext::new(
window : Int64,
tasks : @async.TaskGroup[Unit],
window_id? : String = "main",
source_origin? : String = "app",
page_instance? : String? = None,
permission_extension? : String,
permission_scope? : Json,
wake_signal? : RuntimeWakeSignal = RuntimeWakeSignal::new(),
emit_event? : async (@proton_contract.ContractRoute, String, Json) -> Unit noraise = fn(
_route,
_name,
_payload,
) {
},
) -> AppCommandRequestContext {
AppCommandRequestContext::{
window,
window_id,
source_origin,
page_instance,
permission_extension,
permission_scope_value: permission_scope.map(clone_permission_json),
tasks,
wake_signal,
emit_event,
}
}
///|
pub fn AppCommandRequestContext::wake_signal(
self : AppCommandRequestContext,
) -> RuntimeWakeSignal {
self.wake_signal
}
///|
pub fn AppCommandRequestContext::wake_revision(
self : AppCommandRequestContext,
) -> Int64 {
self.wake_signal.revision()
}
///|
pub async fn AppCommandRequestContext::wait_for_wake(
self : AppCommandRequestContext,
revision : Int64,
) -> Unit {
self.wake_signal.wait_for_change(revision)
}
///|
pub fn AppCommandRequestContext::window(
self : AppCommandRequestContext,
) -> Int64 {
self.window
}
///|
pub fn AppCommandRequestContext::window_id(
self : AppCommandRequestContext,
) -> String {
self.window_id
}
///|
pub fn AppCommandRequestContext::source_origin(
self : AppCommandRequestContext,
) -> String {
self.source_origin
}
///|
/// Returns the renderer page instance that issued this request, when the
/// context came from the native bridge.
pub fn AppCommandRequestContext::page_instance(
self : AppCommandRequestContext,
) -> String? {
self.page_instance
}
///|
/// Returns this request's extension-specific permission scope.
pub fn AppCommandRequestContext::permission_scope(
self : AppCommandRequestContext,
extension_id : String,
) -> Json raise CommandPermissionError {
match (self.permission_extension, self.permission_scope_value) {
(Some(actual), Some(scope)) if actual == extension_id =>
clone_permission_json(scope)
(Some(actual), _) => raise Foreign(expected=extension_id, actual~)
_ => raise Missing(extension_id~)
}
}
///|
fn clone_permission_json(value : Json) -> Json {
match value {
Array(items) => Json::array(items.map(clone_permission_json))
Object(object) => {
let copy : Map[String, Json] = Map([])
for key, item in object {
copy[key] = clone_permission_json(item)
}
Json::object(copy)
}
_ => value
}
}
///|
/// Returns the task group owned by this command lifecycle.
///
/// Child tasks are cancelled and joined when the issuing window closes.
pub fn AppCommandRequestContext::task_group(
self : AppCommandRequestContext,
) -> @async.TaskGroup[Unit] {
self.tasks
}
///|
/// Emits a typed event to the renderer page that issued this command.
pub async fn[Payload : ToJson] AppCommandRequestContext::emit(
self : AppCommandRequestContext,
event : @proton_contract.Event[Payload],
payload : Payload,
) -> Unit {
event.validate()
(self.emit_event)(
event.contract_route(),
event.name(),
ToJson::to_json(payload),
)
}
///|
/// Creates an empty app command host.
///
/// Handler diagnostics remain in backend logs by default. Set
/// `expose_error_details` only for a trusted development frontend.
pub fn AppCommandHost::new(
expose_error_details? : Bool = false,
) -> AppCommandHost {
AppCommandHost::{
host: MbtProcessHost::new(expose_error_details~),
closed: false,
}
}
///|
/// Registers a synchronous app command.
pub fn[Payload : @json.FromJson, Reply : ToJson] AppCommandHost::op(
self : AppCommandHost,
name : String,
callback : (Payload) -> Reply raise?,
) -> Unit raise OpRegistrationError {
self.host.op(name, fn(payload) raise { callback(payload) })
}
///|
/// Registers an async app command.
pub fn[Payload : @json.FromJson, Reply : ToJson] AppCommandHost::op_async(
self : AppCommandHost,
name : String,
callback : async (Payload) -> Reply,
) -> Unit raise OpRegistrationError {
self.host.op_async(name, callback)
}
///|
/// Registers an async app command that receives request-scoped context.
pub fn[Payload : @json.FromJson, Reply : ToJson] AppCommandHost::op_async_with_context(
self : AppCommandHost,
name : String,
callback : async (AppCommandRequestContext, Payload) -> Reply,
) -> Unit raise OpRegistrationError {
self.host.op_async_with_context(name, callback)
}
///|
/// Returns registered command names in registration order.
pub fn AppCommandHost::registered_ops(self : AppCommandHost) -> Array[String] {
self.host.registered_ops()
}
///|
/// Dispatches one IPC request on the current async loop.
pub async fn AppCommandHost::dispatch_ipc(
self : AppCommandHost,
request : @ipc.IpcOpRequest,
) -> @ipc.IpcOpResponse {
self.host.dispatch_async(request)
}
///|
/// Dispatches one IPC request with request-scoped context.
pub async fn AppCommandHost::dispatch_ipc_with_context(
self : AppCommandHost,
context : AppCommandRequestContext,
request : @ipc.IpcOpRequest,
) -> @ipc.IpcOpResponse {
self.host.dispatch_async_with_context(context, request)
}
///|
/// Prevents later command registration while keeping existing commands usable.
pub fn AppCommandHost::seal_registrations(self : AppCommandHost) -> Unit {
guard !self.closed else { return }
self.host.seal_registrations()
}
///|
/// Closes this command host and rejects future dispatches.
pub fn AppCommandHost::close(self : AppCommandHost) -> Unit {
guard !self.closed else { return }
self.closed = true
self.host.close()
}