'use strict'; const { AtomicsNotify, AtomicsStore, AtomicsWaitAsync, Int32Array, SafeMap, globalThis, } = primordials; const { SharedArrayBuffer, } = globalThis; const { isMainThread, threadId: currentThreadId, } = internalBinding('worker'); const { codes: { ERR_WORKER_MESSAGING_ERRORED, ERR_WORKER_MESSAGING_FAILED, ERR_WORKER_MESSAGING_SAME_THREAD, ERR_WORKER_MESSAGING_TIMEOUT, }, } = require('internal/errors'); const { MessageChannel } = require('internal/worker/io'); const { validateNumber } = require('internal/validators'); const messageTypes = { REGISTER_MAIN_THREAD_PORT: 'registerMainThreadPort', UNREGISTER_MAIN_THREAD_PORT: 'unregisterMainThreadPort', SEND_MESSAGE_TO_WORKER: 'sendMessageToWorker', RECEIVE_MESSAGE_FROM_WORKER: 'receiveMessageFromWorker', }; // This is only populated by main thread and always empty in other threads const threadsPorts = new SafeMap(); // This is only populated in child threads and always undefined in main thread let mainThreadPort; // SharedArrayBuffer must always be Int32, so it's * 4. // We need one for the operation status (performing / performed) and one for the result (success / failure). const WORKER_MESSAGING_SHARED_DATA = 2 * 4; const WORKER_MESSAGING_STATUS_INDEX = 0; const WORKER_MESSAGING_RESULT_INDEX = 1; // Response codes const WORKER_MESSAGING_RESULT_DELIVERED = 0; const WORKER_MESSAGING_RESULT_NO_LISTENERS = 1; const WORKER_MESSAGING_RESULT_LISTENER_ERROR = 2; // This event handler is always executed on the main thread only function handleMessageFromThread(message) { switch (message.type) { case messageTypes.REGISTER_MAIN_THREAD_PORT: { const { threadId, port } = message; // Register the port threadsPorts.set(threadId, port); // Handle messages on this port // When a new thread wants to register a children // this take care of doing that. // This way any thread can be linked to the main one. port.on('message', handleMessageFromThread); // Never block the thread on this port port.unref(); } break; case messageTypes.UNREGISTER_MAIN_THREAD_PORT: threadsPorts.get(message.threadId).close(); threadsPorts.delete(message.threadId); break; case messageTypes.SEND_MESSAGE_TO_WORKER: { // Send the message to the target thread const { source, destination, value, transferList, memory } = message; sendMessageToWorker(source, destination, value, transferList, memory); } break; } } function handleMessageFromMainThread(message) { switch (message.type) { case messageTypes.RECEIVE_MESSAGE_FROM_WORKER: receiveMessageFromWorker(message.source, message.value, message.memory); break; } } function sendMessageToWorker(source, destination, value, transferList, memory) { // We are on the main thread, we can directly process the message if (destination === 0) { receiveMessageFromWorker(source, value, memory); return; } // Search the port to the target thread const port = threadsPorts.get(destination); if (!port) { const status = new Int32Array(memory); AtomicsStore(status, WORKER_MESSAGING_RESULT_INDEX, WORKER_MESSAGING_RESULT_NO_LISTENERS); AtomicsStore(status, WORKER_MESSAGING_STATUS_INDEX, 1); AtomicsNotify(status, WORKER_MESSAGING_STATUS_INDEX, 1); return; } port.postMessage( { type: messageTypes.RECEIVE_MESSAGE_FROM_WORKER, source, destination, value, memory, }, transferList, ); } function receiveMessageFromWorker(source, value, memory) { let response = WORKER_MESSAGING_RESULT_NO_LISTENERS; try { if (process.emit('workerMessage', value, source)) { response = WORKER_MESSAGING_RESULT_DELIVERED; } } catch { response = WORKER_MESSAGING_RESULT_LISTENER_ERROR; } // Populate the result const status = new Int32Array(memory); AtomicsStore(status, WORKER_MESSAGING_RESULT_INDEX, response); AtomicsStore(status, WORKER_MESSAGING_STATUS_INDEX, 1); AtomicsNotify(status, WORKER_MESSAGING_STATUS_INDEX, 1); } function createMainThreadPort(threadId) { // Create a channel that links the new thread to the main thread const { port1: mainThreadPortToMain, port2: mainThreadPortToThread, } = new MessageChannel(); const registrationMessage = { type: messageTypes.REGISTER_MAIN_THREAD_PORT, threadId, port: mainThreadPortToMain, }; if (isMainThread) { handleMessageFromThread(registrationMessage); } else { mainThreadPort.postMessage(registrationMessage, [mainThreadPortToMain]); } return mainThreadPortToThread; } function destroyMainThreadPort(threadId) { const unregistrationMessage = { type: messageTypes.UNREGISTER_MAIN_THREAD_PORT, threadId, }; if (isMainThread) { handleMessageFromThread(unregistrationMessage); } else { mainThreadPort.postMessage(unregistrationMessage); } } function setupMainThreadPort(port) { mainThreadPort = port; mainThreadPort.on('message', handleMessageFromMainThread); // Never block the process on this port mainThreadPort.unref(); } async function postMessageToThread(threadId, value, transferList, timeout) { if (typeof transferList === 'number' && typeof timeout === 'undefined') { timeout = transferList; transferList = []; } if (typeof timeout !== 'undefined') { validateNumber(timeout, 'timeout', 0); } if (threadId === currentThreadId) { throw new ERR_WORKER_MESSAGING_SAME_THREAD(); } const memory = new SharedArrayBuffer(WORKER_MESSAGING_SHARED_DATA); const status = new Int32Array(memory); const promise = AtomicsWaitAsync(status, WORKER_MESSAGING_STATUS_INDEX, 0, timeout).value; const message = { type: messageTypes.SEND_MESSAGE_TO_WORKER, source: currentThreadId, destination: threadId, value, memory, transferList, }; if (isMainThread) { handleMessageFromThread(message); } else { mainThreadPort.postMessage(message, transferList); } // Wait for the response const response = await promise; if (response === 'timed-out') { throw new ERR_WORKER_MESSAGING_TIMEOUT(); } else if (status[WORKER_MESSAGING_RESULT_INDEX] === WORKER_MESSAGING_RESULT_NO_LISTENERS) { throw new ERR_WORKER_MESSAGING_FAILED(); } else if (status[WORKER_MESSAGING_RESULT_INDEX] === WORKER_MESSAGING_RESULT_LISTENER_ERROR) { throw new ERR_WORKER_MESSAGING_ERRORED(); } } module.exports = { createMainThreadPort, destroyMainThreadPort, setupMainThreadPort, postMessageToThread, };