///|
/// IPC command request exchanged between the child webview process and the
/// parent async dispatcher.
pub struct ProcessCommandRequest {
name : String
payload : Json
} derive(ToJson, FromJson)
///|
/// IPC command response returned by the parent async dispatcher.
pub(all) enum ProcessCommandResponse {
Ok(Json)
Error(String)
} derive(ToJson, FromJson)
///|
suberror ProcessCommandError {
ProcessCommandError(String)
}
///|
pub fn[Payload : ToJson] ProcessCommandRequest::new(
name : String,
payload : Payload,
) -> ProcessCommandRequest {
{ name, payload: payload.to_json(), }
}
///|
pub fn ProcessCommandRequest::parse(
raw : String,
) -> ProcessCommandRequest raise ProcessCommandError {
let json = @json.parse(raw) catch {
_ => raise ProcessCommandError("Bad bridge request JSON")
}
@json.from_json(json) catch {
_ => raise ProcessCommandError("Bad bridge request shape")
}
}
///|
pub fn[Payload : ToJson] ProcessCommandResponse::ok(
payload : Payload,
) -> ProcessCommandResponse {
Ok(payload.to_json())
}
///|
pub fn ProcessCommandResponse::error(
message : String,
) -> ProcessCommandResponse {
Error(message)
}
///|
pub fn ProcessCommandResponse::stringify(
self : ProcessCommandResponse,
) -> String {
(
match self {
Ok(payload) => [1, payload]
Error(message) => [0, message]
} : Json).stringify()
}
///|
pub fn[Reply : @json.FromJson] ProcessCommandResponse::decode_reply(
self : ProcessCommandResponse,
) -> Reply raise ProcessCommandError {
match self {
Ok(reply_json) =>
@json.from_json(reply_json) catch {
_ => raise ProcessCommandError("Bad bridge reply payload")
}
Error(message) => raise ProcessCommandError(message)
}
}
///|
pub fn[Reply : @json.FromJson] decode_process_command_response(
raw : String,
) -> Reply raise ProcessCommandError {
guard raw != "" else { raise ProcessCommandError("IPC request failed") }
let json = @json.parse(raw) catch {
_ => raise ProcessCommandError("Bad bridge response JSON")
}
let response = decode_process_command_response_wire(json) catch {
_ => raise ProcessCommandError("Bad bridge response shape")
}
response.decode_reply()
}
///|
fn decode_process_command_response_wire(
json : Json,
) -> ProcessCommandResponse raise ProcessCommandError {
match json {
[1, payload] => Ok(payload)
[0, String(message)] => Error(message)
_ => raise ProcessCommandError("Bad bridge response shape")
}
}
///|
pub struct ProcessCommandProxy {
wm : WindowManager
source_window_id : Int
subtype : String
}
///|
/// Parent-process command router for requests coming from a child webview
/// process.
pub struct ProcessCommandRouter {
handlers : Map[String, async (Json) -> ProcessCommandResponse]
}
///|
/// Plugin-scoped registration context for `ProcessCommandRouter`.
pub struct ProcessPluginRouter {
router : ProcessCommandRouter
plugin_name : String
}
///|
fn[Payload : @json.FromJson] decode_process_command_payload(
payload_json : Json,
) -> Payload raise ProcessCommandError {
@json.from_json(payload_json) catch {
_ => raise ProcessCommandError("Bad command payload")
}
}
///|
fn[Payload : @json.FromJson] register_process_command(
router : ProcessCommandRouter,
name : String,
callback : async (Payload) -> ProcessCommandResponse,
) -> Unit {
router.handlers.set(name, payload_json => {
let payload : Payload = decode_process_command_payload(payload_json) catch {
ProcessCommandError(message) =>
return ProcessCommandResponse::error(message)
}
callback(payload)
})
}
///|
pub fn ProcessCommandProxy::new(
wm : WindowManager,
source_window_id : Int,
subtype? : String = "bridge_command",
) -> ProcessCommandProxy {
{ wm, source_window_id, subtype, }
}
///|
/// Creates a parent-process command router.
pub fn ProcessCommandRouter::new() -> ProcessCommandRouter {
{ handlers: {}, }
}
///|
/// Registers a plugin namespace on the parent-process router.
pub fn ProcessCommandRouter::plugin(
self : ProcessCommandRouter,
plugin_name : String,
register : (ProcessPluginRouter) -> Unit,
) -> Unit {
register({ router: self, plugin_name, })
}
///|
pub fn[Payload : ToJson, Reply : @json.FromJson] ProcessCommandProxy::call(
self : ProcessCommandProxy,
name : String,
payload : Payload,
target_window_id? : Int = 0,
timeout_ms? : Int = 10_000,
) -> Reply raise ProcessCommandError {
let request = ProcessCommandRequest::new(name, payload)
let raw = self.wm.request(
self.source_window_id,
target_window_id,
self.subtype,
request.to_json().stringify(),
timeout_ms~,
)
decode_process_command_response(raw)
}
///|
pub fn[Payload : ToJson, Reply : @json.FromJson] ProcessCommandProxy::call_plugin(
self : ProcessCommandProxy,
plugin_name : String,
api_name : String,
payload : Payload,
target_window_id? : Int = 0,
timeout_ms? : Int = 10_000,
) -> Reply raise ProcessCommandError {
self.call(
make_process_plugin_command_name(plugin_name, api_name),
payload,
target_window_id~,
timeout_ms~,
)
}
///|
/// Builds a typed forwarding closure for a plugin command handled by the
/// parent process.
pub fn[Payload : ToJson, Reply : @json.FromJson] ProcessCommandProxy::plugin_handler(
self : ProcessCommandProxy,
plugin_name : String,
api_name : String,
target_window_id? : Int = 0,
timeout_ms? : Int = 10_000,
) -> (Payload) -> Reply raise {
fn(payload : Payload) -> Reply raise {
self.call_plugin(
plugin_name,
api_name,
payload,
target_window_id~,
timeout_ms~,
)
}
}
///|
/// Dispatches a parsed process command through the registered router handlers.
pub async fn ProcessCommandRouter::dispatch(
self : ProcessCommandRouter,
request : ProcessCommandRequest,
) -> ProcessCommandResponse {
match self.handlers.get(request.name) {
Some(handler) => handler(request.payload)
None => ProcessCommandResponse::error("Unknown command: " + request.name)
}
}
///|
/// Serves process commands from a child webview by using the registered router.
pub async fn ProcessCommandRouter::serve(
self : ProcessCommandRouter,
wm : WindowManager,
child_pid : Int,
subtype? : String = "bridge_command",
) -> Unit {
wm.serve_process_commands(
child_pid,
request => self.dispatch(request),
subtype~,
)
}
///|
/// Registers a typed synchronous command on the parent-process router.
pub fn[Payload : @json.FromJson, Reply : ToJson] ProcessCommandRouter::handle(
self : ProcessCommandRouter,
name : String,
callback : (Payload) -> Reply,
) -> Unit {
register_process_command(self, name, payload => {
ProcessCommandResponse::ok(callback(payload))
})
}
///|
/// Registers a typed synchronous command that can return explicit errors.
pub fn[Payload : @json.FromJson, Reply : ToJson] ProcessCommandRouter::handle_result(
self : ProcessCommandRouter,
name : String,
callback : (Payload) -> Reply raise Error,
) -> Unit {
register_process_command(self, name, payload => {
let reply = callback(payload) catch {
message => return ProcessCommandResponse::error(message.to_string())
}
ProcessCommandResponse::ok(reply)
})
}
///|
/// Registers a typed async command on the parent-process router.
pub fn[Payload : @json.FromJson, Reply : ToJson] ProcessCommandRouter::handle_async(
self : ProcessCommandRouter,
name : String,
callback : async (Payload) -> Reply,
) -> Unit {
register_process_command(self, name, payload => {
let reply = callback(payload)
ProcessCommandResponse::ok(reply)
})
}
///|
/// Registers a typed async command that can return explicit errors.
pub fn[Payload : @json.FromJson, Reply : ToJson] ProcessCommandRouter::handle_result_async(
self : ProcessCommandRouter,
name : String,
callback : async (Payload) -> Reply raise Error,
) -> Unit {
register_process_command(self, name, payload => {
let reply = callback(payload) catch {
message => return ProcessCommandResponse::error(message.to_string())
}
ProcessCommandResponse::ok(reply)
})
}
///|
/// Registers a typed synchronous plugin command on the parent-process router.
pub fn[Payload : @json.FromJson, Reply : ToJson] ProcessPluginRouter::command(
self : ProcessPluginRouter,
api_name : String,
callback : (Payload) -> Reply,
) -> Unit {
self.router.handle(
make_process_plugin_command_name(self.plugin_name, api_name),
callback,
)
}
///|
/// Registers a typed synchronous plugin command that can return explicit
/// errors.
pub fn[Payload : @json.FromJson, Reply : ToJson] ProcessPluginRouter::command_result(
self : ProcessPluginRouter,
api_name : String,
callback : (Payload) -> Reply raise Error,
) -> Unit {
self.router.handle_result(
make_process_plugin_command_name(self.plugin_name, api_name),
callback,
)
}
///|
/// Registers a typed async plugin command on the parent-process router.
pub fn[Payload : @json.FromJson, Reply : ToJson] ProcessPluginRouter::command_async(
self : ProcessPluginRouter,
api_name : String,
callback : async (Payload) -> Reply,
) -> Unit {
self.router.handle_async(
make_process_plugin_command_name(self.plugin_name, api_name),
callback,
)
}
///|
/// Registers a typed async plugin command that can return explicit errors.
pub fn[Payload : @json.FromJson, Reply : ToJson] ProcessPluginRouter::command_result_async(
self : ProcessPluginRouter,
api_name : String,
callback : async (Payload) -> Reply raise Error,
) -> Unit {
self.router.handle_result_async(
make_process_plugin_command_name(self.plugin_name, api_name),
callback,
)
}
///|
pub async fn WindowManager::serve_process_commands(
self : WindowManager,
child_pid : Int,
handler : async (ProcessCommandRequest) -> ProcessCommandResponse,
subtype? : String = "bridge_command",
) -> Unit {
while self.wait_child_noblock(child_pid) == 0 {
match self.try_pop_message(0) {
Some(message) =>
if message.message_type == Request && message.subtype == subtype {
let response = try {
let request = ProcessCommandRequest::parse(message.data)
handler(request)
} catch {
ProcessCommandError(message) =>
ProcessCommandResponse::error(message)
_ => ProcessCommandResponse::error("Unexpected bridge error")
}
ignore(
self.respond(
0,
message.source_window_id,
message.message_id,
response.stringify(),
),
)
} else {
@async.pause()
}
None => @async.sleep(10)
}
}
}
///|
fn make_process_plugin_command_name(
plugin_name : String,
api_name : String,
) -> String {
"plugin:\{Json::string(plugin_name).stringify()}:\{Json::string(api_name).stringify()}"
}
///|
test "ProcessCommandResponse ok stringifies without enum tag" {
let raw = ProcessCommandResponse::ok({ "total": Json::number(5) }).stringify()
assert_eq(raw, "[1,{\"total\":5}]")
}
///|
test "ProcessCommandResponse error stringifies without enum tag" {
let raw = ProcessCommandResponse::error("oops").stringify()
assert_eq(raw, "[0,\"oops\"]")
}
///|
struct ProcessCommandReplyForTest {
total : Int
} derive(FromJson)
///|
test "decode_process_command_response supports tagged ok payload" {
let reply : ProcessCommandReplyForTest = decode_process_command_response(
"[1,{\"total\":5}]",
) catch {
ProcessCommandError(message) => abort(message)
}
assert_eq(reply.total, 5)
}