///|
pub struct BridgeConfig {
global_name : String
native_hook : String
event_dispatch_hook : String
window_label : String
} derive(Debug, Eq)
///|
pub fn BridgeConfig::new(
global_name? : String = "lepusa",
native_hook? : String = "__lepusaInvoke",
event_dispatch_hook? : String = "__lepusaDispatchEvent",
window_label? : String = "main",
) -> BridgeConfig {
{ global_name, native_hook, event_dispatch_hook, window_label }
}
///|
pub fn BridgeConfig::validate(self : BridgeConfig) -> Array[String] {
let problems : Array[String] = []
if self.global_name == "" {
problems.push("bridge global name is required")
}
if self.native_hook == "" {
problems.push("bridge native hook is required")
}
if self.event_dispatch_hook == "" {
problems.push("bridge event dispatch hook is required")
}
if self.window_label == "" {
problems.push("bridge window label is required")
}
problems
}
///|
pub struct BridgeScript {
global_name : String
native_hook : String
event_dispatch_hook : String
window_label : String
routes : Array[String]
source : String
} derive(Debug, Eq)
///|
pub fn BridgeScript::global_name(self : BridgeScript) -> String {
self.global_name
}
///|
pub fn BridgeScript::native_hook(self : BridgeScript) -> String {
self.native_hook
}
///|
pub fn BridgeScript::event_dispatch_hook(self : BridgeScript) -> String {
self.event_dispatch_hook
}
///|
pub fn BridgeScript::window_label(self : BridgeScript) -> String {
self.window_label
}
///|
pub fn BridgeScript::routes(self : BridgeScript) -> Array[String] {
self.routes.copy()
}
///|
pub fn BridgeScript::source(self : BridgeScript) -> String {
self.source
}
///|
pub fn RuntimePlan::bridge_script(
self : RuntimePlan,
config? : BridgeConfig = BridgeConfig::new(),
) -> Result[BridgeScript, Array[String]] {
self.bridge_script_with_registered_routes(config~, registered_routes=[])
}
///|
fn RuntimePlan::bridge_script_with_registered_routes(
self : RuntimePlan,
config? : BridgeConfig = BridgeConfig::new(),
registered_routes~ : Array[String],
restrict_registered? : Bool = false,
) -> Result[BridgeScript, Array[String]] {
let problems = config.validate()
let routes = self.bridge_routes(
window_label=config.window_label,
registered_routes~,
restrict_registered~,
)
for route in routes {
if route == "" {
problems.push("bridge route is required")
}
}
if problems.is_empty() {
Ok({
global_name: config.global_name,
native_hook: config.native_hook,
event_dispatch_hook: config.event_dispatch_hook,
window_label: config.window_label,
routes,
source: bridge_source(config, routes),
})
} else {
Err(problems)
}
}
///|
fn RuntimePlan::bridge_routes(
self : RuntimePlan,
window_label~ : String,
registered_routes~ : Array[String],
restrict_registered~ : Bool,
) -> Array[String] {
let routes : Array[String] = []
for entry in self.command_manifest().entries() {
if entry.allowed_in_window(window_label) &&
entry.mode().requires_bridge_route() &&
(!restrict_registered || registered_routes.contains(entry.route())) {
routes.push(entry.route())
}
}
routes
}
///|
fn CommandMode::requires_bridge_route(self : CommandMode) -> Bool {
match self {
Sync | Async | Stream => true
Event => false
}
}
///|
fn bridge_source(config : BridgeConfig, routes : Array[String]) -> String {
let route_literals = routes
.map(fn(route) { route.js_string_literal() })
.join(",")
let global_name = config.global_name.js_string_literal()
let native_hook = config.native_hook.js_string_literal()
let event_dispatch_hook = config.event_dispatch_hook.js_string_literal()
let window_label = config.window_label.js_string_literal()
[
"(() => {",
" const globalName = \{global_name};",
" const nativeHook = \{native_hook};",
" const eventDispatchHook = \{event_dispatch_hook};",
" const windowLabel = \{window_label};",
" const routes = [\{route_literals}];",
" const routeSet = new Set(routes);",
" const reservedApiKeys = new Set([\"routes\", \"can\", \"command\", \"commands\", \"listen\", \"unlisten\", \"emit\", \"emitTo\", \"invoke\", \"stream\", \"drainStream\", \"cancelStream\", \"closeResource\", \"InvokeError\", \"isInvokeError\"]);",
" let nextId = 0;",
" const eventHandlers = new Map();",
" const decodePayload = (payload) => {",
" if (typeof payload !== \"string\") {",
" return payload ?? null;",
" }",
" if (payload === \"\") {",
" return null;",
" }",
" try {",
" return JSON.parse(payload);",
" } catch (_) {",
" return payload;",
" }",
" };",
" class LepusaInvokeError extends Error {",
" constructor(message, options = {}) {",
" super(message);",
" this.name = \"LepusaInvokeError\";",
" this.code = options.code || \"handler-error\";",
" this.route = options.route || \"\";",
" this.id = options.id || \"\";",
" this.failure = options.failure || null;",
" this.response = options.response || null;",
" }",
" }",
" const createInvokeError = (response, route = \"\") => {",
" const failure = response && typeof response.failure === \"object\" ? response.failure : null;",
" const message = (response && response.error) || (failure && failure.message) || \"Lepusa invoke failed\";",
" return new LepusaInvokeError(message, {",
" code: (response && response.errorCode) || (failure && failure.code) || \"handler-error\",",
" route: (failure && failure.route) || route || \"\",",
" id: (response && response.id) || \"\",",
" failure,",
" response: response || null,",
" });",
" };",
" const invokeControlError = (code, route, message) => new LepusaInvokeError(message, {",
" code,",
" route,",
" failure: { code, message, route },",
" response: null,",
" });",
" const withInvokeControls = (promise, route, options = {}) => {",
" const signal = options && options.signal;",
" const timeoutMs = Number(options && options.timeoutMs ? options.timeoutMs : 0);",
" if ((!signal || typeof signal !== \"object\") && !(timeoutMs > 0)) {",
" return promise;",
" }",
" return new Promise((resolve, reject) => {",
" let settled = false;",
" let timeoutId = 0;",
" let abortHandler = null;",
" const cleanup = () => {",
" if (timeoutId) {",
" clearTimeout(timeoutId);",
" }",
" if (signal && abortHandler && typeof signal.removeEventListener === \"function\") {",
" signal.removeEventListener(\"abort\", abortHandler);",
" }",
" };",
" const settle = (fn, value) => {",
" if (settled) {",
" return;",
" }",
" settled = true;",
" cleanup();",
" fn(value);",
" };",
" if (signal && signal.aborted) {",
" settle(reject, invokeControlError(\"cancelled\", route, \"invoke cancelled\"));",
" return;",
" }",
" if (timeoutMs > 0) {",
" timeoutId = setTimeout(() => settle(reject, invokeControlError(\"timeout\", route, \"invoke timed out\")), timeoutMs);",
" }",
" if (signal && typeof signal.addEventListener === \"function\") {",
" abortHandler = () => settle(reject, invokeControlError(\"cancelled\", route, \"invoke cancelled\"));",
" signal.addEventListener(\"abort\", abortHandler, { once: true });",
" }",
" promise.then((value) => settle(resolve, value), (error) => settle(reject, error));",
" });",
" };",
" const dispatchEvent = (event) => {",
" if (!event || typeof event.name !== \"string\") {",
" return false;",
" }",
" const handlers = eventHandlers.get(event.name);",
" if (!handlers || handlers.size === 0) {",
" return false;",
" }",
" const payload = decodePayload(event.payload);",
" for (const handler of Array.from(handlers)) {",
" handler(payload, event);",
" }",
" return true;",
" };",
" const createInvoker = (route) => (payload = null, options = {}) => api.invoke(route, payload, options);",
" const currentOrigin = () => {",
" const location = globalThis.location;",
" if (!location || !location.origin || location.origin === \"null\") {",
" return \"\";",
" }",
" return String(location.origin);",
" };",
" const parseInvokePayload = (response) => {",
" if (!response || response.payload == null) {",
" return null;",
" }",
" if (typeof response.payload !== \"string\") {",
" return response.payload;",
" }",
" try {",
" return JSON.parse(response.payload);",
" } catch (_) {",
" return response.payload;",
" }",
" };",
" const callNative = async (plugin, command, payload, route, options = {}) => {",
" const hook = globalThis[nativeHook];",
" if (typeof hook !== \"function\") {",
" throw new LepusaInvokeError(`Missing Lepusa native hook: ${nativeHook}`, { code: \"transport-error\", route });",
" }",
" const callbackId = String(++nextId);",
" const errorId = String(++nextId);",
" const request = {",
" id: callbackId,",
" window_label: windowLabel,",
" plugin,",
" command,",
" payload: payload == null ? \"\" : JSON.stringify(payload),",
" origin: currentOrigin(),",
" callback_id: callbackId,",
" error_id: errorId,",
" };",
" const response = await withInvokeControls(Promise.resolve().then(() => hook(request)), route, options);",
" if (response && response.error) {",
" throw createInvokeError(response, route);",
" }",
" return parseInvokePayload(response);",
" };",
" const normalizeEventTarget = (target) => {",
" if (target == null) {",
" return { kind: \"app\" };",
" }",
" if (typeof target === \"string\") {",
" if (target === \"\") {",
" throw new Error(\"Lepusa event target label is required\");",
" }",
" return { kind: \"window\", label: target };",
" }",
" if (typeof target === \"object\" && typeof target.kind === \"string\") {",
" return target;",
" }",
" throw new Error(\"Lepusa event target must be a string or target object\");",
" };",
" const emitNative = async (target, name, payload = null) => {",
" if (typeof name !== \"string\" || name === \"\") {",
" throw new Error(\"Lepusa event name is required\");",
" }",
" const hook = globalThis[nativeHook];",
" if (typeof hook !== \"function\") {",
" return dispatchEvent({ name, payload: payload == null ? \"\" : JSON.stringify(payload) }) ? 1 : 0;",
" }",
" const result = await callNative(\"__lepusa\", \"event.emit\", {",
" target: normalizeEventTarget(target),",
" name,",
" payload: payload == null ? \"\" : JSON.stringify(payload),",
" }, \"__lepusa.event.emit\");",
" return result && typeof result.delivered === \"number\" ? result.delivered : 0;",
" };",
" const runtimeControl = (command, payload = {}, options = {}) => callNative(\"__lepusa\", command, payload, `__lepusa.${command}`, options);",
" const attachStream = (stream) => {",
" if (!stream || typeof stream !== \"object\" || typeof stream.channelId !== \"string\") {",
" return stream;",
" }",
" const channelId = stream.channelId;",
" return Object.freeze({",
" ...stream,",
" drain: (options = {}) => runtimeControl(\"stream.drain\", { channelId }, options),",
" cancel: (options = {}) => runtimeControl(\"stream.cancel\", { channelId }, options),",
" close: (options = {}) => runtimeControl(\"resource.close\", { id: channelId, kind: \"channel\" }, options),",
" });",
" };",
" const installRoute = (target, route, options = {}) => {",
" const parts = route.split(\".\").filter(Boolean);",
" if (parts.length === 0) {",
" return;",
" }",
" if (options.direct && reservedApiKeys.has(parts[0])) {",
" return;",
" }",
" const leaf = parts.pop();",
" let scope = target;",
" for (const part of parts) {",
" const existing = scope[part];",
" if (existing != null && (typeof existing !== \"object\" || Array.isArray(existing))) {",
" return;",
" }",
" scope = scope[part] ||= {};",
" }",
" if (scope[leaf] == null) {",
" scope[leaf] = createInvoker(route);",
" }",
" };",
" const freezeTree = (value) => {",
" if (!value || typeof value !== \"object\" || Object.isFrozen(value)) {",
" return value;",
" }",
" for (const key of Object.keys(value)) {",
" freezeTree(value[key]);",
" }",
" return Object.freeze(value);",
" };",
" const commands = {};",
" for (const route of routes) {",
" installRoute(commands, route);",
" }",
" const api = {",
" routes: () => routes.slice(),",
" can: (route) => routeSet.has(route),",
" command: (route) => {",
" if (!routeSet.has(route)) {",
" throw new LepusaInvokeError(`Unknown Lepusa command route: ${route}`, { code: \"unknown-command\", route });",
" }",
" return createInvoker(route);",
" },",
" commands: freezeTree(commands),",
" listen: (name, handler) => {",
" if (typeof name !== \"string\" || name === \"\") {",
" throw new Error(\"Lepusa event name is required\");",
" }",
" if (typeof handler !== \"function\") {",
" throw new Error(\"Lepusa event handler must be a function\");",
" }",
" let handlers = eventHandlers.get(name);",
" if (!handlers) {",
" handlers = new Set();",
" eventHandlers.set(name, handlers);",
" }",
" handlers.add(handler);",
" return () => api.unlisten(name, handler);",
" },",
" unlisten: (name, handler) => {",
" const handlers = eventHandlers.get(name);",
" if (!handlers) {",
" return false;",
" }",
" const removed = handlers.delete(handler);",
" if (handlers.size === 0) {",
" eventHandlers.delete(name);",
" }",
" return removed;",
" },",
" emit: (name, payload = null) => emitNative(null, name, payload),",
" emitTo: (target, name, payload = null) => emitNative(target, name, payload),",
" invoke: async (route, payload = null, options = {}) => {",
" if (!routeSet.has(route)) {",
" throw new LepusaInvokeError(`Unknown Lepusa command route: ${route}`, { code: \"unknown-command\", route });",
" }",
" const parts = route.split(\".\");",
" return callNative(parts[0], parts.slice(1).join(\".\"), payload, route, options);",
" },",
" stream: async (route, payload = null, options = {}) => attachStream(await api.invoke(route, payload, options)),",
" drainStream: (channelId, options = {}) => runtimeControl(\"stream.drain\", { channelId }, options),",
" cancelStream: (channelId, options = {}) => runtimeControl(\"stream.cancel\", { channelId }, options),",
" closeResource: (resource, options = {}) => {",
" const payload = typeof resource === \"string\" ? { id: resource } : { id: resource && resource.id, kind: resource && resource.kind };",
" return runtimeControl(\"resource.close\", payload, options);",
" },",
" InvokeError: LepusaInvokeError,",
" isInvokeError: (error) => error instanceof LepusaInvokeError || (error && error.name === \"LepusaInvokeError\" && typeof error.code === \"string\"),",
" };",
" Object.defineProperty(globalThis, eventDispatchHook, {",
" value: dispatchEvent,",
" configurable: true,",
" });",
" for (const route of routes) {",
" installRoute(api, route, { direct: true });",
" }",
" Object.defineProperty(globalThis, globalName, {",
" value: Object.freeze(api),",
" configurable: true,",
" });",
"})();",
"",
].join("\n")
}
///|
fn String::js_string_literal(self : String) -> String {
"\"" +
self
.replace_all(old="\\", new="\\\\")
.replace_all(old="\"", new="\\\"")
.replace_all(old="\n", new="\\n")
.replace_all(old="\r", new="\\r")
.replace_all(old="\t", new="\\t") +
"\""
}