@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.
- package/CHANGELOG.md +20 -0
- package/LICENSE +190 -0
- package/README.md +187 -2
- package/index.js +50 -0
- package/node-red-base/NodeRedNodeBase.js +143 -0
- package/node-red-base/README.md +215 -0
- package/node-red-editorcomms/EditorCommsBackend.js +155 -0
- package/node-red-editorcomms/EditorCommsFrontend.js +105 -0
- package/node-red-editorcomms/README.md +164 -0
- package/node-red-gpu/GpuBuffer.js +29 -0
- package/node-red-gpu/Kernel.js +35 -0
- package/node-red-gpu/README.md +220 -0
- package/node-red-gpu/backends/CpuBackend.js +109 -0
- package/node-red-gpu/backends/VulkanBackend.js +232 -0
- package/node-red-gpu/errors.js +48 -0
- package/node-red-gpu/index.js +6 -0
- package/node-red-gpu/log.js +18 -0
- package/node-red-gpu/native/binding.gyp +14 -0
- package/node-red-gpu/native/src/gpu_addon.cpp +1066 -0
- package/node-red-gpu/nodeRedGpuMain.js +39 -0
- package/node-red-gpu/nodeRedGpuWorker.js +242 -0
- package/node-red-gpu/transpiler/emitGlsl.js +497 -0
- package/node-red-gpu/transpiler/glslNames.js +60 -0
- package/node-red-gpu/transpiler/index.js +81 -0
- package/node-red-gpu/transpiler/inferTypes.js +302 -0
- package/node-red-gpu/transpiler/validate.js +461 -0
- package/node-red-popup/NodeRedPopupBackend.js +69 -0
- package/node-red-popup/NodeRedPopupFrontend.js +508 -0
- package/node-red-popup/README.md +113 -0
- package/node-red-pubsub-bus/NodeRedPubsubBus.js +617 -0
- package/node-red-pubsub-bus/NodeRedPubsubBusWorkerProxy.js +239 -0
- package/node-red-pubsub-bus/README.md +201 -0
- package/node-red-resourceendpoint/NodeRedResourceEndpoint.js +205 -0
- package/node-red-resourceendpoint/README.md +119 -0
- package/node-red-singleton/NodeRedSingletonBackend.js +30 -0
- package/node-red-singleton/NodeRedSingletonFrontend.js +54 -0
- package/node-red-singleton/README.md +72 -0
- package/node-red-worker/NodeRedWorker.js +443 -0
- package/node-red-worker/NodeRedWorkerRuntime.js +659 -0
- package/node-red-worker/README.md +273 -0
- package/node-red-worker/workerRuntimeLoader.js +55 -0
- 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;
|