#3481·ava

(Critical Architecture Bug): Shared Worker Plugin Crash & Test Worker Collision when `workerThreads: false`

Author: codeCraft-RitikCreated Sep 15, 2026Updated Sep 15, 2026
Labelsneeds triage

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:

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:

  1. lib/worker/channel.js sends port: theirPort (MessagePort) over handle.send(). In a child process, handle uses IpcHandle which calls Node's process.send() with serialization: 'advanced'. In Node.js, v8.serialize({ port: new MessageChannel().port1 }) throws a fatal TypeError [ERR_INVALID_STATE]: Unserializable host object: MessagePort because MessagePort cannot be cloned across OS processes.
  2. In lib/fork.js, threadId: worker.threadId is used as the unique test worker identifier. For child processes, worker.threadId is undefined. Consequently, all concurrent test files register with id: undefined in shared-worker-loader.js. When any single test file finishes and deregisters id: undefined, it prematurely shuts down the shared worker for all running test files.

Root Cause Analysis

  1. Unserializable MessagePort over Process IPC: In lib/worker/channel.js lines 142–153:

    javascript
    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 isRunningInChildProcess is true:

    javascript
    handle = new IpcHandle(controlFlow(process));

    IpcHandle.send calls this.sendRaw({ava: evt}) without transfer lists. Passing port: theirPort into Node's process.send() triggers V8 serialization:

    javascript
    // 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: MessagePort
  2. Undefined Worker Identifier: In lib/fork.js lines 156–160:

    javascript
    return {
        file,
        threadId: worker.threadId,
        promise,

    For childProcess.fork(), worker.threadId is undefined. In lib/plugin-support/shared-workers.js lines 114–126:

    javascript
    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 in activeTestWorkers.set(id, ...). When the first test file completes, its teardown executes deregister-test-worker with id: undefined, terminating the second test file's worker connection mid-execution.

Proposed Fix

  1. Guard against attempting to use registerSharedWorker in child processes if MessagePort transfer is unsupported, providing an actionable error message.
  2. Provide a stable fallback identifier (e.g. worker.pid or an incrementing counter) when worker.threadId is undefined:
diff
--- 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();