@nrutils/node-red-utils 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +190 -0
  3. package/README.md +187 -2
  4. package/index.js +50 -0
  5. package/node-red-base/NodeRedNodeBase.js +143 -0
  6. package/node-red-base/README.md +215 -0
  7. package/node-red-editorcomms/EditorCommsBackend.js +155 -0
  8. package/node-red-editorcomms/EditorCommsFrontend.js +105 -0
  9. package/node-red-editorcomms/README.md +164 -0
  10. package/node-red-gpu/GpuBuffer.js +29 -0
  11. package/node-red-gpu/Kernel.js +35 -0
  12. package/node-red-gpu/README.md +220 -0
  13. package/node-red-gpu/backends/CpuBackend.js +109 -0
  14. package/node-red-gpu/backends/VulkanBackend.js +232 -0
  15. package/node-red-gpu/errors.js +48 -0
  16. package/node-red-gpu/index.js +6 -0
  17. package/node-red-gpu/log.js +18 -0
  18. package/node-red-gpu/native/binding.gyp +14 -0
  19. package/node-red-gpu/native/src/gpu_addon.cpp +1066 -0
  20. package/node-red-gpu/nodeRedGpuMain.js +39 -0
  21. package/node-red-gpu/nodeRedGpuWorker.js +242 -0
  22. package/node-red-gpu/transpiler/emitGlsl.js +497 -0
  23. package/node-red-gpu/transpiler/glslNames.js +60 -0
  24. package/node-red-gpu/transpiler/index.js +81 -0
  25. package/node-red-gpu/transpiler/inferTypes.js +302 -0
  26. package/node-red-gpu/transpiler/validate.js +461 -0
  27. package/node-red-popup/NodeRedPopupBackend.js +69 -0
  28. package/node-red-popup/NodeRedPopupFrontend.js +508 -0
  29. package/node-red-popup/README.md +113 -0
  30. package/node-red-pubsub-bus/NodeRedPubsubBus.js +617 -0
  31. package/node-red-pubsub-bus/NodeRedPubsubBusWorkerProxy.js +239 -0
  32. package/node-red-pubsub-bus/README.md +201 -0
  33. package/node-red-resourceendpoint/NodeRedResourceEndpoint.js +205 -0
  34. package/node-red-resourceendpoint/README.md +119 -0
  35. package/node-red-singleton/NodeRedSingletonBackend.js +30 -0
  36. package/node-red-singleton/NodeRedSingletonFrontend.js +54 -0
  37. package/node-red-singleton/README.md +72 -0
  38. package/node-red-worker/NodeRedWorker.js +443 -0
  39. package/node-red-worker/NodeRedWorkerRuntime.js +659 -0
  40. package/node-red-worker/README.md +273 -0
  41. package/node-red-worker/workerRuntimeLoader.js +55 -0
  42. package/package.json +70 -5
