Files
node/test/parallel/test-webworker-postmessage-lifecycle.js
Filip Skokan e6b3f96cd0 worker: discard queued messages on termination
Close the outside port and remove its message forwarding listeners
synchronously. Closing the port alone is asynchronous and can leave
queued messages dispatching when terminate() runs inside a listener.

Allow the current event to finish while discarding subsequent messages,
including those drained when the backing thread exits.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66354
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
2026-09-29 17:19:32 +00:00

79 lines
2.9 KiB
JavaScript

// Flags: --experimental-web-worker
'use strict';
const common = require('../common');
const assert = require('node:assert');
function checkSerialization(worker) {
assert.throws(() => worker.postMessage(() => {}), { name: 'DataCloneError' });
const buffer = new ArrayBuffer(8);
assert.throws(() => worker.postMessage(null, [buffer, buffer]), { name: 'DataCloneError' });
assert.strictEqual(buffer.byteLength, 8);
worker.postMessage(null, [buffer]);
assert.strictEqual(buffer.byteLength, 0);
}
// A failed script fetch still leaves an outside port that serializes messages.
{
const worker = new Worker('data:text/plain,');
worker.onerror = common.mustCall(() => checkSerialization(worker));
checkSerialization(worker);
}
// Serialization is required even after the backing thread has exited.
{
const worker = new Worker('data:text/javascript,close()');
worker.onerror = common.mustNotCall('worker failed');
process.once('worker', common.mustCall((thread) => {
thread.once('exit', common.mustCall(() => checkSerialization(worker)));
}));
}
// Queue messages before terminating so the test does not depend on thread speed.
{
const source = `
onmessage = ({ data }) => {
for (let i = 0; i < 3; i++) postMessage(i);
const state = new Int32Array(data);
Atomics.store(state, 0, 1);
Atomics.notify(state, 0);
};
`;
const worker = new Worker(`data:text/javascript,${encodeURIComponent(source)}`);
worker.onerror = common.mustNotCall('worker failed');
worker.onmessage = common.mustNotCall('message delivered after terminate()');
const state = new Int32Array(new SharedArrayBuffer(4));
worker.postMessage(state.buffer);
assert.notStrictEqual(Atomics.wait(state, 0, 0, common.platformTimeout(10000)), 'timed-out');
worker.terminate();
worker.terminate();
checkSerialization(worker);
}
// terminate() can run during message dispatch, including while the backing
// thread is draining messages on exit. Finish this event but discard later ones.
for (const closeAfterPosting of [false, true]) {
const source = `
onmessage = ({ data }) => {
for (let i = 0; i < 3; i++) postMessage(i);
const state = new Int32Array(data);
Atomics.store(state, 0, 1);
Atomics.notify(state, 0);
if (${closeAfterPosting}) close();
};
`;
const worker = new Worker(`data:text/javascript,${encodeURIComponent(source)}`);
worker.onerror = common.mustNotCall('worker failed');
worker.addEventListener('message', common.mustCall(({ data }) => {
assert.strictEqual(data, 0);
worker.terminate();
checkSerialization(worker);
}));
worker.addEventListener('message', common.mustCall(({ data }) => {
assert.strictEqual(data, 0);
}));
const state = new Int32Array(new SharedArrayBuffer(4));
worker.postMessage(state.buffer);
assert.notStrictEqual(Atomics.wait(state, 0, 0, common.platformTimeout(10000)), 'timed-out');
}