Generated from Markdown source. Edit the source, not this HTML.
Worker render diagnostics, deadlines and rollback #
renderSequenceInWorker keeps the main thread free while a static module Worker renders a score and streams encoded WAV chunks to a host sink. This guide covers the startup handshake, optional phase diagnostics and how a host should bound time and clean up partial files. Core behavior (one unacknowledged chunk, exact byte accounting, cancellation without waiting for a hung sink) is unchanged; see host integration.
Startup handshake #
The Worker posts { type: 'ready', protocol: 1 } after its module has been fetched, parsed and evaluated. The host sends the score only after a valid ready. The message is validated as strict own data; a second ready, a different protocol number or any chunk before ready fails the render, aborts the sink once and terminates the Worker.
startupTimeoutMs (optional, integer 1–2,147,483,647) starts when the Worker is constructed and stops at ready. If it expires the promise rejects with a TimeoutError DOMException, the Worker is terminated and sink.abort() is called. It never measures rendering, sink writes or close(), so a slow disk or a long score cannot trip it. It exists to tell "the Worker module never started" (bad workerUrl, blocked MIME, suspended environment) from "rendering is slow".
const result = await renderSequenceInWorker(score, {
sink, format: 'pcm24',
startupTimeoutMs: 10_000, // module start only
signal: AbortSignal.timeout(5 * 60_000), // your own whole-job deadline
phaseDiagnostics: true,
onPhase: status => console.debug(status.phase, status.phaseElapsedMs),
});
console.log(result.diagnostics.phases); // { status: 'completed', initializingMs, renderingMs, writingMs, closingMs, totalMs }
There is no built-in overall deadline and no automatic retry. A legitimately long render or a slow sink is indistinguishable from a stalled one without host knowledge, and a blind retry would re-send chunks to a sink that may already hold partial data. Use an AbortSignal for the policy that fits the app.
Phases #
onPhase(status) is called synchronously, for every transition, with a frozen { phase, elapsedMs, phaseElapsedMs, frames, totalFrames, bytesWritten, errors }:
| Phase | Interval |
|---|---|
initializing | from Worker construction until ready |
rendering | Worker compute: from ready/each acknowledgement until the next chunk arrives |
writing | from chunk arrival until the awaited sink.write() and onProgress finish |
closing | from the Worker's done until sink.close() resolves |
completed, cancelled, failed | terminal; reported once to onPhase |
rendering and writing alternate once per chunk. With phaseDiagnostics: true the successful result carries four accumulated durations and the total in diagnostics.phases. Failed or cancelled renders reject; use onPhase to capture their terminal status. Timings are host monotonic wall-clock readings (performance.now()), not DSP benchmarks, and include scheduler and GC pauses. Neither option changes the bytes written.
If onPhase or onProgress throws, the render fails with that error exactly once; the first failure is preserved and the observer is not called again after the terminal phase.
Partial output and rollback #
The sink owns its output. abort(reason) is called at most once, without being awaited, and the Worker is already terminated when it runs. It must make any in-flight write harmless and discard the partial file; cancellation cannot retract bytes already persisted. A robust File System Access sink:
async function createFileSink(handle: FileSystemFileHandle): Promise<WavSink> {
const writable = await handle.createWritable(); // writes to a swap file until close()
return {
write: bytes => writable.write(bytes),
close: () => writable.close(),
abort: reason => writable.abort(reason), // discards the swap file: the target is untouched
};
}
Acquire the handle from a trusted gesture, keep the cancellation signal alive across picker and createWritable() awaits, and clean up acquisition failures before starting a Worker. Never fall back to accumulating the whole file in a Blob.
Diagnosing a stalled start #
TimeoutErrorfromstartupTimeoutMs: fetchworkerUrldirectly. It must be same-origin, served as JavaScript withnosniffand a 200 status, next to the rest of the matchingdist/tree (npx --no-install opm-assets check <base-url>run from a project with OPM.js installed, verifies bytes, status and MIME without fetching a registry CLI).A Worker that reaches
readybut never delivers a chunk: look atstatus.phaseinonPhase. A longrenderingphase withframesnot advancing points at the score; a longwritingphase points at the sink.Repeated, order-dependent startup failures in an automation harness are not evidence about the package. During the 1.7 verification a managed browser closed targets or missed Worker start deadlines when many Workers started back to back, while the same path passed in isolation. The cause was not established, which is why this release adds measurement (
ready, phases) rather than an automatic retry.
Verified behavior #
test/render-worker.test.ts (checkout-only) runs the real Worker module in a thread. It checks the phase sequence, that diagnostics do not change the bytes, that startupTimeoutMs rejects a Worker that never answers (no score is sent and the sink is aborted once) while a 30 ms-per-write sink succeeds under a 1 s watchdog, rejection of invalid or premature ready messages, and that a throwing observer fails the render once.