From 26950651c37010f6c5809357e534169929258d85 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Sun, 23 Aug 2026 11:22:34 -0700 Subject: [PATCH 1/5] worker: add ref/unref to web workers Signed-off-by: avivkeller --- doc/api/globals.md | 5 ++++ lib/internal/webworker.js | 13 +++++++++ test/fixtures/web-worker/echo.js | 5 ++++ test/parallel/test-webworker-ref-unref.js | 34 +++++++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 test/fixtures/web-worker/echo.js create mode 100644 test/parallel/test-webworker-ref-unref.js diff --git a/doc/api/globals.md b/doc/api/globals.md index cd6afed9626f..fd3d509de3a4 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -1401,6 +1401,11 @@ Inside a worker, \[`worker_threads.parentPort`]\[] is the port behind `self.postMessage()` and the worker's `message` events, `isMainThread` is `false`, and `workerData` is `undefined`. +Because that thread keeps the event loop alive, `Worker` instances also get +the non-standard `ref()` and `unref()` methods of [`node:worker_threads`][] +{Worker}: `worker.unref()` allows the program to exit even while the worker +is still running, and `worker.ref()` restores the default behavior. + As a rule of thumb, use [`node:worker_threads`][] directly when a program needs `workerData`, a custom `env` or `execArgv`, resource limits, stdio redirection, the `'online'` and `'exit'` events, or `worker.threadId`; diff --git a/lib/internal/webworker.js b/lib/internal/webworker.js index bc7b55ed8ac1..51363201763a 100644 --- a/lib/internal/webworker.js +++ b/lib/internal/webworker.js @@ -865,11 +865,24 @@ class Worker extends EventTarget { // arguments, and returned the same return value." this[kWorker]?.postMessage(message, transfer); } + + // The following properties are non-standard, Node.js extensions + ref() { + validateThisInternalField(this, kWorker, 'Worker'); + this[kWorker]?.ref(); + } + + unref() { + validateThisInternalField(this, kWorker, 'Worker'); + this[kWorker]?.unref(); + } } ObjectDefineProperties(Worker.prototype, { terminate: kEnumerableProperty, postMessage: kEnumerableProperty, + ref: kEnumerableProperty, + unref: kEnumerableProperty, [SymbolToStringTag]: { __proto__: null, configurable: true, diff --git a/test/fixtures/web-worker/echo.js b/test/fixtures/web-worker/echo.js new file mode 100644 index 000000000000..753c32599775 --- /dev/null +++ b/test/fixtures/web-worker/echo.js @@ -0,0 +1,5 @@ +'use strict'; + +addEventListener('message', (event) => { + postMessage(event.data); +}); diff --git a/test/parallel/test-webworker-ref-unref.js b/test/parallel/test-webworker-ref-unref.js new file mode 100644 index 000000000000..7abbf1f69124 --- /dev/null +++ b/test/parallel/test-webworker-ref-unref.js @@ -0,0 +1,34 @@ +// Flags: --experimental-web-worker +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); + +for (const method of ['ref', 'unref']) { + assert.throws(() => Worker.prototype[method].call({}), + { code: 'ERR_INVALID_THIS' }); +} + +{ + // Both are no-ops on a worker whose script could not be loaded. + const worker = new Worker(fixtures.fileURL('web-worker', 'nonexistent.js').href); + worker.addEventListener('error', common.mustCall()); + worker.unref(); + worker.ref(); +} + +const worker = new Worker(fixtures.fileURL('web-worker', 'echo.js').href); + +worker.addEventListener('error', common.mustNotCall('worker failed')); +worker.addEventListener('message', common.mustCall(({ data }) => { + assert.strictEqual(data, 'hello'); + worker.terminate(); +})); + +process.once('beforeExit', common.mustCall(() => { + worker.ref(); + worker.postMessage('hello'); +})); + +worker.unref(); From f7c75813d9d5e224b65956ca47e113487e7cce39 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 24 Aug 2026 13:15:52 -0700 Subject: [PATCH 2/5] fixup! worker: add ref/unref to web workers --- lib/internal/webworker.js | 8 ++++---- test/parallel/test-webworker-ref-unref.js | 13 ++++--------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/internal/webworker.js b/lib/internal/webworker.js index 51363201763a..73b78c7f415e 100644 --- a/lib/internal/webworker.js +++ b/lib/internal/webworker.js @@ -99,6 +99,8 @@ const { const kCurrentlyReceivingPorts = SymbolFor('nodejs.internal.kCurrentlyReceivingPorts'); +const kRef = SymbolFor('nodejs.ref') +const kUnref = SymbolFor('nodejs.unref') const kCreate = Symbol('kCreate'); const kInsidePort = Symbol('kInsidePort'); @@ -867,12 +869,12 @@ class Worker extends EventTarget { } // The following properties are non-standard, Node.js extensions - ref() { + [kRef]() { validateThisInternalField(this, kWorker, 'Worker'); this[kWorker]?.ref(); } - unref() { + [kUnref]() { validateThisInternalField(this, kWorker, 'Worker'); this[kWorker]?.unref(); } @@ -881,8 +883,6 @@ class Worker extends EventTarget { ObjectDefineProperties(Worker.prototype, { terminate: kEnumerableProperty, postMessage: kEnumerableProperty, - ref: kEnumerableProperty, - unref: kEnumerableProperty, [SymbolToStringTag]: { __proto__: null, configurable: true, diff --git a/test/parallel/test-webworker-ref-unref.js b/test/parallel/test-webworker-ref-unref.js index 7abbf1f69124..cff2bb5d097f 100644 --- a/test/parallel/test-webworker-ref-unref.js +++ b/test/parallel/test-webworker-ref-unref.js @@ -5,17 +5,12 @@ const common = require('../common'); const fixtures = require('../common/fixtures'); const assert = require('assert'); -for (const method of ['ref', 'unref']) { - assert.throws(() => Worker.prototype[method].call({}), - { code: 'ERR_INVALID_THIS' }); -} - { // Both are no-ops on a worker whose script could not be loaded. const worker = new Worker(fixtures.fileURL('web-worker', 'nonexistent.js').href); worker.addEventListener('error', common.mustCall()); - worker.unref(); - worker.ref(); + process.unref(worker); + process.ref(worker); } const worker = new Worker(fixtures.fileURL('web-worker', 'echo.js').href); @@ -27,8 +22,8 @@ worker.addEventListener('message', common.mustCall(({ data }) => { })); process.once('beforeExit', common.mustCall(() => { - worker.ref(); + process.ref(worker); worker.postMessage('hello'); })); -worker.unref(); +process.unref(worker); From e8a5577694ef2c4468a40ade0b30fa3b9c544d36 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Mon, 24 Aug 2026 13:16:50 -0700 Subject: [PATCH 3/5] fixup! worker: add ref/unref to web workers --- doc/api/globals.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/api/globals.md b/doc/api/globals.md index fd3d509de3a4..f7dfc4c59b4d 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -1401,10 +1401,9 @@ Inside a worker, \[`worker_threads.parentPort`]\[] is the port behind `self.postMessage()` and the worker's `message` events, `isMainThread` is `false`, and `workerData` is `undefined`. -Because that thread keeps the event loop alive, `Worker` instances also get -the non-standard `ref()` and `unref()` methods of [`node:worker_threads`][] -{Worker}: `worker.unref()` allows the program to exit even while the worker -is still running, and `worker.ref()` restores the default behavior. +Because that thread keeps the event loop alive, `Worker` instances can be +mutated using the non-standard `process.ref(worker)` and `process.unref(worker)` +methods. As a rule of thumb, use [`node:worker_threads`][] directly when a program needs `workerData`, a custom `env` or `execArgv`, resource limits, stdio From f5ec2047b37034d6e710298020abe833b822bcb6 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Mon, 24 Aug 2026 13:25:42 -0700 Subject: [PATCH 4/5] fix semicolons of kRef and kUnref (from a plane) --- lib/internal/webworker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/webworker.js b/lib/internal/webworker.js index 73b78c7f415e..e2a2086f9039 100644 --- a/lib/internal/webworker.js +++ b/lib/internal/webworker.js @@ -99,8 +99,8 @@ const { const kCurrentlyReceivingPorts = SymbolFor('nodejs.internal.kCurrentlyReceivingPorts'); -const kRef = SymbolFor('nodejs.ref') -const kUnref = SymbolFor('nodejs.unref') +const kRef = SymbolFor('nodejs.ref'); +const kUnref = SymbolFor('nodejs.unref'); const kCreate = Symbol('kCreate'); const kInsidePort = Symbol('kInsidePort'); From 5b894fdc5591cb5273dcefcba826f67332440196 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Tue, 25 Aug 2026 12:54:39 -0400 Subject: [PATCH 5/5] fixup! --- doc/api/globals.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/api/globals.md b/doc/api/globals.md index f7dfc4c59b4d..3be01a4acb3e 100644 --- a/doc/api/globals.md +++ b/doc/api/globals.md @@ -1401,9 +1401,9 @@ Inside a worker, \[`worker_threads.parentPort`]\[] is the port behind `self.postMessage()` and the worker's `message` events, `isMainThread` is `false`, and `workerData` is `undefined`. -Because that thread keeps the event loop alive, `Worker` instances can be -mutated using the non-standard `process.ref(worker)` and `process.unref(worker)` -methods. +Web Workers, like `node:worker_threads` workers, keep the event loop alive by +default. In Node.js, Web Workers implement the [Refable protocol][], and can be +ref'd and unref'd using `process.ref(worker)` and `process.unref(worker)`. As a rule of thumb, use [`node:worker_threads`][] directly when a program needs `workerData`, a custom `env` or `execArgv`, resource limits, stdio @@ -1460,6 +1460,7 @@ A browser-compatible implementation of [`WritableStreamDefaultWriter`][]. [ECMAScript module]: esm.md [HTML Standard]: https://html.spec.whatwg.org/multipage/workers.html [Navigator API]: https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object +[Refable protocol]: process.md#processrefmayberefable [RFC 5646]: https://www.rfc-editor.org/rfc/rfc5646.txt [Web Crypto API]: webcrypto.md [`--experimental-eventsource`]: cli.md#--experimental-eventsource