Skip to main content

ACTOR_SW_JS

Constant ACTOR_SW_JS 

Source
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).