(Critical Architecture Bug): Shared Worker Plugin Crash & Test Worker Collision when `workerThreads: false`
Issue :- (Critical Architecture Bug): Shared Worker Plugin Crash & Test Worker Collision when workerThreads: false
Metadata
- Title:
[Bug] Shared worker plugins crash with TypeError (unserializable MessagePort) and collide on undefined threadId when workerThreads: false - Severity: Critical (Breaks Shared Worker plugin API under process isolation)
- Affected Files:
lib/worker/channel.js(registerSharedWorker)lib/plugin-support/shared-workers.js(observeWorkerProcess)lib/fork.js(threadId)
Problem Summary
AVA supports running test files either in worker threads (workerThreads: true, default) or in child processes (workerThreads: false, essential for native bindings, memory isolation, or debugging).
However, the Shared Worker plugin infrastructure relies on two assumptions that break completely under child process execution:
lib/worker/channel.jssendsport: theirPort(MessagePort) overhandle.send(). In a child process,handleusesIpcHandlewhich calls Node'sprocess.send()withserialization: 'advanced'. In Node.js,v8.serialize({ port: new MessageChannel().port1 })throws a fatalTypeError [ERR_INVALID_STATE]: Unserializable host object: MessagePortbecauseMessagePortcannot be cloned across OS processes.- In
lib/fork.js,threadId: worker.threadIdis used as the unique test worker identifier. For child processes,worker.threadIdisundefined. Consequently, all concurrent test files register withid: undefinedinshared-worker-loader.js. When any single test file finishes and deregistersid: undefined, it prematurely shuts down the shared worker for all running test files.
Root Cause Analysis
Unserializable MessagePort over Process IPC: In
lib/worker/channel.jslines 142–153:const {port1: ourPort, port2: theirPort} = new MessageChannel(); const sharedWorkerHandle = new MessagePortHandle(ourPort); handle.send({ type: 'shared-worker-connect', channelId, filename, initialData, port: theirPort, }, [theirPort]);When
isRunningInChildProcessis true:handle = new IpcHandle(controlFlow(process));IpcHandle.sendcallsthis.sendRaw({ava: evt})without transfer lists. Passingport: theirPortinto Node'sprocess.send()triggers V8 serialization:// Node.js REPL test: const {MessageChannel} = require('node:worker_threads'); const {port1} = new MessageChannel(); require('node:v8').serialize({port: port1}); // => TypeError [ERR_INVALID_STATE]: Unserializable host object: MessagePortUndefined Worker Identifier: In
lib/fork.jslines 156–160:return { file, threadId: worker.threadId, promise,For
childProcess.fork(),worker.threadIdisundefined. Inlib/plugin-support/shared-workers.jslines 114–126:launched.worker.postMessage({ type: 'register-test-worker', id: fork.threadId, // <--- undefined! file: pathToFileURL(fork.file).toString(), port, }, [port]); fork.promise.finally(() => { launched.worker.postMessage({ type: 'deregister-test-worker', id: fork.threadId, // <--- undefined! }); });All test files share
id: undefined. The second test file overwrites the first inactiveTestWorkers.set(id, ...). When the first test file completes, its teardown executesderegister-test-workerwithid: undefined, terminating the second test file's worker connection mid-execution.
Proposed Fix
- Guard against attempting to use
registerSharedWorkerin child processes ifMessagePorttransfer is unsupported, providing an actionable error message. - Provide a stable fallback identifier (e.g.
worker.pidor an incrementing counter) whenworker.threadIdisundefined:
--- a/lib/fork.js
+++ b/lib/fork.js
@@ -155,7 +155,7 @@ export default function loadFork(file, options, execArgv = process.execArgv) {
return {
file,
- threadId: worker.threadId,
+ threadId: worker.threadId ?? `process-${worker.pid}`,
promise,
exit() {
--- a/lib/worker/channel.js
+++ b/lib/worker/channel.js
@@ -139,6 +139,10 @@ function createChannelEmitter(channelId) {
export function registerSharedWorker(filename, initialData) {
+ if (isRunningInChildProcess) {
+ throw new Error('Shared workers are only supported when running tests with `workerThreads: true`.');
+ }
+
const channelId = `${threadId}/channel/${++channelCounter}`;
const {port1: ourPort, port2: theirPort} = new MessageChannel();Source: avajs/ava