(opts)
| 5 | import { DEFAULT_HOST, DEFAULT_IMPORTS } from './defaults.js'; |
| 6 | |
| 7 | export async function createWorker(opts) { |
| 8 | // Chromium blocks `new Worker(crossOriginUrl)` even with `type:'module'`; cross-origin runtimes need the Blob bootstrap below. |
| 9 | const workerUrl = new URL('../worker/worker.js', import.meta.url); |
| 10 | const sameOrigin = workerUrl.origin === self.location.origin; |
| 11 | const worker = sameOrigin |
| 12 | ? new Worker(workerUrl, { type: 'module' }) |
| 13 | : spawnCrossOriginWorker(workerUrl.href); |
| 14 | |
| 15 | let reqIdCounter = 0; |
| 16 | const pending = new Map(); |
| 17 | let outputHandler = null; |
| 18 | |
| 19 | /* Fire a string into the running script's `receive()` queue. Defined early so main-thread module factories can capture it. */ |
| 20 | const pushEvent = (message) => worker.postMessage({ type: 'push-event', message: String(message) }); |
| 21 | |
| 22 | /* Resolve each `mainThreadModules[name]` (factory or object) into a flat handler map keyed `module:name`. */ |
| 23 | const mainThreadHandlers = {}; |
| 24 | const manifests = []; |
| 25 | for (const [modName, src] of Object.entries(opts?.mainThreadModules || {})) { |
| 26 | const handlers = typeof src === 'function' ? src({ pushEvent }) : src; |
| 27 | manifests.push({ name: modName, exports: Object.keys(handlers) }); |
| 28 | for (const [fnName, handler] of Object.entries(handlers)) { |
| 29 | mainThreadHandlers[`${modName}:${fnName}`] = handler; |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | /* Lazy host modules: name -> ESM url, imported only when the worker reports the bare name is used. Base defaults sit under user entries; `defaults:false` opts out. */ |
| 34 | const hostUrls = { ...(opts?.defaults !== false ? DEFAULT_HOST : {}), ...(opts?.hostModules || {}) }; |
| 35 | const loadedHosts = new Map(); // name -> export names, memoized across runs |
| 36 | const loadHostModule = async (name) => { |
| 37 | if (loadedHosts.has(name)) return loadedHosts.get(name); |
| 38 | const url = hostUrls[name]; |
| 39 | if (!url) throw new Error(`no host module registered for '${name}'`); |
| 40 | const mod = await import(url); |
| 41 | const factory = mod[name] ?? mod.default; |
| 42 | const handlers = typeof factory === 'function' ? factory({ pushEvent }) : factory; |
| 43 | for (const [fnName, handler] of Object.entries(handlers)) { |
| 44 | mainThreadHandlers[`${name}:${fnName}`] = handler; |
| 45 | } |
| 46 | const exports = Object.keys(handlers); |
| 47 | loadedHosts.set(name, exports); |
| 48 | return exports; |
| 49 | }; |
| 50 | |
| 51 | worker.onmessage = async ({ data }) => { |
| 52 | if (data.type === 'line') { |
| 53 | if (outputHandler) outputHandler(data.text); |
| 54 | return; |
| 55 | } |
| 56 | if (data.type === 'host-call') { |
| 57 | const handler = mainThreadHandlers[`${data.module}:${data.name}`]; |
| 58 | if (!handler) { |
| 59 | worker.postMessage({ type: 'host-call-response', reqId: data.reqId, error: `no main-thread handler for '${data.module}.${data.name}'` }); |
| 60 | return; |
| 61 | } |
| 62 | try { |
| 63 | const value = await handler(...data.args); |
| 64 | worker.postMessage({ type: 'host-call-response', reqId: data.reqId, value }); |
no test coverage detected