@@ -0,0 +1,659 @@
1
+ /**
2
+ * Copyright 2024 Contributors
3
+ * Licensed under the Apache License, Version 2.0
4
+ * http://www.apache.org/licenses/LICENSE-2.0
5
+ */
6
+
7
+ //
8
+ // This class is created in the worker thread
9
+ //
10
+
11
+ 'use strict';
12
+ const { parentPort, MessagePort } = require('worker_threads');
13
+
14
+ // Types that the library uses itself. 'default' handlers do not get them.
15
+ const isInternalType = (type) => type === 'ack' || type.startsWith('_');
16
+
17
+ /**
18
+ * Walks a value and collects every unique ArrayBuffer / MessagePort found.
19
+ * Used by Connection.pipe() so forwarded messages preserve zero-copy transfer
20
+ * semantics across relay hops without the relay having to know the payload shape.
21
+ */
22
+ function collectTransferables(obj, acc = new Set(), seen = new WeakSet()) {
23
+ if (!obj || typeof obj !== 'object') return acc;
24
+ if (seen.has(obj)) return acc;
25
+ seen.add(obj);
26
+ if (obj instanceof ArrayBuffer) { acc.add(obj); return acc; }
27
+ if (ArrayBuffer.isView(obj)) { acc.add(obj.buffer); return acc; }
28
+ if (obj instanceof MessagePort) { acc.add(obj); return acc; }
29
+ for (const k in obj) collectTransferables(obj[k], acc, seen);
30
+ return acc;
31
+ }
32
+
33
+ /**
34
+ * Represents a connection between nodes with pub/sub capabilities
35
+ * @extends {Object}
36
+ */
37
+ class Connection extends Object {
38
+ /**
39
+ * Creates a new Connection
40
+ * @param {MessagePort} messagePort - MessagePort for communication
41
+ * @param {Connections} connections - Parent Connections instance
42
+ */
43
+ constructor(messagePort, trace) {
44
+ super();
45
+ this.trace = trace;
46
+ this._intervalID = null; // used in publishUntilAcknowledged
47
+ this._subscribers = new Map();
48
+ this.messagePort = messagePort;
49
+ this.setMessagePort(messagePort);
50
+ this._currentPollingTopic = null;
51
+ this._currentPollingAckTopic = null;
52
+ this._currentPollingAckHandler = null;
53
+ this._currentPollingCancel = null;
54
+ Object.defineProperty(this, 'length', { value: 1, writable: false, configurable: false });
55
+
56
+ // namedPorts setup
57
+ this._namedPortsMap = new Map();
58
+ this.namedPorts = new Proxy({}, {
59
+ get: (target, prop) => {
60
+ if (prop === 'forEach') {
61
+ return (callback) => {
62
+ if (typeof callback !== 'function') {
63
+ throw new Error('Callback must be a function');
64
+ }
65
+ for (const [name, port] of this._namedPortsMap.entries()) {
66
+ callback(port, name, this.namedPorts);
67
+ }
68
+ };
69
+ } else if (prop === 'length') {
70
+ return this._namedPortsMap.size;
71
+ } else if (prop === Symbol.iterator) {
72
+ return function* () {
73
+ for (const [name, port] of this._namedPortsMap.entries()) {
74
+ yield [name, port];
75
+ }
76
+ }.bind(this);
77
+ } else if (prop === 'delete') {
78
+ return (portName) => {
79
+ if (this._namedPortsMap.has(portName)) {
80
+ const port = this._namedPortsMap.get(portName);
81
+ if (port && typeof port.close === 'function') {
82
+ port.close();
83
+ }
84
+ return this._namedPortsMap.delete(portName);
85
+ }
86
+ return false;
87
+ };
88
+ } else if (prop === 'deleteAll') {
89
+ return () => {
90
+ for (const port of this._namedPortsMap.values()) {
91
+ if (port && typeof port.close === 'function') {
92
+ port.close();
93
+ }
94
+ }
95
+ this._namedPortsMap.clear();
96
+ return true;
97
+ };
98
+ } else if (typeof prop === 'string') {
99
+ return this._namedPortsMap.get(prop);
100
+ }
101
+ return target[prop];
102
+ }
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Creates a named MessageChannel
108
+ */
109
+ createNamedPort(myPortName, theirPortName = myPortName) {
110
+ const channel = new MessageChannel();
111
+ this._namedPortsMap.set(myPortName, channel.port2);
112
+ this.publish('_createNamedPort', { portName: theirPortName, port: channel.port1 }, [channel.port1]);
113
+ return channel.port2;
114
+ };
115
+
116
+ /**
117
+ * Forwards every raw message arriving on this connection's MessagePort to
118
+ * the target's MessagePort, reconstructing transferList so ArrayBuffers
119
+ * stay zero-copy across the hop. Used to build generic passthrough relay
120
+ * workers (link in / link out / taps) without knowing the payload shape.
121
+ *
122
+ * @param {Connection|Connections|Ports} target - destination to pipe into.
123
+ * Connection: forward to its single messagePort. Connections/Ports: fan
124
+ * out to every contained Connection (only the first gets the transfer;
125
+ * remaining recipients receive structured-clone copies since a transferable
126
+ * can only be moved once).
127
+ */
128
+ pipe(target) {
129
+ const resolveTargets = () => {
130
+ if (target instanceof Connection) return [target];
131
+ if (target && typeof target === 'object' && target._connections) {
132
+ return Object.values(target._connections);
133
+ }
134
+ if (target && typeof target === 'object' && target._ports) {
135
+ const out = [];
136
+ for (const port of Object.values(target._ports)) {
137
+ if (port._connections) out.push(...Object.values(port._connections));
138
+ }
139
+ return out;
140
+ }
141
+ return [];
142
+ };
143
+
144
+ this.messagePort.on('message', (message) => {
145
+ const targets = resolveTargets();
146
+ if (targets.length === 0) return;
147
+ const transferables = [...collectTransferables(message)];
148
+ for (let i = 0; i < targets.length; i++) {
149
+ const t = targets[i];
150
+ if (!t.messagePort) continue;
151
+ t.messagePort.postMessage(message, i === 0 ? transferables : []);
152
+ }
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Sets or updates the message port for this connection and establishes message handling
158
+ * @param {MessagePort} messagePort - The message port to use for communication
159
+ * @returns {void}
160
+ *
161
+ * @description
162
+ * This method assigns a MessagePort to the connection and sets up event listeners
163
+ * to handle incoming messages. When a message arrives, it:
164
+ * 1. Extracts trace information for the receiver
165
+ * 2. Processes the message data and sender trace
166
+ * 3. Calls appropriate subscribers at multiple levels (port group, port, and connection)
167
+ * 4. Routes the message to the handlers of its type and to the 'default' handlers
168
+ *
169
+ * If the messagePort parameter is falsy, the method returns early without making changes.
170
+ */
171
+ setMessagePort(messagePort) {
172
+ if (!messagePort) return;
173
+ this.messagePort = messagePort;
174
+ this.linkId = null;
175
+ messagePort.on('message', (message) => {
176
+ if (this.messagePort !== messagePort) return; // a port that was replaced or removed
177
+ const { baseObject, basePortsGroup, portIndex } = this.trace;
178
+ const levels = [baseObject[basePortsGroup], baseObject[basePortsGroup][portIndex], this];
179
+
180
+ // Before the worker file runs, keep a message that has no handler yet.
181
+ // The runtime sends it again after the worker file adds its subscriptions.
182
+ if (!baseObject._ready && typeof message?.type === 'string' &&
183
+ !levels.some(level => level._subscribers.has(message.type))) {
184
+ baseObject._earlyMessages.push({ connection: this, message });
185
+ return;
186
+ }
187
+ this._dispatch(message);
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Calls the subscribers of a message at the port group, port, and connection levels
193
+ * @param {object} message - The received message
194
+ * @private
195
+ */
196
+ async _dispatch(message) {
197
+ // Other users of the port (e.g. the pubsub proxy) send messages without a type
198
+ if (typeof message?.type !== 'string') return;
199
+ const { baseObject, basePortsGroup, portIndex, connectionIndex } = this.trace;
200
+ // A request() from the main thread goes to its worker.handle() handler only
201
+ if (message.requestId !== undefined) return baseObject._answer(this, message);
202
+ const receiverTrace = { nodeId: baseObject.nodeId, basePortsGroup, portIndex, connectionIndex };
203
+ const { type, data, senderTrace } = message;
204
+ // The handlers at the port group, port and connection levels, in this order.
205
+ // A handler that fails goes to worker.handlerError(); the next handlers still run.
206
+ for (const subscriberMap of [baseObject[basePortsGroup]._subscribers,
207
+ baseObject[basePortsGroup][portIndex]._subscribers, this._subscribers]) {
208
+ const handlers = subscriberMap.get(type) || [];
209
+ const defaultHandlers = isInternalType(type) ? [] : subscriberMap.get('default') || [];
210
+ for (const handler of [...handlers, ...defaultHandlers]) {
211
+ try {
212
+ await handler(data, senderTrace, receiverTrace, type);
213
+ } catch (error) {
214
+ baseObject.handlerError(error, type, data);
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Subscribes to a message type
222
+ * @param {string} type - The message type to subscribe to
223
+ * @param {Function} handler - Handler function to call when messages of this type arrive
224
+ * @throws {Error} If handler is not a function
225
+ */
226
+ subscribe(type, handler) {
227
+ if (typeof handler !== 'function') { throw new Error('Handler must be a function') };
228
+ if (!this._subscribers.has(type)) { this._subscribers.set(type, []) };
229
+ this._subscribers.get(type).push(handler);
230
+ }
231
+
232
+ /**
233
+ * Unsubscribes a handler from a message type
234
+ * @param {string} type - The message type to unsubscribe from
235
+ * @param {Function} handler - The handler function to remove
236
+ * @returns {boolean} - Whether the handler was successfully removed
237
+ */
238
+ unsubscribe(type, handler) {
239
+ if (!this._subscribers.has(type)) return false;
240
+ const handlers = this._subscribers.get(type);
241
+ const index = handlers.indexOf(handler);
242
+ if (index !== -1) {
243
+ handlers.splice(index, 1);
244
+ if (handlers.length === 0) {
245
+ this._subscribers.delete(type);
246
+ }
247
+ return true;
248
+ }
249
+ return false;
250
+ }
251
+
252
+ /**
253
+ * Publishes a message to this connection
254
+ * @param {string} type - The message type
255
+ * @param {any} data - Data to send
256
+ * @param {Array} [transferList=[]] - Optional array of transferable objects
257
+ */
258
+ publish(type, data, transferList = []) {
259
+ if (!this.messagePort) {
260
+ // silently fail
261
+ } else {
262
+ const { basePortsGroup, portIndex, connectionIndex } = this.trace;
263
+ const nodeId = this.trace.baseObject.nodeId;
264
+ const senderTrace = { nodeId, hostType: 'worker', basePortsGroup, portIndex, connectionIndex };
265
+ this.messagePort.postMessage({ type, data, senderTrace }, transferList);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Repeatedly sends a message until an acknowledgment is received
271
+ * @param {string} messageTopic - The message type to send
272
+ * @param {string} ackTopic - The message type to wait for as acknowledgment
273
+ * @param {number} interval - Milliseconds between retries
274
+ * @param {any} data - Data to send
275
+ * @param {Array} [transferList=[]] - Optional array of transferable objects
276
+ * @returns {Function} cancel - Function to call to stop sending. If this topic is already polling, nothing is sent again and the cancel function of that poll is returned.
277
+ */
278
+ publishUntilAcknowledged(messageTopic, ackTopic, interval, data, transferList = []) {
279
+
280
+ if (this._currentPollingTopic === messageTopic) return this._currentPollingCancel; // already polling this topic
281
+
282
+ // Cancel any existing message polling
283
+ if (this._publishIntervalId) {
284
+ clearInterval(this._publishIntervalId);
285
+ this._publishIntervalId = null;
286
+ if (this._currentPollingTopic && this._currentPollingAckHandler) {
287
+ this.unsubscribe(this._currentPollingAckTopic, this._currentPollingAckHandler);
288
+ this._currentPollingAckHandler = null;
289
+ }
290
+ }
291
+
292
+ // Store the current polling topic and ack topic
293
+ this._currentPollingTopic = messageTopic;
294
+ this._currentPollingAckTopic = ackTopic;
295
+
296
+ // Publish the message
297
+ this.publish(messageTopic, data, transferList); // Send the first message immediately
298
+ this._publishIntervalId = setInterval(() => {
299
+ this.publish(messageTopic, data, transferList) }, interval);
300
+
301
+ // Create and store the acknowledgment handler
302
+ this._currentPollingAckHandler = (ackData, senderTrace, receiverTrace) => {
303
+ clearInterval(this._publishIntervalId);
304
+ this._publishIntervalId = null;
305
+ this._currentPollingTopic = null;
306
+ this._currentPollingAckTopic = null;
307
+ this.unsubscribe(ackTopic, this._currentPollingAckHandler);
308
+ this._currentPollingAckHandler = null;
309
+ };
310
+
311
+ // Subscribe to the acknowledgment topic
312
+ this.subscribe(ackTopic, this._currentPollingAckHandler);
313
+
314
+ // Return a cancel function
315
+ this._currentPollingCancel = () => {
316
+ if (this._publishIntervalId) {
317
+ clearInterval(this._publishIntervalId);
318
+ this._publishIntervalId = null;
319
+ }
320
+
321
+ if (this._currentPollingAckHandler) {
322
+ this.unsubscribe(ackTopic, this._currentPollingAckHandler);
323
+ this._currentPollingAckHandler = null;
324
+ }
325
+
326
+ this._currentPollingTopic = null;
327
+ this._currentPollingAckTopic = null;
328
+ };
329
+ return this._currentPollingCancel;
330
+ }
331
+
332
+ }
333
+
334
+ /**
335
+ * Manages multiple connections for a port
336
+ * @extends {Object}
337
+ */
338
+ class Connections extends Object {
339
+ /**
340
+ * Creates a new Connections instance
341
+ * @param {Ports} ports - Parent Ports instance
342
+ * @returns {Proxy} A proxy that auto-creates connections on property access
343
+ */
344
+ constructor(trace) {
345
+ super();
346
+ this.trace = trace;
347
+ this._subscribers = new Map();
348
+ this._connections = {};
349
+
350
+ Object.defineProperty(this, 'length', {
351
+ get: () =>
352
+ Object.keys(this._connections).length
353
+ });
354
+
355
+ // Make the class iterable
356
+ this[Symbol.iterator] = function* () {
357
+ for (const key of Object.keys(this._connections).sort((a, b) => Number(a) - Number(b))) {
358
+ yield this._connections[key];
359
+ }
360
+ };
361
+
362
+ return new Proxy(this, {
363
+ get(target, prop) {
364
+ if (typeof prop === 'string' && !isNaN(prop) && !(prop in target)) {
365
+ target.addConnection(Number(prop));
366
+ }
367
+ return target[prop];
368
+ }
369
+ });
370
+ }
371
+
372
+ forEach(callback) {
373
+ if (typeof callback !== 'function') { throw new Error('Callback must be a function') };
374
+
375
+ for (const [index, connection] of Object.entries(this._connections)) {
376
+ callback(connection, Number(index), this);
377
+ }
378
+ }
379
+
380
+ addConnection(connectionIndex, messagePort) {
381
+ if (this._connections[connectionIndex]) { // If the connection already exists but has no messagePort, just set the port
382
+ if (messagePort) { this._connections[connectionIndex].setMessagePort(messagePort) };
383
+ } else { // Otherwise create a new connection
384
+ this[connectionIndex] = new Connection(messagePort, { ...this.trace, connectionIndex });
385
+ this._connections[connectionIndex] = this[connectionIndex];
386
+ }
387
+ }
388
+
389
+ hasConnection(connectionIndex) {
390
+ return connectionIndex in this._connections;
391
+ }
392
+
393
+ /**
394
+ * Subscribes to a message type for all connections in this group
395
+ * @param {string} type - The message type to subscribe to
396
+ * @param {Function} handler - Handler function to call when messages of this type arrive
397
+ * @throws {Error} If handler is not a function
398
+ */
399
+ subscribe(type, handler) {
400
+ if (typeof handler !== 'function') { throw new Error('Handler must be a function') };
401
+ if (!this._subscribers.has(type)) { this._subscribers.set(type, []) };
402
+ this._subscribers.get(type).push(handler);
403
+ }
404
+
405
+ /**
406
+ * Publishes a message to all connections in this group
407
+ * @param {string} type - The message type
408
+ * @param {any} data - Data to send
409
+ * @param {Array} [transferList=[]] - Optional array of transferable objects
410
+ */
411
+ publish(type, data, transferList = []) {
412
+ Object.values(this._connections).forEach(connection => {
413
+ connection.publish(type, data, transferList);
414
+ });
415
+ }
416
+
417
+ /**
418
+ * Closes the connection with this linkId, if this port has it, and frees its slot
419
+ * @returns {boolean} true if the connection was found
420
+ */
421
+ removeLink(linkId) {
422
+ for (const connection of Object.values(this._connections)) {
423
+ if (connection.linkId !== linkId) continue;
424
+ connection.messagePort.close();
425
+ connection.messagePort = null;
426
+ connection.linkId = null;
427
+ return true;
428
+ }
429
+ return false;
430
+ }
431
+
432
+ getNextConnectionNumber() {
433
+ // First look for existing connections without messagePorts
434
+ for (const [key, connection] of Object.entries(this._connections)) {
435
+ if (!connection.messagePort) {
436
+ return Number(key);
437
+ }
438
+ }
439
+ // If no pending connections found, get next available number
440
+ const numericKeys = Object.keys(this._connections)
441
+ .filter(key => !isNaN(key))
442
+ .map(Number);
443
+ return numericKeys.length ? Math.max(...numericKeys) + 1 : 0;
444
+ }
445
+
446
+ }
447
+
448
+ /**
449
+ * Manages multiple ports for a base ports group
450
+ * @extends {Object}
451
+ */
452
+ class Ports extends Object {
453
+ /**
454
+ * Creates a new Ports instance
455
+ * @param {NodeRedWorkerRuntime} basePortsGroup - Parent NodeRedWorkerRuntime instance
456
+ * @returns {Proxy} A proxy that auto-creates ports on property access
457
+ */
458
+ constructor(trace) {
459
+ super()
460
+ this.trace = trace;
461
+ this._subscribers = new Map();
462
+ this._ports = {};
463
+
464
+ Object.defineProperty(this, 'length', {
465
+ get: () =>
466
+ Object.keys(this._ports).length
467
+ });
468
+
469
+ this[Symbol.iterator] = function* () {
470
+ for (const key of Object.keys(this._ports).sort((a, b) => Number(a) - Number(b))) {
471
+ yield this._ports[key];
472
+ }
473
+ };
474
+
475
+ return new Proxy(this, {
476
+ get(target, prop) {
477
+ if (typeof prop === 'string' && !isNaN(prop) && !(prop in target)) {
478
+ target.addPort(Number(prop));
479
+ }
480
+ return target[prop];
481
+ }
482
+ });
483
+ }
484
+
485
+ forEach(callback) {
486
+ if (typeof callback !== 'function') {
487
+ throw new Error('Callback must be a function');
488
+ }
489
+
490
+ for (const [index, port] of Object.entries(this._ports)) {
491
+ callback(port, Number(index), this);
492
+ }
493
+ }
494
+
495
+ addPort(portIndex) {
496
+ this[portIndex] = new Connections({ ...this.trace, portIndex });
497
+ this._ports[portIndex] = this[portIndex];
498
+ }
499
+
500
+ hasPort(portIndex) {
501
+ return portIndex in this._ports;
502
+ }
503
+
504
+ /**
505
+ * Subscribes to a message type for all ports in this group
506
+ * @param {string} type - The message type to subscribe to
507
+ * @param {Function} handler - Handler function to call when messages of this type arrive
508
+ * @throws {Error} If handler is not a function
509
+ */
510
+ subscribe(type, handler) {
511
+ if (typeof handler !== 'function') { throw new Error('Handler must be a function') };
512
+ if (!this._subscribers.has(type)) { this._subscribers.set(type, []) };
513
+ this._subscribers.get(type).push(handler);
514
+ }
515
+
516
+ /**
517
+ * Publishes a message to all ports in this group
518
+ * @param {string} type - The message type
519
+ * @param {any} data - Data to send
520
+ * @param {Array} [transferList=[]] - Optional array of transferable objects
521
+ */
522
+ publish(type, data, transferList = []) {
523
+ Object.values(this._ports).forEach(port => {
524
+ port.publish(type, data, transferList);
525
+ });
526
+ }
527
+
528
+ }
529
+
530
+ /**
531
+ * Pub/Sub message communication between worker threads and the main thread in Node-RED
532
+ */
533
+ class NodeRedWorkerRuntime {
534
+ /**
535
+ * Creates a new NodeRedWorkerRuntime instance
536
+ * @param {string} workerPath - Path to the worker directory
537
+ * @param {Function} [ConnectionsCompleteCallback=null] - Callback function executed when all connections are established
538
+ */
539
+ constructor(params) {
540
+ // list all params here
541
+ this._connectionsCompleteCallback = params.ConnectionsCompleteCallback;
542
+
543
+ this.nodeId = null;
544
+ this._started = false; // true after the worker file was loaded
545
+ this._ready = false; // true after the worker file added its subscriptions
546
+ this._earlyMessages = []; // messages that arrived before _ready, in order
547
+ this._handlers = new Map(); // request type -> handler of worker.handle()
548
+ this.inputPorts = new Ports({ baseObject: this, basePortsGroup: 'inputPorts' });
549
+ this.outputPorts = new Ports({ baseObject: this, basePortsGroup: 'outputPorts' });
550
+ this.parentPorts = new Ports({ baseObject: this, basePortsGroup: 'parentPorts' });
551
+ this.parentPorts[0][0].setMessagePort(parentPort); // attach the standard worker parentPort to our parentPorts object
552
+ this._setupStandardSubscriptions();
553
+ }
554
+
555
+ /**
556
+ * Answers the main-thread request() calls of one type.
557
+ * The handler gets (data, transferList) and returns the answer or a Promise for it.
558
+ * Push ArrayBuffers of the answer into transferList to move them instead of copying them.
559
+ * If the handler throws, request() rejects with an Error that has the same name and message.
560
+ * @param {string} type - The request type
561
+ * @param {Function} handler - (data, transferList) => answer
562
+ * @throws {Error} If handler is not a function, or the type has a handler already
563
+ */
564
+ handle(type, handler) {
565
+ if (typeof handler !== 'function') throw new Error('Handler must be a function');
566
+ if (this._handlers.has(type)) throw new Error(`worker.handle(): type '${type}' has a handler already`);
567
+ this._handlers.set(type, handler);
568
+ }
569
+
570
+ /**
571
+ * Calls the handler of a request and sends the answer back on the same connection.
572
+ * @private
573
+ */
574
+ async _answer(connection, { type, data, requestId }) {
575
+ const reply = (body, transferList = []) =>
576
+ connection.messagePort.postMessage({ type: '_reply', data: { requestId, ...body } }, transferList);
577
+ try {
578
+ const handler = this._handlers.get(type);
579
+ if (!handler) throw new Error(`the worker has no worker.handle('${type}', ...) handler`);
580
+ const transferList = [];
581
+ const result = await handler(data, transferList);
582
+ reply({ result }, transferList);
583
+ } catch (error) {
584
+ // For example a throw in the handler, or an answer that cannot be cloned
585
+ reply({ error: { name: (error && error.name) || 'Error', message: (error && error.message) || String(error) } });
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Reports a throw (or a rejected Promise) of a subscribe handler in the worker.
591
+ * The default sends it to the main thread, where the node logs it with node.error.
592
+ * A worker file can replace it: worker.handlerError = (error, type, data) => { ... }.
593
+ * @param {*} error - The value that the handler threw
594
+ * @param {string} type - The message type of the handler
595
+ * @param {*} data - The data of the message that the handler got
596
+ */
597
+ handlerError(error, type, data) {
598
+ this.parentPorts.publish('_handlerError', { type, message: (error && error.message) || String(error) });
599
+ }
600
+
601
+ _setupStandardSubscriptions() {
602
+
603
+ // Handle input port addition
604
+ this.parentPorts.subscribe('_addInputConnection', ({ nodeId, linkId, messagePort }, senderTrace, receiverTrace) => {
605
+ const myConnectionIndex = this.inputPorts[0].getNextConnectionNumber();
606
+ this.nodeId = nodeId;
607
+ this.inputPorts[0][myConnectionIndex].setMessagePort(messagePort);
608
+ this.inputPorts[0][myConnectionIndex].linkId = linkId;
609
+ this.parentPorts.publish('ack', '_inputPortReceived');
610
+ });
611
+
612
+ // Handle output port addition
613
+ this.parentPorts.subscribe('_addOutputConnection', ({ nodeId, portIndex, connectionIndex, linkId, messagePort }, senderTrace, receiverTrace) => {
614
+ this.nodeId = nodeId;
615
+ this.outputPorts[portIndex][connectionIndex].setMessagePort(messagePort);
616
+ this.outputPorts[portIndex][connectionIndex].linkId = linkId;
617
+ this.parentPorts.publish('ack', '_outputPortReceived');
618
+ });
619
+
620
+ // The worker at the other end of a wire closed: close this end and free the slot
621
+ this.parentPorts.subscribe('_removeConnection', ({ linkId }) => {
622
+ for (const group of [this.inputPorts, this.outputPorts]) {
623
+ for (const port of Object.values(group._ports)) {
624
+ if (port.removeLink(linkId)) return;
625
+ }
626
+ }
627
+ });
628
+
629
+ // Handle _createNamedPorts from other workers
630
+ this.inputPorts.subscribe('_createNamedPort', ({ portName, port }, senderTrace, receiverTrace) => {
631
+ const connection = this.inputPorts[receiverTrace.portIndex][receiverTrace.connectionIndex];
632
+ if (connection._namedPortsMap.has(portName)) throw new Error(`_createNamedPort error: Named port '${portName}' already exists.`);
633
+ connection._namedPortsMap.set(portName, port);
634
+ });
635
+
636
+ // Handle connections complete notification
637
+ // Load the worker file one time only, then send the early messages again
638
+ this.parentPorts.subscribe('_ConnectionsComplete', async (data, senderTrace, receiverTrace) => {
639
+ if (this._started) return;
640
+ this._started = true;
641
+ if (typeof this._connectionsCompleteCallback === 'function') {
642
+ await this._connectionsCompleteCallback();
643
+ }
644
+ this._ready = true;
645
+ const early = this._earlyMessages;
646
+ this._earlyMessages = [];
647
+ for (const { connection, message } of early) connection._dispatch(message);
648
+ });
649
+
650
+ // Built-in close ack — fires at connection level (after user-level 'close' handlers)
651
+ // so gpu.close() and other cleanup runs first, then we ack the main thread
652
+ this.parentPorts[0][0].subscribe('close', () => {
653
+ this.parentPorts.publish('ack', 'close');
654
+ });
655
+
656
+ }
657
+ }
658
+
659
+ module.exports.NodeRedWorkerRuntime = NodeRedWorkerRuntime;