Skip to content

Commit 73b5c16

Browse files
ShogunPandamarco-ippolito
authored andcommittedFeb 20, 2025··
worker: add postMessageToThread
PR-URL: #53682 Backport-PR-URL: #57101 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
1 parent 9e975f1 commit 73b5c16

14 files changed

+685
-5
lines changed
 

‎doc/api/errors.md

+49
Original file line numberDiff line numberDiff line change
@@ -3213,6 +3213,54 @@ The `Worker` instance terminated because it reached its memory limit.
32133213
The path for the main script of a worker is neither an absolute path
32143214
nor a relative path starting with `./` or `../`.
32153215

3216+
<a id="ERR_WORKER_MESSAGING_ERRORED"></a>
3217+
3218+
### `ERR_WORKER_MESSAGING_ERRORED`
3219+
3220+
<!-- YAML
3221+
added: REPLACEME
3222+
-->
3223+
3224+
> Stability: 1.1 - Active development
3225+
3226+
The destination thread threw an error while processing a message sent via [`postMessageToThread()`][].
3227+
3228+
<a id="ERR_WORKER_MESSAGING_FAILED"></a>
3229+
3230+
### `ERR_WORKER_MESSAGING_FAILED`
3231+
3232+
<!-- YAML
3233+
added: REPLACEME
3234+
-->
3235+
3236+
> Stability: 1.1 - Active development
3237+
3238+
The thread requested in [`postMessageToThread()`][] is invalid or has no `workerMessage` listener.
3239+
3240+
<a id="ERR_WORKER_MESSAGING_SAME_THREAD"></a>
3241+
3242+
### `ERR_WORKER_MESSAGING_SAME_THREAD`
3243+
3244+
<!-- YAML
3245+
added: REPLACEME
3246+
-->
3247+
3248+
> Stability: 1.1 - Active development
3249+
3250+
The thread id requested in [`postMessageToThread()`][] is the current thread id.
3251+
3252+
<a id="ERR_WORKER_MESSAGING_TIMEOUT"></a>
3253+
3254+
### `ERR_WORKER_MESSAGING_TIMEOUT`
3255+
3256+
<!-- YAML
3257+
added: REPLACEME
3258+
-->
3259+
3260+
> Stability: 1.1 - Active development
3261+
3262+
Sending a message via [`postMessageToThread()`][] timed out.
3263+
32163264
<a id="ERR_WORKER_UNSERIALIZABLE_ERROR"></a>
32173265

32183266
### `ERR_WORKER_UNSERIALIZABLE_ERROR`
@@ -3954,6 +4002,7 @@ An error occurred trying to allocate memory. This should never happen.
39544002
[`new URLSearchParams(iterable)`]: url.md#new-urlsearchparamsiterable
39554003
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
39564004
[`postMessage()`]: worker_threads.md#portpostmessagevalue-transferlist
4005+
[`postMessageToThread()`]: worker_threads.md#workerpostmessagetothreadthreadid-value-transferlist-timeout
39574006
[`process.on('exit')`]: process.md#event-exit
39584007
[`process.send()`]: process.md#processsendmessage-sendhandle-options-callback
39594008
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn

‎doc/api/process.md

+13
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,18 @@ possible to record such errors in an error log, either periodically (which is
327327
likely best for long-running application) or upon process exit (which is likely
328328
most convenient for scripts).
329329

330+
### Event: `'workerMessage'`
331+
332+
<!-- YAML
333+
added: REPLACEME
334+
-->
335+
336+
* `value` {any} A value transmitted using [`postMessageToThread()`][].
337+
* `source` {number} The transmitting worker thread ID or `0` for the main thread.
338+
339+
The `'workerMessage'` event is emitted for any incoming message send by the other
340+
party by using [`postMessageToThread()`][].
341+
330342
### Event: `'uncaughtException'`
331343

