// The render worker: numbl runs here so a seconds-long solve never // blocks the UI. Requests are processed one at a time; the core yields to // the macrotask queue between stages, which is when queued cancel messages // are delivered, so cancellation is cooperative and the caches survive it. import { renderRequest, type MaracaVariant } from './core.ts'; import { DEFAULT_MFILES } from './mfiles.ts'; import type { MFile, NoteSpec } from './types.ts'; export interface WorkerRenderMsg { kind: 'render'; id: number; variant: MaracaVariant; notes: NoteSpec[]; sampleRate: number; lengthSamples: number; quality: 'preview' | 'full'; settings?: Record; params?: Record; mfiles?: MFile[]; wantDiagnostics?: boolean; } export interface WorkerCancelMsg { kind: 'cancel'; id: number; } export type WorkerResponse = | { kind: 'progress'; id: number; frac: number; stage: string } | { kind: 'result'; id: number; audio: Float32Array; diag?: unknown } | { kind: 'cancelled'; id: number } | { kind: 'error'; id: number; message: string; file?: string; line?: number }; class Cancelled extends Error {} const cancelled = new Set(); const queue: WorkerRenderMsg[] = []; let running = false; const post = (m: WorkerResponse, transfer?: Transferable[]) => (self as unknown as Worker).postMessage(m, transfer ?? []); const SKIP_WARNING = /skipping (\S+\.m) \(syntax error at line (\d+)\)/; async function run(msg: WorkerRenderMsg): Promise { // numbl reports a file it could not parse through console.warn and then // skips it, so the failure surfaces at the call site as "Undefined // function". Capture the warning to report the actual syntax error. const warnings: string[] = []; const origWarn = console.warn; console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(' ')); }; try { const result = await renderRequest(msg.variant, { notes: msg.notes, sampleRate: msg.sampleRate, lengthSamples: msg.lengthSamples, quality: msg.quality, settings: msg.settings, params: msg.params, wantDiagnostics: msg.wantDiagnostics, }, { mfiles: msg.mfiles ?? DEFAULT_MFILES, post: (frac, stage) => post({ kind: 'progress', id: msg.id, frac, stage }), checkCancel: () => { if (cancelled.has(msg.id)) throw new Cancelled(); }, yieldToHost: () => new Promise((res) => setTimeout(res, 0)), }); const transfer: Transferable[] = [result.audio.buffer]; if (result.diag) { for (const v of Object.values(result.diag as unknown as Record)) { if (v instanceof Float32Array && v.buffer !== result.audio.buffer) transfer.push(v.buffer); } } post({ kind: 'result', id: msg.id, audio: result.audio, diag: result.diag }, transfer); } catch (err) { if (err instanceof Cancelled) { post({ kind: 'cancelled', id: msg.id }); } else { const e = err as { message?: string; file?: string; line?: number }; let m: WorkerResponse = { kind: 'error', id: msg.id, message: e?.message ? String(e.message) : String(err), file: typeof e?.file === 'string' ? e.file : undefined, line: typeof e?.line === 'number' ? e.line : undefined, }; for (const w of warnings) { const hit = SKIP_WARNING.exec(w); if (hit) { m = { kind: 'error', id: msg.id, message: 'syntax error', file: hit[1], line: Number(hit[2]) }; break; } } post(m); } } finally { console.warn = origWarn; cancelled.delete(msg.id); } } async function pump(): Promise { if (running) return; running = true; while (queue.length > 0) { const msg = queue.shift()!; if (cancelled.has(msg.id)) { post({ kind: 'cancelled', id: msg.id }); cancelled.delete(msg.id); continue; } await run(msg); } running = false; } self.onmessage = (ev: MessageEvent) => { const msg = ev.data; if (msg.kind === 'render') { queue.push(msg); void pump(); } else if (msg.kind === 'cancel') { cancelled.add(msg.id); } };