import { loadModuleRunner } from './module-runner.ts'; import { type InvokeMessage, type ResultMessage, WORKER_CHANNEL, type WorkerChannelMessage, } from './protocol.ts'; /** options for {@link runWorker} */ export interface RunWorkerOptions { /** absolute path to the worker module, with any query */ entry: string; /** URL of `vite/module-runner`, matching the `vite` that runs the dev server */ moduleRunner: string; } /** * evaluates a module through Vite in a worker spawned by the dev server's Deno process. * call before registering message listeners. * * @param options worker options * @returns resolves after module evaluation * @throws when the module fails to load or evaluate */ export const runWorker = async ({ entry, moduleRunner }: RunWorkerOptions): Promise => { // buffer messages during async module loading to preserve native worker startup ordering const early: MessageEvent[] = []; const capture = (event: Event) => { if (event instanceof MessageEvent) { event.stopImmediatePropagation(); early.push(event); } }; globalThis.addEventListener('message', capture); const channel = new BroadcastChannel(WORKER_CHANNEL); const invokes = new Map void>(); channel.onmessage = ({ data }: MessageEvent) => { if (data.type === 'result') { const resolve = invokes.get(data.id); if (resolve) { invokes.delete(data.id); resolve(data.result); } } }; const createRunner = await loadModuleRunner(moduleRunner); const runner = createRunner({ transport: { invoke(payload) { const id = crypto.randomUUID(); const { promise, resolve } = Promise.withResolvers(); invokes.set(id, resolve); const message: InvokeMessage = { type: 'invoke', id, payload }; channel.postMessage(message); return promise; }, }, // re-evaluation would leave existing message listeners registered hmr: false, }); await runner.import(entry); globalThis.removeEventListener('message', capture); for (const { data, ports } of early) { globalThis.dispatchEvent(new MessageEvent('message', { data, ports: [...ports] })); } };