332344
<!-- YAML
@@ -4173,6 +4185,7 @@ cases:
41734185
[`net.Server`]: net.md#class-netserver
41744186
[`net.Socket`]: net.md#class-netsocket
41754187
[`os.constants.dlopen`]: os.md#dlopen-constants
4188+
[`postMessageToThread()`]: worker_threads.md#workerpostmessagetothreadthreadid-value-transferlist-timeout
41764189
[`process.argv`]: #processargv
41774190
[`process.config`]: #processconfig
41784191
[`process.execPath`]: #processexecpath

‎doc/api/worker_threads.md

+116
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,118 @@ if (isMainThread) {
220220
}
221221
```
222222

223+
## `worker.postMessageToThread(threadId, value[, transferList][, timeout])`
224+
225+
<!-- YAML
226+
added: REPLACEME
227+
-->
228+
229+
> Stability: 1.1 - Active development
230+
231+
* `destination` {number} The target thread ID. If the thread ID is invalid, a
232+
[`ERR_WORKER_MESSAGING_FAILED`][] error will be thrown. If the target thread ID is the current thread ID,
233+
a [`ERR_WORKER_MESSAGING_SAME_THREAD`][] error will be thrown.
234+
* `value` {any} The value to send.
235+
* `transferList` {Object\[]} If one or more `MessagePort`-like objects are passed in `value`,
236+
a `transferList` is required for those items or [`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`][] is thrown.
237+
See [`port.postMessage()`][] for more information.
238+
* `timeout` {number} Time to wait for the message to be delivered in milliseconds.
239+
By default it's `undefined`, which means wait forever. If the operation times out,
240+
a [`ERR_WORKER_MESSAGING_TIMEOUT`][] error is thrown.
241+
* Returns: {Promise} A promise which is fulfilled if the message was successfully processed by destination thread.
242+
243+
Sends a value to another worker, identified by its thread ID.
244+
245+
If the target thread has no listener for the `workerMessage` event, then the operation will throw
246+
a [`ERR_WORKER_MESSAGING_FAILED`][] error.
247+
248+
If the target thread threw an error while processing the `workerMessage` event, then the operation will throw
249+
a [`ERR_WORKER_MESSAGING_ERRORED`][] error.
250+
251+
This method should be used when the target thread is not the direct
252+
parent or child of the current thread.
253+
If the two threads are parent-children, use the [`require('node:worker_threads').parentPort.postMessage()`][]
254+
and the [`worker.postMessage()`][] to let the threads communicate.
255+
256+
The example below shows the use of of `postMessageToThread`: it creates 10 nested threads,
257+
the last one will try to communicate with the main thread.
258+
259+
```mjs
260+
import { fileURLToPath } from 'node:url';
261+
import { once } from 'node:events';
262+
import process from 'node:process';
263+
import {
264+
isMainThread,
265+
postMessageToThread,
266+
threadId,
267+
workerData,
268+
Worker,
269+
} from 'node:worker_threads';
270+
271+
const channel = new BroadcastChannel('sync');
272+
const level = workerData?.level ?? 0;
273+
274+
if (level < 10) {
275+
const worker = new Worker(fileURLToPath(import.meta.url), {
276+
workerData: { level: level + 1 },
277+
});
278+
}
279+
280+
if (level === 0) {
281+
process.on('workerMessage', (value, source) => {
282+
console.log(`${source} -> ${threadId}:`, value);
283+
postMessageToThread(source, { message: 'pong' });
284+
});
285+
} else if (level === 10) {
286+
process.on('workerMessage', (value, source) => {
287+
console.log(`${source} -> ${threadId}:`, value);
288+
channel.postMessage('done');
289+
channel.close();
290+
});
291+
292+
await postMessageToThread(0, { message: 'ping' });
293+
}
294+
295+
channel.onmessage = channel.close;
296+
```
297+
298+
```cjs
299+
const { once } = require('node:events');
300+
const {
301+
isMainThread,
302+
postMessageToThread,
303+
threadId,
304+
workerData,
305+
Worker,
306+
} = require('node:worker_threads');
307+
308+
const channel = new BroadcastChannel('sync');
309+
const level = workerData?.level ?? 0;
310+
311+
if (level < 10) {
312+
const worker = new Worker(__filename, {
313+
workerData: { level: level + 1 },
314+
});
315+
}
316+
317+
if (level === 0) {
318+
process.on('workerMessage', (value, source) => {
319+
console.log(`${source} -> ${threadId}:`, value);
320+
postMessageToThread(source, { message: 'pong' });
321+
});
322+
} else if (level === 10) {
323+
process.on('workerMessage', (value, source) => {
324+
console.log(`${source} -> ${threadId}:`, value);
325+
channel.postMessage('done');
326+
channel.close();
327+
});
328+
329+
postMessageToThread(0, { message: 'ping' });
330+
}
331+
332+
channel.onmessage = channel.close;
333+
```
334+
223335
## `worker.receiveMessageOnPort(port)`
224336
225337
<!-- YAML
@@ -1361,6 +1473,10 @@ thread spawned will spawn another until the application crashes.
13611473
[`Buffer.allocUnsafe()`]: buffer.md#static-method-bufferallocunsafesize
13621474
[`Buffer`]: buffer.md
13631475
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: errors.md#err_missing_message_port_in_transfer_list
1476+
[`ERR_WORKER_MESSAGING_ERRORED`]: errors.md#err_worker_messaging_errored
1477+
[`ERR_WORKER_MESSAGING_FAILED`]: errors.md#err_worker_messaging_failed
1478+
[`ERR_WORKER_MESSAGING_SAME_THREAD`]: errors.md#err_worker_messaging_same_thread
1479+
[`ERR_WORKER_MESSAGING_TIMEOUT`]: errors.md#err_worker_messaging_timeout
13641480
[`ERR_WORKER_NOT_RUNNING`]: errors.md#err_worker_not_running
13651481
[`EventTarget`]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
13661482
[`FileHandle`]: fs.md#class-filehandle

‎lib/internal/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -1911,6 +1911,10 @@ E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
19111911
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
19121912
`Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`,
19131913
Error);
1914+
E('ERR_WORKER_MESSAGING_ERRORED', 'The destination thread threw an error while processing the message', Error);
1915+
E('ERR_WORKER_MESSAGING_FAILED', 'Cannot find the destination thread or listener', Error);
1916+
E('ERR_WORKER_MESSAGING_SAME_THREAD', 'Cannot sent a message to the same thread', Error);
1917+
E('ERR_WORKER_MESSAGING_TIMEOUT', 'Sending a message to another thread timed out', Error);
19141918
E('ERR_WORKER_NOT_RUNNING', 'Worker instance not running', Error);
19151919
E('ERR_WORKER_OUT_OF_MEMORY',
19161920
'Worker terminated due to reaching memory limit: %s', Error);

‎lib/internal/main/worker_thread.js

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ const {
4444
kStdioWantsMoreDataCallback,
4545
} = workerIo;
4646

47+
const { setupMainThreadPort } = require('internal/worker/messaging');
48+
4749
const {
4850
onGlobalUncaughtException,
4951
} = require('internal/process/execution');
@@ -99,6 +101,7 @@ port.on('message', (message) => {
99101
manifestURL,
100102
publicPort,
101103
workerData,
104+
mainThreadPort,
102105
} = message;
103106

104107
if (doEval !== 'internal') {
@@ -112,6 +115,7 @@ port.on('message', (message) => {
112115
}
113116

114117
require('internal/worker').assignEnvironmentData(environmentData);
118+
setupMainThreadPort(mainThreadPort);
115119

116120
if (SharedArrayBuffer !== undefined && Atomics !== undefined) {
117121
// The counter is only passed to the workers created by the main thread,

‎lib/internal/worker.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const {
5656
ReadableWorkerStdio,
5757
WritableWorkerStdio,
5858
} = workerIo;
59+
const { createMainThreadPort, destroyMainThreadPort } = require('internal/worker/messaging');
5960
const { deserializeError } = require('internal/error_serdes');
6061
const { fileURLToPath, isURL, pathToFileURL } = require('internal/url');
6162
const { kEmptyObject } = require('internal/util');
@@ -251,14 +252,18 @@ class Worker extends EventEmitter {
251252

252253
this[kParentSideStdio] = { stdin, stdout, stderr };
253254

254-
const { port1, port2 } = new MessageChannel();
255-
const transferList = [port2];
255+
const mainThreadPortToWorker = createMainThreadPort(this.threadId);
256+
const {
257+
port1: publicPortToParent,
258+
port2: publicPortToWorker,
259+
} = new MessageChannel();
260+
const transferList = [mainThreadPortToWorker, publicPortToWorker];
256261
// If transferList is provided.
257262
if (options.transferList)
258263
ArrayPrototypePush(transferList,
259264
...new SafeArrayIterator(options.transferList));
260265

261-
this[kPublicPort] = port1;
266+
this[kPublicPort] = publicPortToParent;
262267
ArrayPrototypeForEach(['message', 'messageerror'], (event) => {
263268
this[kPublicPort].on(event, (message) => this.emit(event, message));
264269
});
@@ -272,14 +277,15 @@ class Worker extends EventEmitter {
272277
cwdCounter: cwdCounter || workerIo.sharedCwdCounter,
273278
workerData: options.workerData,
274279
environmentData,
275-
publicPort: port2,
276280
manifestURL: getOptionValue('--experimental-policy') ?
277281
require('internal/process/policy').url :
278282
null,
279283
manifestSrc: getOptionValue('--experimental-policy') ?
280284
require('internal/process/policy').src :
281285
null,
282286
hasStdin: !!options.stdin,
287+
publicPort: publicPortToWorker,
288+
mainThreadPort: mainThreadPortToWorker,
283289
}, transferList);
284290
// Use this to cache the Worker's loopStart value once available.
285291
this[kLoopStartTime] = -1;
@@ -302,6 +308,7 @@ class Worker extends EventEmitter {
302308
debug(`[${threadId}] hears end event for Worker ${this.threadId}`);
303309
drainMessagePort(this[kPublicPort]);
304310
drainMessagePort(this[kPort]);
311+
destroyMainThreadPort(this.threadId);
305312
this.removeAllListeners('message');
306313
this.removeAllListeners('messageerrors');
307314
this[kPublicPort].unref();

‎lib/internal/worker/messaging.js

+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
'use strict';
2+
3+
const {
4+
Int32Array,
5+
SafeMap,
6+
globalThis: {
7+
Atomics,
8+
SharedArrayBuffer,
9+
},
10+
} = primordials;
11+
12+
const {
13+
isMainThread,
14+
threadId: currentThreadId,
15+
} = internalBinding('worker');
16+
17+
const {
18+
codes: {
19+
ERR_WORKER_MESSAGING_ERRORED,
20+
ERR_WORKER_MESSAGING_FAILED,
21+
ERR_WORKER_MESSAGING_SAME_THREAD,
22+
ERR_WORKER_MESSAGING_TIMEOUT,
23+
},
24+
} = require('internal/errors');
25+
26+
const { MessageChannel } = require('internal/worker/io');
27+
28+
const { validateNumber } = require('internal/validators');
29+
30+
const messageTypes = {
31+
REGISTER_MAIN_THREAD_PORT: 'registerMainThreadPort',
32+
UNREGISTER_MAIN_THREAD_PORT: 'unregisterMainThreadPort',
33+
SEND_MESSAGE_TO_WORKER: 'sendMessageToWorker',
34+
RECEIVE_MESSAGE_FROM_WORKER: 'receiveMessageFromWorker',
35+
};
36+
37+
// This is only populated by main thread and always empty in other threads
38+
const threadsPorts = new SafeMap();
39+
40+
// This is only populated in child threads and always undefined in main thread
41+
let mainThreadPort;
42+
43+
// SharedArrayBuffer must always be Int32, so it's * 4.
44+
// We need one for the operation status (performing / performed) and one for the result (success / failure).
45+
const WORKER_MESSAGING_SHARED_DATA = 2 * 4;
46+
const WORKER_MESSAGING_STATUS_INDEX = 0;
47+
const WORKER_MESSAGING_RESULT_INDEX = 1;
48+
49+
// Response codes
50+
const WORKER_MESSAGING_RESULT_DELIVERED = 0;
51+
const WORKER_MESSAGING_RESULT_NO_LISTENERS = 1;
52+
const WORKER_MESSAGING_RESULT_LISTENER_ERROR = 2;
53+
54+
// This event handler is always executed on the main thread only
55+
function handleMessageFromThread(message) {
56+
switch (message.type) {
57+
case messageTypes.REGISTER_MAIN_THREAD_PORT:
58+
{
59+
const { threadId, port } = message;
60+
61+
// Register the port
62+
threadsPorts.set(threadId, port);
63+
64+
// Handle messages on this port
65+
// When a new thread wants to register a children
66+
// this take care of doing that.
67+
// This way any thread can be linked to the main one.
68+
port.on('message', handleMessageFromThread);
69+
70+
// Never block the thread on this port
71+
port.unref();
72+
}
73+
74+
break;
75+
case messageTypes.UNREGISTER_MAIN_THREAD_PORT:
76+
threadsPorts.get(message.threadId).close();
77+
threadsPorts.delete(message.threadId);
78+
break;
79+
case messageTypes.SEND_MESSAGE_TO_WORKER:
80+
{
81+
// Send the message to the target thread
82+
const { source, destination, value, transferList, memory } = message;
83+
sendMessageToWorker(source, destination, value, transferList, memory);
84+
}
85+
break;
86+
}
87+
}
88+
89+
function handleMessageFromMainThread(message) {
90+
switch (message.type) {
91+
case messageTypes.RECEIVE_MESSAGE_FROM_WORKER:
92+
receiveMessageFromWorker(message.source, message.value, message.memory);
93+
break;
94+
}
95+
}
96+
97+
function sendMessageToWorker(source, destination, value, transferList, memory) {
98+
if (SharedArrayBuffer === undefined || Atomics === undefined) {
99+
return;
100+
}
101+
102+
// We are on the main thread, we can directly process the message
103+
if (destination === 0) {
104+
receiveMessageFromWorker(source, value, memory);
105+
return;
106+
}
107+
108+
// Search the port to the target thread
109+
const port = threadsPorts.get(destination);
110+
111+
if (!port) {
112+
const status = new Int32Array(memory);
113+
Atomics.store(status, WORKER_MESSAGING_RESULT_INDEX, WORKER_MESSAGING_RESULT_NO_LISTENERS);
114+
Atomics.store(status, WORKER_MESSAGING_STATUS_INDEX, 1);
115+
Atomics.notify(status, WORKER_MESSAGING_STATUS_INDEX, 1);
116+
return;
117+
}
118+
119+
port.postMessage(
120+
{
121+
type: messageTypes.RECEIVE_MESSAGE_FROM_WORKER,
122+
source,
123+
destination,
124+
value,
125+
memory,
126+
},
127+
transferList,
128+
);
129+
}
130+
131+
function receiveMessageFromWorker(source, value, memory) {
132+
if (SharedArrayBuffer === undefined || Atomics === undefined) {
133+
return;
134+
}
135+
136+
let response = WORKER_MESSAGING_RESULT_NO_LISTENERS;
137+
138+
try {
139+
if (process.emit('workerMessage', value, source)) {
140+
response = WORKER_MESSAGING_RESULT_DELIVERED;
141+
}
142+
} catch {
143+
response = WORKER_MESSAGING_RESULT_LISTENER_ERROR;
144+
}
145+
146+
// Populate the result
147+
const status = new Int32Array(memory);
148+
Atomics.store(status, WORKER_MESSAGING_RESULT_INDEX, response);
149+
Atomics.store(status, WORKER_MESSAGING_STATUS_INDEX, 1);
150+
Atomics.notify(status, WORKER_MESSAGING_STATUS_INDEX, 1);
151+
}
152+
153+
function createMainThreadPort(threadId) {
154+
// Create a channel that links the new thread to the main thread
155+
const {
156+
port1: mainThreadPortToMain,
157+
port2: mainThreadPortToThread,
158+
} = new MessageChannel();
159+
160+
const registrationMessage = {
161+
type: messageTypes.REGISTER_MAIN_THREAD_PORT,
162+
threadId,
163+
port: mainThreadPortToMain,
164+
};
165+
166+
if (isMainThread) {
167+
handleMessageFromThread(registrationMessage);
168+
} else {
169+
mainThreadPort.postMessage(registrationMessage, [mainThreadPortToMain]);
170+
}
171+
172+
return mainThreadPortToThread;
173+
}
174+
175+
function destroyMainThreadPort(threadId) {
176+
const unregistrationMessage = {
177+
type: messageTypes.UNREGISTER_MAIN_THREAD_PORT,
178+
threadId,
179+
};
180+
181+
if (isMainThread) {
182+
handleMessageFromThread(unregistrationMessage);
183+
} else {
184+
mainThreadPort.postMessage(unregistrationMessage);
185+
}
186+
}
187+
188+
function setupMainThreadPort(port) {
189+
mainThreadPort = port;
190+
mainThreadPort.on('message', handleMessageFromMainThread);
191+
192+
// Never block the process on this port
193+
mainThreadPort.unref();
194+
}
195+
196+
async function postMessageToThread(threadId, value, transferList, timeout) {
197+
if (SharedArrayBuffer === undefined || Atomics === undefined) {
198+
return;
199+
}
200+
201+
if (typeof transferList === 'number' && typeof timeout === 'undefined') {
202+
timeout = transferList;
203+
transferList = [];
204+
}
205+
206+
if (typeof timeout !== 'undefined') {
207+
validateNumber(timeout, 'timeout', 0);
208+
}
209+
210+
if (threadId === currentThreadId) {
211+
throw new ERR_WORKER_MESSAGING_SAME_THREAD();
212+
}
213+
214+
const memory = new SharedArrayBuffer(WORKER_MESSAGING_SHARED_DATA);
215+
const status = new Int32Array(memory);
216+
const promise = Atomics.waitAsync(status, WORKER_MESSAGING_STATUS_INDEX, 0, timeout).value;
217+
218+
const message = {
219+
type: messageTypes.SEND_MESSAGE_TO_WORKER,
220+
source: currentThreadId,
221+
destination: threadId,
222+
value,
223+
memory,
224+
transferList,
225+
};
226+
227+
if (isMainThread) {
228+
handleMessageFromThread(message);
229+
} else {
230+
mainThreadPort.postMessage(message, transferList);
231+
}
232+
233+
// Wait for the response
234+
const response = await promise;
235+
236+
if (response === 'timed-out') {
237+
throw new ERR_WORKER_MESSAGING_TIMEOUT();
238+
} else if (status[WORKER_MESSAGING_RESULT_INDEX] === WORKER_MESSAGING_RESULT_NO_LISTENERS) {
239+
throw new ERR_WORKER_MESSAGING_FAILED();
240+
} else if (status[WORKER_MESSAGING_RESULT_INDEX] === WORKER_MESSAGING_RESULT_LISTENER_ERROR) {
241+
throw new ERR_WORKER_MESSAGING_ERRORED();
242+
}
243+
}
244+
245+
module.exports = {
246+
createMainThreadPort,
247+
destroyMainThreadPort,
248+
setupMainThreadPort,
249+
postMessageToThread,
250+
};

‎lib/worker_threads.js

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const {
1818
BroadcastChannel,
1919
} = require('internal/worker/io');
2020

21+
const {
22+
postMessageToThread,
23+
} = require('internal/worker/messaging');
24+
2125
const {
2226
markAsUntransferable,
2327
} = require('internal/buffer');
@@ -30,6 +34,7 @@ module.exports = {
3034
moveMessagePortToContext,
3135
receiveMessageOnPort,
3236
resourceLimits,
37+
postMessageToThread,
3338
threadId,
3439
SHARE_ENV,
3540
Worker,

‎test/parallel/test-bootstrap-modules.js

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ if (common.isMainThread) {
140140
'NativeModule internal/streams/writable',
141141
'NativeModule internal/worker',
142142
'NativeModule internal/worker/io',
143+
'NativeModule internal/worker/messaging',
143144
'NativeModule stream',
144145
'NativeModule stream/promises',
145146
'NativeModule string_decoder',

‎test/parallel/test-http-multiple-headers.js

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict';
22

3-
// TODO@PI: Run all tests
43
const common = require('../common');
54
const assert = require('assert');
65
const { createServer, request } = require('http');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const {
5+
parentPort,
6+
postMessageToThread,
7+
Worker,
8+
workerData,
9+
} = require('node:worker_threads');
10+
const { rejects } = require('node:assert');
11+
12+
async function test() {
13+
const worker = new Worker(__filename, { workerData: { children: true } });
14+
15+
await rejects(common.mustCall(function() {
16+
return postMessageToThread(worker.threadId);
17+
}), {
18+
name: 'Error',
19+
code: 'ERR_WORKER_MESSAGING_ERRORED',
20+
});
21+
22+
worker.postMessage('success');
23+
}
24+
25+
if (!workerData?.children) {
26+
test();
27+
} else {
28+
process.on('workerMessage', () => {
29+
throw new Error('KABOOM');
30+
});
31+
32+
parentPort.postMessage('ready');
33+
parentPort.once('message', common.mustCall());
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { once } = require('node:events');
5+
const {
6+
parentPort,
7+
postMessageToThread,
8+
threadId,
9+
Worker,
10+
workerData,
11+
} = require('node:worker_threads');
12+
const { rejects } = require('node:assert');
13+
14+
async function test() {
15+
await rejects(common.mustCall(function() {
16+
return postMessageToThread(threadId);
17+
}), {
18+
name: 'Error',
19+
code: 'ERR_WORKER_MESSAGING_SAME_THREAD',
20+
});
21+
22+
await rejects(common.mustCall(function() {
23+
return postMessageToThread(Date.now());
24+
}), {
25+
name: 'Error',
26+
code: 'ERR_WORKER_MESSAGING_FAILED',
27+
});
28+
29+
// The delivery to the first worker will fail as there is no listener for `workerMessage`
30+
const worker = new Worker(__filename, { workerData: { children: true } });
31+
await once(worker, 'message');
32+
33+
await rejects(common.mustCall(function() {
34+
return postMessageToThread(worker.threadId);
35+
}), {
36+
name: 'Error',
37+
code: 'ERR_WORKER_MESSAGING_FAILED',
38+
});
39+
40+
worker.postMessage('success');
41+
}
42+
43+
if (!workerData?.children) {
44+
test();
45+
} else {
46+
parentPort.postMessage('ready');
47+
parentPort.once('message', common.mustCall());
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const {
5+
postMessageToThread,
6+
workerData,
7+
Worker,
8+
} = require('node:worker_threads');
9+
const { rejects } = require('node:assert');
10+
11+
const memory = new SharedArrayBuffer(4);
12+
13+
async function test() {
14+
const worker = new Worker(__filename, { workerData: { memory, children: true } });
15+
const array = new Int32Array(memory);
16+
17+
await rejects(common.mustCall(function() {
18+
return postMessageToThread(worker.threadId, 0, common.platformTimeout(500));
19+
}), {
20+
name: 'Error',
21+
code: 'ERR_WORKER_MESSAGING_TIMEOUT',
22+
});
23+
24+
Atomics.store(array, 0, 1);
25+
Atomics.notify(array, 0);
26+
}
27+
28+
if (!workerData?.children) {
29+
test();
30+
} else {
31+
process.on('beforeExit', common.mustCall());
32+
33+
const array = new Int32Array(workerData.memory);
34+
35+
// Starve this thread waiting for the status to be unlocked.
36+
// This happens in the main thread AFTER the timeout.
37+
Atomics.wait(array, 0, 0);
38+
}
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const Countdown = require('../common/countdown');
5+
const {
6+
parentPort,
7+
postMessageToThread,
8+
threadId,
9+
workerData,
10+
Worker,
11+
} = require('node:worker_threads');
12+
const { strictEqual, deepStrictEqual } = require('node:assert');
13+
const { once } = require('node:events');
14+
15+
// Spawn threads on three levels: 1 main thread, two children, four grand childrens. 7 threads total, max id = 6
16+
const MAX_LEVEL = 2;
17+
const MAX_THREAD = 6;
18+
19+
// This is to allow the test to run in --worker mode
20+
const mainThread = workerData?.mainThread ?? threadId;
21+
const level = workerData?.level ?? 0;
22+
23+
const channel = new BroadcastChannel('nodejs:test-worker-connection');
24+
let completed;
25+
26+
if (level === 0) {
27+
completed = new Countdown(MAX_THREAD + 1, () => {
28+
channel.postMessage('exit');
29+
channel.close();
30+
});
31+
}
32+
33+
async function createChildren() {
34+
const worker = new Worker(__filename, { workerData: { mainThread, level: level + 1 } });
35+
await once(worker, 'message');
36+
}
37+
38+
async function ping() {
39+
let target;
40+
do {
41+
target = mainThread + Math.floor(Math.random() * MAX_THREAD);
42+
} while (target === threadId);
43+
44+
const { port1, port2 } = new MessageChannel();
45+
await postMessageToThread(target, { level, port: port2 }, [port2]);
46+
47+
port1.on('message', common.mustCall(function(message) {
48+
deepStrictEqual(message, { message: 'pong', source: target, destination: threadId });
49+
port1.close();
50+
51+
if (level === 0) {
52+
completed.dec();
53+
} else {
54+
channel.postMessage('end');
55+
}
56+
}));
57+
58+
port1.postMessage({ message: 'ping', source: threadId, destination: target });
59+
}
60+
61+
// Do not use mustCall here as the thread might not receive any connection request
62+
process.on('workerMessage', ({ port, level }, source) => {
63+
// Let's verify the source hierarchy
64+
// Given we do depth first, the level is 1 for thread 1 and 4, 2 for other threads
65+
if (source !== mainThread) {
66+
const currentThread = source - mainThread;
67+
strictEqual(level, (currentThread === 1 || currentThread === 4) ? 1 : 2);
68+
} else {
69+
strictEqual(level, 0);
70+
}
71+
72+
// Verify communication
73+
port.on('message', common.mustCall(function(message) {
74+
deepStrictEqual(message, { message: 'ping', source, destination: threadId });
75+
port.postMessage({ message: 'pong', source: threadId, destination: source });
76+
port.close();
77+
}));
78+
});
79+
80+
async function test() {
81+
if (level < MAX_LEVEL) {
82+
await createChildren();
83+
await createChildren();
84+
}
85+
86+
channel.onmessage = function(message) {
87+
switch (message.data) {
88+
case 'start':
89+
ping();
90+
break;
91+
case 'end':
92+
if (level === 0) {
93+
completed.dec();
94+
}
95+
break;
96+
case 'exit':
97+
channel.close();
98+
break;
99+
}
100+
};
101+
102+
if (level > 0) {
103+
const currentThread = threadId - mainThread;
104+
strictEqual(level, (currentThread === 1 || currentThread === 4) ? 1 : 2);
105+
parentPort.postMessage({ type: 'ready', threadId });
106+
} else {
107+
channel.postMessage('start');
108+
ping();
109+
}
110+
}
111+
112+
test();

0 commit comments

Comments
 (0)
Please sign in to comment.