pub const ACTOR_SW_JS: &str = "/* Actor-RTC Service Worker entry \u{2014} wasm-bindgen guest path.\n *\n * Loads a wasm-bindgen guest bundle (produced by `tools/wit-compile-web` \u{2192}\n * `actr-web-abi` + `wasm-pack --target no-modules`) and bridges it into the\n * sw-host runtime. This is the only browser dispatch path post Option U\n * Phase 8 (the previous Component Model + jco variant was deleted; see\n * `bindings/web/docs/option-u-wit-compile-web.zh.md` \u{a7}11).\n *\n * Required `runtimeConfig` fields:\n * package_url - URL of the `.actr` workload package\n * runtime_wasm_url - URL of the SW runtime WASM (sw-host wasm-pack output)\n * trust - array of `TrustAnchor` entries\n *\n * Companion artefact convention:\n * `<package_url>.wbg/guest.js` and `<package_url>.wbg/guest_bg.wasm` must\n * be served alongside the .actr; the CLI mounts them when the companion\n * directory exists.\n */\n\n/* global wasm_bindgen */\n\nlet SW_BROADCAST = null;\n\n// \u{2500}\u{2500} Console interception: forward WASM logs to main page \u{2500}\u{2500}\n//\n// Identical to actor.sw.js; the WBG path still emits Rust `log::info!` from\n// sw-host, so this broadcaster stays the same.\n(function () {\n const _origInfo = console.info;\n const _origWarn = console.warn;\n const _origError = console.error;\n const _origLog = console.log;\n\n function extractMessage(args) {\n return Array.from(args)\n .filter(\n (a) => typeof a === \'string\' && !/^\\s*(color|background|font-weight|padding)\\s*:/.test(a)\n )\n .join(\' \')\n .replace(/%c/g, \'\')\n .trim();\n }\n\n function broadcast(data) {\n self.clients\n .matchAll({ type: \'window\' })\n .then((clients) => {\n for (const client of clients) {\n client.postMessage(data);\n }\n })\n .catch(() => {\n /* ignore */\n });\n }\n SW_BROADCAST = broadcast;\n\n console.info = function (...args) {\n _origInfo.apply(console, args);\n const msg = extractMessage(args);\n if (msg.includes(\'\u{1f4e8}\') && msg.includes(\'Echo request\')) {\n const m = msg.match(/message=\'([^\']*)\'/);\n broadcast({ type: \'echo_event\', event: \'request\', detail: m ? m[1] : \'\', ts: Date.now() });\n } else if (msg.includes(\'\u{1f4e4}\') && msg.includes(\'Echo response\')) {\n const m = msg.match(/reply=\'([^\']*)\'/);\n broadcast({ type: \'echo_event\', event: \'response\', detail: m ? m[1] : \'\', ts: Date.now() });\n }\n if (\n msg.includes(\'[SW]\') ||\n msg.includes(\'[WBG]\') ||\n msg.includes(\'EchoService\') ||\n msg.includes(\'Echo\') ||\n msg.includes(\'Registering\') ||\n msg.includes(\'SendEcho\') ||\n msg.includes(\'Scheduler\') ||\n msg.includes(\'Dispatcher\') ||\n msg.includes(\'HostGate\') ||\n msg.includes(\'PeerGate\')\n ) {\n broadcast({ type: \'sw_log\', level: \'info\', message: msg, ts: Date.now() });\n }\n };\n\n console.warn = function (...args) {\n _origWarn.apply(console, args);\n const msg = extractMessage(args);\n if (msg.length > 0) {\n broadcast({ type: \'sw_log\', level: \'warn\', message: msg, ts: Date.now() });\n }\n };\n\n console.error = function (...args) {\n _origError.apply(console, args);\n const msg = extractMessage(args);\n broadcast({ type: \'sw_log\', level: \'error\', message: msg, ts: Date.now() });\n if (msg.includes(\'Echo\') || msg.includes(\'handle_request\') || msg.includes(\'service\')) {\n broadcast({ type: \'echo_event\', event: \'error\', detail: msg, ts: Date.now() });\n }\n };\n\n console.log = function (...args) {\n _origLog.apply(console, args);\n const msg = extractMessage(args);\n if (\n msg.includes(\'[EchoService]\') ||\n msg.includes(\'[SW]\') ||\n msg.includes(\'[WBG]\') ||\n msg.includes(\'[SendEcho]\') ||\n msg.includes(\'[WebRTC]\')\n ) {\n broadcast({ type: \'sw_log\', level: \'info\', message: msg, ts: Date.now() });\n }\n };\n})();\n\n/** @type {import(\'@actrium/actr-web\').SwRuntimeConfig | null} */\nlet RUNTIME_CONFIG = null;\n\nlet wasmReady = false;\nlet wsProbeDone = false;\n\nconst clientPorts = new Map();\nconst browserToSwClient = new Map();\nlet staleCleanupTimer = null;\n\nasync function cleanupStaleClients() {\n if (!wasmReady) return;\n try {\n const activeWindows = await self.clients.matchAll({ type: \'window\' });\n const activeIds = new Set(activeWindows.map((c) => c.id));\n for (const [browserId, swClientId] of browserToSwClient.entries()) {\n if (!activeIds.has(browserId)) {\n console.log(\'[SW] Cleaning up stale client:\', swClientId, \'(browser:\', browserId, \')\');\n browserToSwClient.delete(browserId);\n clientPorts.delete(swClientId);\n try {\n await wasm_bindgen.unregister_client(swClientId);\n } catch (e) {\n console.warn(\'[SW] unregister_client error for\', swClientId, \':\', e);\n }\n }\n }\n } catch (e) {\n console.warn(\'[SW] cleanupStaleClients error:\', e);\n }\n}\n\nfunction scheduleStaleClientCleanup(delayMs = 0) {\n if (staleCleanupTimer) {\n clearTimeout(staleCleanupTimer);\n }\n staleCleanupTimer = setTimeout(() => {\n staleCleanupTimer = null;\n cleanupStaleClients();\n }, delayMs);\n}\n\nfunction emitSwLog(level, message, detail) {\n for (const port of clientPorts.values()) {\n try {\n port.postMessage({\n type: \'webrtc_event\',\n payload: {\n eventType: \'sw_log\',\n data: { level, message, detail },\n },\n });\n } catch (_) {\n /* port may be closed */\n }\n }\n}\n\n// \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n// Schema adapters between sw-host (camelCase) and actr-web-abi guest\n// (kebab-case, from serde with `#[serde(rename = \"...\")]`).\n//\n// sw-host\'s `actr_id_to_js` emits `{ realm: { realmId }, serialNumber,\n// type: { manufacturer, name, version } }` (camelCase, the WBG guest\'s\n// `serde-wasm-bindgen` shape).\n// actr-web-abi `ActrId` deserialises from `{ realm: { \"realm-id\" },\n// \"serial-number\", type: {...} }`.\n// \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\nfunction actrIdCamelToKebab(id) {\n if (id == null) return id;\n return {\n realm: { \'realm-id\': id.realm && id.realm.realmId },\n \'serial-number\': id.serialNumber,\n // `type` is a WIT-reserved-ish name; both sides keep the key as `type`.\n type: id.type,\n };\n}\n\nfunction actrIdKebabToCamel(id) {\n if (id == null) return id;\n return {\n realm: { realmId: id.realm && id.realm[\'realm-id\'] },\n serialNumber: id[\'serial-number\'],\n type: id.type,\n };\n}\n\n// \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n// Install the 8 `actrHost*` globals the wasm-bindgen guest imports.\n//\n// The guest\'s `.wasm` imports these by bare global name (wasm-pack\n// `--target no-modules` resolves them from the enclosing global scope \u{2014}\n// see `echo_*_guest_wbg.js` `__wbg_actrHostCallRaw_*` entries).\n//\n// Each one is a thin proxy onto `wasm_bindgen.host_*_async` etc. from\n// `actr_sw_host.js`, with argument/result reshaping to bridge the\n// serde-wasm-bindgen (kebab) <-> hand-written Reflect (camel) gap.\n// \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\nfunction installActrHostGlobals() {\n // \u{3b3}-unified (Option U Phase 6 \u{a7}3.4/\u{a7}3.6): every `actrHost*` global now\n // accepts `requestId` as the first argument, threading it into the\n // sw-host `host_*_async` wasm-bindgen imports so the `DISPATCH_CTXS`\n // HashMap can look up the per-dispatch `RuntimeContext` keyed on that\n // id. This lets multiple concurrent dispatches share the single-threaded\n // JS bridge without clobbering each other\'s runtime context (TD-003).\n\n self.actrHostCall = async function (requestId, target, routeKey, payload) {\n // actr-web-abi `call` passes `Dest` variant as `{ actor: {...} }` or\n // `\"shell\"`/`\"local\"`; sw-host\'s `host_call_async` reads `{ tag, val }`.\n // The WBG path is only exercised by the echo client which uses\n // `call_raw`, but keep the shape future-proof.\n let destCamel;\n if (target === \'shell\' || target === \'local\') {\n destCamel = { tag: target };\n } else if (target && target.actor) {\n destCamel = { tag: \'actor\', val: actrIdKebabToCamel(target.actor) };\n } else {\n throw new Error(\'[WBG] actrHostCall: unknown dest shape \' + JSON.stringify(target));\n }\n const u8 = payload instanceof Uint8Array ? payload : new Uint8Array(payload);\n try {\n const reply = await wasm_bindgen.host_call_async(requestId, destCamel, routeKey, u8);\n return { ok: Array.from(reply) };\n } catch (e) {\n // Host rejects with Error carrying `actrErrorTag`; fold into the\n // guest-expected `Result::Err(ActrError)` shape.\n return { err: mapHostErrorToActr(e) };\n }\n };\n\n self.actrHostCallRaw = async function (requestId, target, routeKey, payload) {\n // `target` is an `ActrId` (kebab) from the guest.\n const targetCamel = actrIdKebabToCamel(target);\n const u8 = payload instanceof Uint8Array ? payload : new Uint8Array(payload);\n try {\n const reply = await wasm_bindgen.host_call_raw_async(requestId, targetCamel, routeKey, u8);\n // `reply` is a `Uint8Array` from Rust; guest expects Vec<u8> \u{2014}\n // serde-wasm-bindgen deserialises Array / Uint8Array / numbers\n // buffer \u{2014} Uint8Array works directly, but the outer Result\n // needs the `Ok` tag matching `#[serde(rename = \"ok\")]`?\n // Actually generated `ActrError` variants use kebab tags but\n // `Result<T, E>` is serde\'s default which emits `{ Ok: ... }`\n // (capitalized). Keep capitalization exact.\n return { Ok: reply };\n } catch (e) {\n return { Err: mapHostErrorToActr(e) };\n }\n };\n\n self.actrHostDiscover = async function (requestId, targetType) {\n // `targetType` is `ActrType { manufacturer, name, version }` \u{2014}\n // identical shape on both sides.\n try {\n const id = await wasm_bindgen.host_discover_async(requestId, targetType);\n return { Ok: actrIdCamelToKebab(id) };\n } catch (e) {\n return { Err: mapHostErrorToActr(e) };\n }\n };\n\n self.actrHostGetCallerId = async function (requestId) {\n try {\n const id = wasm_bindgen.host_get_caller_id(requestId);\n if (id == null || id === undefined) return null;\n return actrIdCamelToKebab(id);\n } catch (e) {\n console.warn(\'[WBG] host_get_caller_id threw:\', e);\n return null;\n }\n };\n\n self.actrHostGetRequestId = async function (requestId) {\n try {\n return wasm_bindgen.host_get_request_id(requestId);\n } catch (e) {\n console.warn(\'[WBG] host_get_request_id threw:\', e);\n return \'\';\n }\n };\n\n self.actrHostGetSelfId = async function (requestId) {\n const id = wasm_bindgen.host_get_self_id(requestId);\n return actrIdCamelToKebab(id);\n };\n\n self.actrHostLogMessage = async function (requestId, level, message) {\n wasm_bindgen.host_log_message(requestId, level, message);\n };\n\n self.actrHostTell = async function (requestId, target, routeKey, payload) {\n let destCamel;\n if (target === \'shell\' || target === \'local\') {\n destCamel = { tag: target };\n } else if (target && target.actor) {\n destCamel = { tag: \'actor\', val: actrIdKebabToCamel(target.actor) };\n } else {\n throw new Error(\'[WBG] actrHostTell: unknown dest shape \' + JSON.stringify(target));\n }\n const u8 = payload instanceof Uint8Array ? payload : new Uint8Array(payload);\n try {\n await wasm_bindgen.host_tell_async(requestId, destCamel, routeKey, u8);\n return { Ok: null };\n } catch (e) {\n return { Err: mapHostErrorToActr(e) };\n }\n };\n\n console.log(\'[SW][WBG] actrHost* globals installed (\u{3b3}-unified, request_id-threaded)\');\n}\n\nfunction mapHostErrorToActr(e) {\n // sw-host\'s `actr_error_to_js` attaches `actrErrorTag` + message.\n // actr-web-abi `ActrError` variant serde tags are kebab-case; we\n // return the matching `{ \"kebab-tag\": string }` shape.\n const tag = (e && e.actrErrorTag) || \'internal\';\n const msg = (e && e.message) || String(e);\n // `timed-out` is a unit variant; serde emits it as a bare string.\n if (tag === \'timed-out\') return \'timed-out\';\n // `dependency-not-found` carries a record payload; we don\'t have the\n // fields cleanly separated here, so fall back to `internal` to keep\n // the result deserialisable.\n if (tag === \'dependency-not-found\') {\n return { internal: msg };\n }\n return { [tag]: msg };\n}\n\n// \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n// Main bootstrap: load sw-host, load WBG guest, bridge them.\n// \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\n\nasync function loadWithGuestBridge(packageUrl, runtimeWasmUrl) {\n emitSwLog(\'info\', \'guest_bridge_start\', { packageUrl, runtimeWasmUrl });\n\n // \u{2500}\u{2500} 1. Load sw-host WASM + JS glue (unchanged from CM path) \u{2500}\u{2500}\n const jsUrl = runtimeWasmUrl.replace(/_bg\\.wasm$/, \'.js\');\n const jsResp = await fetch(jsUrl, { cache: \'no-store\' });\n if (!jsResp.ok) {\n throw new Error(\'[SW] Failed to fetch runtime JS glue: \' + jsResp.status);\n }\n const jsText = await jsResp.text();\n const patchedText = jsText.replace(\'let wasm_bindgen =\', \'self.wasm_bindgen =\');\n (0, eval)(patchedText);\n emitSwLog(\'info\', \'guest_bridge_runtime_js_loaded\', jsText.length);\n\n await wasm_bindgen({ module_or_path: runtimeWasmUrl });\n wasm_bindgen.init_global();\n emitSwLog(\'info\', \'guest_bridge_runtime_ready\', null);\n\n // \u{2500}\u{2500} 2. Install actrHost* globals BEFORE guest instantiation \u{2500}\u{2500}\n // The guest wasm resolves imports at instantiate time; globals must\n // exist up-front.\n installActrHostGlobals();\n\n // \u{2500}\u{2500} 3. Verify + extract .actr (keeps the mandatory-verify contract) \u{2500}\u{2500}\n const resp = await fetch(packageUrl, { cache: \'no-store\' });\n if (!resp.ok) {\n throw new Error(\'[SW] Failed to fetch .actr package: \' + resp.status);\n }\n const buffer = await resp.arrayBuffer();\n emitSwLog(\'info\', \'guest_bridge_actr_size\', buffer.byteLength);\n\n const trustJson = JSON.stringify(\n RUNTIME_CONFIG && Array.isArray(RUNTIME_CONFIG.trust) ? RUNTIME_CONFIG.trust : []\n );\n try {\n // Phase 4c: the `.actr` package for the WBG variant wraps the\n // wasm-bindgen core wasm; we still call verify_and_extract to honour\n // signing, but we intentionally ignore `extracted.binary` because the\n // actual module we load is the companion `guest_bg.wasm` shipped in\n // the `.wbg/` sibling directory (see \u{a7}5 below). The core wasm inside\n // the .actr is identical; we just need the JS glue from the sibling\n // bundle to drive it with wasm-bindgen conventions.\n wasm_bindgen.verify_and_extract_actr_package(new Uint8Array(buffer), trustJson);\n emitSwLog(\'info\', \'guest_bridge_verify_ok\', null);\n } catch (verifyError) {\n emitSwLog(\'error\', \'guest_bridge_verify_failed\', String(verifyError));\n throw verifyError;\n }\n\n // \u{2500}\u{2500} 4. Resolve wbg bundle URL \u{2500}\u{2500}\n const wbgJsUrl =\n (RUNTIME_CONFIG && RUNTIME_CONFIG.wbg_module_url) ||\n packageUrl.replace(/\\.actr$/, \'\') + \'.wbg/guest.js\';\n emitSwLog(\'info\', \'guest_bridge_guest_js_url\', wbgJsUrl);\n\n // \u{2500}\u{2500} 5. Fetch guest JS glue, rewrite top-level `let wasm_bindgen` to\n // write to a dedicated global (avoid clobbering sw-host\'s).\n const wbgResp = await fetch(wbgJsUrl, { cache: \'no-store\' });\n if (!wbgResp.ok) {\n throw new Error(\'[SW] Failed to fetch WBG guest JS: \' + wbgResp.status);\n }\n const wbgSrc = await wbgResp.text();\n // wasm-pack `--target no-modules` emits `let wasm_bindgen = (function(exports) {...})({});`.\n // Rewrite to `self.actrGuestBindgen = ...` so the glue coexists with\n // sw-host (also keyed to `self.wasm_bindgen`).\n const patchedGuestSrc = wbgSrc.replace(/^\\s*let\\s+wasm_bindgen\\s*=/m, \'self.actrGuestBindgen =\');\n if (patchedGuestSrc === wbgSrc) {\n throw new Error(\n \'[SW] WBG guest JS does not match expected `let wasm_bindgen =` preamble; refusing to eval\'\n );\n }\n (0, eval)(patchedGuestSrc);\n if (typeof self.actrGuestBindgen !== \'function\') {\n throw new Error(\'[SW] actrGuestBindgen init not installed after eval\');\n }\n\n // \u{2500}\u{2500} 6. Instantiate guest wasm \u{2500}\u{2500}\n const wbgWasmUrl = wbgJsUrl.replace(/\\.js$/, \'_bg.wasm\');\n emitSwLog(\'info\', \'guest_bridge_guest_wasm_url\', wbgWasmUrl);\n await self.actrGuestBindgen({ module_or_path: wbgWasmUrl });\n emitSwLog(\'info\', \'guest_bridge_guest_instantiated\', Object.keys(self.actrGuestBindgen));\n\n // \u{2500}\u{2500} 7. Build dispatchFn that adapts sw-host envelope (camelCase) to\n // the actr-web-abi guest (kebab-case + Vec<u8>).\n const dispatchFn = async (envelope) => {\n // sw-host builds `{ requestId, routeKey, payload: Uint8Array }`.\n // actr-web-abi `RpcEnvelope` uses `request-id`, `route-key`, `payload`.\n const kebabEnv = {\n \'request-id\': envelope.requestId,\n \'route-key\': envelope.routeKey,\n // serde-wasm-bindgen reads Vec<u8> from Uint8Array fine, but\n // also accepts plain arrays. Uint8Array is the natural form.\n payload: envelope.payload,\n };\n const result = await self.actrGuestBindgen.dispatch(kebabEnv);\n // actr-web-abi `dispatch` returns `Result<Vec<u8>, ActrError>` \u{2014}\n // serde-wasm-bindgen encodes `Ok(bytes)` as `{ Ok: [...] }`.\n if (result && Object.prototype.hasOwnProperty.call(result, \'Ok\')) {\n const ok = result.Ok;\n if (ok instanceof Uint8Array) return ok;\n if (Array.isArray(ok)) return new Uint8Array(ok);\n throw new Error(\'[WBG] dispatch Ok was not bytes-like: \' + typeof ok);\n }\n if (result && Object.prototype.hasOwnProperty.call(result, \'Err\')) {\n throw new Error(\'[WBG] guest dispatch returned Err: \' + JSON.stringify(result.Err));\n }\n // Older serde shapes or direct Uint8Array \u{2014} accept as last resort.\n if (result instanceof Uint8Array) return result;\n throw new Error(\'[WBG] guest dispatch returned unexpected shape: \' + JSON.stringify(result));\n };\n\n wasm_bindgen.register_guest_workload(dispatchFn);\n emitSwLog(\'info\', \'guest_bridge_ready\', \'WBG workload registered\');\n}\n\nasync function ensureWasmReady() {\n if (wasmReady) return;\n\n if (!RUNTIME_CONFIG) {\n throw new Error(\'[SW] Cannot load WASM: RUNTIME_CONFIG not yet received\');\n }\n\n const packageUrl = RUNTIME_CONFIG.package_url;\n const runtimeWasmUrl = RUNTIME_CONFIG.runtime_wasm_url;\n\n try {\n if (!wsProbeDone) {\n wsProbeDone = true;\n try {\n emitSwLog(\'info\', \'ws_probe_start\', RUNTIME_CONFIG.signaling_url);\n const probe = new WebSocket(RUNTIME_CONFIG.signaling_url);\n probe.binaryType = \'arraybuffer\';\n probe.onopen = () => {\n emitSwLog(\'info\', \'ws_probe_open\', null);\n probe.close();\n };\n probe.onerror = () => {\n emitSwLog(\'error\', \'ws_probe_error\', null);\n };\n probe.onclose = (event) => {\n emitSwLog(\'info\', \'ws_probe_close\', {\n code: event.code,\n reason: event.reason,\n wasClean: event.wasClean,\n });\n };\n } catch (error) {\n emitSwLog(\'error\', \'ws_probe_throw\', String(error));\n }\n }\n\n if (!runtimeWasmUrl || !packageUrl) {\n throw new Error(\'[SW] RUNTIME_CONFIG requires both `runtime_wasm_url` and `package_url`\');\n }\n await loadWithGuestBridge(packageUrl, runtimeWasmUrl);\n\n wasmReady = true;\n emitSwLog(\'info\', \'wasm_ready\', null);\n } catch (error) {\n console.error(\n \'[SW] WASM init failed:\',\n error && error.message ? error.message : String(error),\n \'name=\' + (error && error.name),\n \'stack=\' + (error && error.stack)\n );\n emitSwLog(\'error\', \'wasm_init_failed\', {\n error: error && error.message ? error.message : String(error),\n name: error && error.name ? error.name : undefined,\n stack: error && error.stack ? error.stack : undefined,\n packageUrl: packageUrl || null,\n runtimeWasmUrl: runtimeWasmUrl || null,\n });\n throw error;\n }\n}\n\nself.addEventListener(\'install\', (event) => {\n console.log(\'[SW] installing (WBG variant)...\');\n event.waitUntil(self.skipWaiting());\n});\n\nself.addEventListener(\'activate\', (event) => {\n console.log(\'[SW] activated (WBG variant)\');\n event.waitUntil(self.clients.claim());\n});\n\nself.addEventListener(\'message\', async (event) => {\n if (event.data && event.data.type === \'PING\') {\n if (event.source && event.source.postMessage) {\n event.source.postMessage({ type: \'PONG\' });\n }\n return;\n }\n\n if (event.data && event.data.type === \'CLIENT_UNREGISTER\') {\n const clientId = event.data.clientId;\n if (!clientId) return;\n try {\n await ensureWasmReady();\n await wasm_bindgen.unregister_client(clientId);\n } catch (e) {\n console.warn(\'[SW] top-level unregister_client error for\', clientId, \':\', e);\n }\n clientPorts.delete(clientId);\n for (const [browserId, swClientId] of browserToSwClient.entries()) {\n if (swClientId === clientId) {\n browserToSwClient.delete(browserId);\n }\n }\n emitSwLog(\'info\', \'client_unregistered\', { clientId, variant: \'wbg\', source: \'top-level\' });\n return;\n }\n\n if (!event.data || event.data.type !== \'DOM_PORT_INIT\') {\n return;\n }\n\n const port = event.data.port;\n const clientId = event.data.clientId;\n if (!port || !clientId) return;\n\n if (event.data.runtimeConfig && !RUNTIME_CONFIG) {\n RUNTIME_CONFIG = event.data.runtimeConfig;\n }\n\n const browserId = event.source && event.source.id;\n if (browserId) {\n const previousClientId = browserToSwClient.get(browserId);\n if (previousClientId && previousClientId !== clientId) {\n console.log(\n \'[SW] browser client remapped, unregistering previous client:\',\n previousClientId,\n \'browser:\',\n browserId\n );\n const previousPort = clientPorts.get(previousClientId);\n if (previousPort) {\n try {\n previousPort.close();\n } catch (_) {\n /* ignore */\n }\n clientPorts.delete(previousClientId);\n }\n try {\n await ensureWasmReady();\n await wasm_bindgen.unregister_client(previousClientId);\n } catch (e) {\n console.warn(\'[SW] remap unregister_client error for\', previousClientId, \':\', e);\n }\n }\n }\n\n clientPorts.set(clientId, port);\n if (browserId) {\n browserToSwClient.set(browserId, clientId);\n }\n\n cleanupStaleClients();\n scheduleStaleClientCleanup(1500);\n\n console.log(\'[SW] port initialized (WBG) for client:\', clientId, \'total:\', clientPorts.size);\n\n if (event.source && event.source.postMessage) {\n event.source.postMessage({ type: \'sw_ack\', message: \'port_ready\' });\n }\n\n emitSwLog(\'info\', \'sw_env\', {\n clientId,\n variant: \'wbg\',\n location: self.location ? self.location.href : null,\n totalClients: clientPorts.size,\n });\n\n let portMessageChain = Promise.resolve();\n\n async function processPortMessage(message) {\n try {\n await ensureWasmReady();\n } catch (error) {\n console.error(\'[SW] WASM not ready:\', error);\n return;\n }\n\n if (!message || !message.type) return;\n\n switch (message.type) {\n case \'control\':\n try {\n await wasm_bindgen.handle_dom_control(clientId, message.payload);\n } catch (error) {\n console.error(\'[SW] handle_dom_control failed:\', error);\n emitSwLog(\'error\', \'handle_dom_control_failed\', String(error));\n }\n break;\n\n case \'webrtc_event\':\n try {\n await wasm_bindgen.handle_dom_webrtc_event(clientId, message.payload);\n } catch (error) {\n console.error(\'[SW] handle_dom_webrtc_event failed:\', error);\n emitSwLog(\'error\', \'handle_dom_webrtc_event_failed\', String(error));\n }\n break;\n\n case \'fast_path_data\':\n try {\n await wasm_bindgen.handle_dom_fast_path(clientId, message.payload);\n } catch (error) {\n console.error(\'[SW] handle_dom_fast_path failed:\', error);\n emitSwLog(\'error\', \'handle_dom_fast_path_failed\', String(error));\n }\n break;\n\n case \'register_datachannel_port\':\n try {\n const dcPort = message.payload.port;\n const dcPeerId = message.payload.peerId;\n if (dcPort && dcPeerId) {\n await wasm_bindgen.register_datachannel_port(clientId, dcPeerId, dcPort);\n } else {\n console.warn(\'[SW] register_datachannel_port: missing port or peerId\');\n }\n } catch (error) {\n console.error(\'[SW] register_datachannel_port failed:\', error);\n emitSwLog(\'error\', \'register_datachannel_port_failed\', String(error));\n }\n break;\n\n case \'unregister_client\':\n try {\n await wasm_bindgen.unregister_client(clientId);\n clientPorts.delete(clientId);\n for (const [browserId, swClientId] of browserToSwClient.entries()) {\n if (swClientId === clientId) {\n browserToSwClient.delete(browserId);\n }\n }\n emitSwLog(\'info\', \'client_unregistered\', { clientId, variant: \'wbg\' });\n } catch (error) {\n console.error(\'[SW] unregister_client failed:\', error);\n emitSwLog(\'error\', \'unregister_client_failed\', { clientId, error: String(error) });\n }\n break;\n\n default:\n console.log(\'[SW] unknown message type:\', message.type);\n break;\n }\n }\n\n port.onmessage = (portEvent) => {\n const message = portEvent.data;\n portMessageChain = portMessageChain\n .then(() => processPortMessage(message))\n .catch((error) => {\n console.error(\'[SW] port message pipeline failed:\', error);\n emitSwLog(\'error\', \'port_message_pipeline_failed\', String(error));\n });\n };\n\n port.start();\n\n ensureWasmReady().then(async () => {\n try {\n if (!RUNTIME_CONFIG) {\n console.error(\'[SW] RUNTIME_CONFIG not received from main thread\');\n return;\n }\n await wasm_bindgen.register_client(clientId, RUNTIME_CONFIG, port);\n console.log(\'[SW] Client registered (WBG):\', clientId);\n emitSwLog(\'info\', \'client_registered\', { clientId, variant: \'wbg\' });\n } catch (error) {\n console.error(\'[SW] register_client failed:\', error);\n emitSwLog(\'error\', \'register_client_failed\', { clientId, error: String(error) });\n }\n });\n});\n";Expand description
Service Worker entry point — wasm-bindgen guest bridge (Option U).