@nrutils/node-red-utils 0.0.0-stage → 0.1.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 +15 -0
  2. package/LICENSE +190 -0
  3. package/README.md +181 -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 +414 -0
  29. package/node-red-popup/README.md +110 -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 +438 -0
  39. package/node-red-worker/NodeRedWorkerRuntime.js +651 -0
  40. package/node-red-worker/README.md +265 -0
  41. package/node-red-worker/workerRuntimeLoader.js +55 -0
  42. package/package.json +70 -5
@@ -0,0 +1,215 @@
1
+ # node-red-base
2
+
3
+ `NodeRedNodeBase` turns a list of mixins and a setup function into a Node-RED node module.
4
+
5
+ ## Usage
6
+
7
+ ```js
8
+ const { NodeRedNodeBase } = require('@nrutils/node-red-utils');
9
+
10
+ module.exports = NodeRedNodeBase.createNode('upper-case', [], (RED, thisNode, config) => {
11
+ thisNode.on('input', (msg, send, done) => {
12
+ msg.payload = String(msg.payload).toUpperCase();
13
+ send(msg);
14
+ done();
15
+ });
16
+ });
17
+ ```
18
+
19
+ Put mixins in the array to add features. With an empty array you get a plain node with less code than the usual `registerType` wrapper.
20
+
21
+ ## Example: one node with two mixins
22
+
23
+ A vibration sensor node. Its [worker](../node-red-worker/README.md) computes the RMS of each block of samples, off the Node-RED event loop. The node shows the result in its status, on its output and, through [editor comms](../node-red-editorcomms/README.md), in its open edit dialog. The Reset button goes from the dialog to the runtime to the worker, and the answer comes back to the dialog.
24
+
25
+ `vibration.js`:
26
+
27
+ ```js
28
+ const path = require('path');
29
+ const { NodeRedNodeBase, nodeRedWorker, nodeRedEditorComms } = require('@nrutils/node-red-utils');
30
+
31
+ module.exports = NodeRedNodeBase.createNode('vibration', [nodeRedWorker, nodeRedEditorComms], (RED, thisNode, config) => {
32
+ const worker = thisNode.nodeRedWorker.init(path.join(__dirname, 'vibration-worker.js'), { rate: 1000 });
33
+ const comms = thisNode.nodeRedEditorComms;
34
+
35
+ // Each second from the worker: to the status, to an open edit dialog, and to the flow
36
+ worker.subscribe('rms', ({ rms, peak }) => {
37
+ thisNode.status({ fill: rms > 1 ? 'red' : 'green', shape: 'dot', text: `rms ${rms.toFixed(2)}` });
38
+ comms.publish('rms', { rms, peak });
39
+ thisNode.send({ payload: rms, peak });
40
+ });
41
+
42
+ // The Reset button in the dialog: editor to runtime to worker, and the answer back
43
+ comms.subscribe('reset-peak', () => worker.request('reset-peak', null, [], { timeout: 2000 }));
44
+ });
45
+ ```
46
+
47
+ `vibration-worker.js`:
48
+
49
+ ```js
50
+ // Stand-in for a driver that reads a block of samples from an ADC
51
+ const readSamples = (n) => Float64Array.from({ length: n }, (_, i) => Math.sin(i / 5) + Math.random() * 0.1);
52
+
53
+ module.exports = function (worker, data) {
54
+ let peak = 0;
55
+ const timer = setInterval(() => {
56
+ let sum = 0;
57
+ for (const v of readSamples(data.rate)) {
58
+ sum += v * v;
59
+ peak = Math.max(peak, Math.abs(v));
60
+ }
61
+ worker.parentPorts.publish('rms', { rms: Math.sqrt(sum / data.rate), peak });
62
+ }, 1000);
63
+
64
+ // The answer to request('reset-peak'): the peak before the reset
65
+ worker.handle('reset-peak', () => { const before = peak; peak = 0; return before; });
66
+ worker.parentPorts.subscribe('close', () => clearInterval(timer));
67
+ };
68
+ ```
69
+
70
+ `vibration.html`:
71
+
72
+ ```html
73
+ <script type="text/javascript" src="node-red-resourceendpoint/node-red-editorcomms/EditorCommsFrontend.js"></script>
74
+
75
+ <script type="text/javascript">
76
+ RED.nodes.registerType('vibration', {
77
+ category: 'input',
78
+ defaults: {},
79
+ outputs: 1,
80
+ label: function () { return 'vibration'; },
81
+ // Only the open dialog of this node has the data-node attribute
82
+ nodeRedEditorComms_subscriptions: {
83
+ rms: function (v) { $('#vibration-live[data-node="' + this.id + '"]').text('rms ' + v.rms.toFixed(2) + ', peak ' + v.peak.toFixed(2)); }
84
+ },
85
+ oneditprepare: function () {
86
+ const node = this;
87
+ $('#vibration-live').attr('data-node', node.id);
88
+ $('#vibration-reset').on('click', () => node.nodeRedEditorComms.publish('reset-peak')
89
+ .then((before) => RED.notify('Peak before the reset: ' + before.toFixed(2)))
90
+ .catch((err) => RED.notify(err.status === 404 ? 'Deploy the node first' : err.message, 'warning')));
91
+ }
92
+ });
93
+ </script>
94
+
95
+ <script type="text/html" data-template-name="vibration">
96
+ <div class="form-row">
97
+ <span id="vibration-live">waiting for data</span>
98
+ <button type="button" id="vibration-reset" class="red-ui-button">Reset peak</button>
99
+ </div>
100
+ </script>
101
+ ```
102
+
103
+ ## API
104
+
105
+ ### `NodeRedNodeBase.createNode(nodeType, mixins, setup, [options])`
106
+
107
+ Returns `function (RED)` for `module.exports`. That function calls `onRegisterType(RED)` on the composed class one time, then registers `nodeType` with `RED.nodes.registerType(nodeType, constructor, options)`. For each node instance it:
108
+
109
+ 1. calls `RED.nodes.createNode(this, config)`,
110
+ 2. applies the mixins left to right,
111
+ 3. calls `setup(RED, thisNode, config)`, where `thisNode` is the Node-RED node.
112
+
113
+ Register your `input` and `close` handlers, and call mixin methods such as `nodeRedWorker.init()`, before the first `await` in `setup`. `createNode` does not await `setup`; an error after an `await` is only logged with `node.error` as `setup of <type> failed: <message>`.
114
+
115
+ `options` is optional. It is the third argument of `RED.nodes.registerType`, for example `{ credentials }` or `{ settings }`. See [Credentials and settings](#credentials-and-settings). One more key, `resources`, is for the [`nodeRedResourceEndpoint`](../node-red-resourceendpoint/README.md) mixin: an array of file and directory paths that it serves from when Node-RED loads the node. `createNode` does not pass `resources` on to `registerType`. Without the mixin in `mixins`, `resources` does nothing.
116
+
117
+ `mixins` is always an array; use `[]` for none. `createNode()` and `with()` check their arguments and throw one `TypeError` that names the function and the bad argument, for example `createNode(type, mixins, setup): mixins must be an array of mixin functions; got function (anonymous)`. A common mistake is to pass a class, such as `NodeRedWorker`, instead of the mixin `nodeRedWorker`.
118
+
119
+ ### `NodeRedNodeBase.with(...mixins)`
120
+
121
+ Returns a class that extends `NodeRedNodeBase` with the mixins applied. `createNode` uses it. You only need it to compose classes yourself, for example in a node that cannot use `createNode` (see [Adding the library to an existing node](#adding-the-library-to-an-existing-node)). If you do, call `MyClass.onRegisterType(RED)` one time before `RED.nodes.registerType`. Without that call, the editor scripts of `nodeRedEditorComms`, `nodeRedPopup` and `nodeRedSingleton` are not served until a node starts.
122
+
123
+ ### `static onRegisterType(RED)`
124
+
125
+ `createNode` calls this one time, before any node instance exists. `nodeRedEditorComms`, `nodeRedPopup` and `nodeRedSingleton` override it to serve their editor scripts before the first deploy. `nodeRedResourceEndpoint`, `nodeRedPubsubBus` and `nodeRedWorker` override it too, for their own one-time work. A mixin that needs one-time work can override it too; call `super.onRegisterType(RED)` first.
126
+
127
+ ## Credentials and settings
128
+
129
+ The fourth argument of `createNode` goes to `registerType` as its options, for example `{ credentials: { token: { type: 'password' } } }`. Node-RED then stores the credentials outside the flow file and puts them on `thisNode.credentials`. The HTML file needs the same `credentials` definition and an input with the id `node-input-<name>`. Use the credential inside the node. Do not copy it onto `msg`.
130
+
131
+ ## Config nodes
132
+
133
+ A config node holds settings that several nodes share, such as a server address. Make it with `createNode` and give it the category `config` in the HTML file; the ids in its template start with `node-config-input-`. A consumer node stores the config node's id and reads it with `RED.nodes.getNode(id)`, which returns `null` when no config node is selected. Both types can go in one file:
134
+
135
+ ```js
136
+ const server = NodeRedNodeBase.createNode('my-server', [], (RED, thisNode, config) => {
137
+ thisNode.host = config.host;
138
+ }, { credentials: { password: { type: 'password' } } });
139
+
140
+ const query = NodeRedNodeBase.createNode('my-query', [], (RED, thisNode, config) => {
141
+ const serverNode = RED.nodes.getNode(config.server);
142
+ thisNode.on('input', (msg, send, done) => {
143
+ if (!serverNode) return done(new Error('no server configured'));
144
+ // use serverNode.host and serverNode.credentials.password here
145
+ send(msg);
146
+ done();
147
+ });
148
+ });
149
+
150
+ module.exports = function (RED) {
151
+ server(RED);
152
+ query(RED);
153
+ };
154
+ ```
155
+
156
+ In the HTML file, `server: { value: '', type: 'my-server' }` in the consumer's `defaults` gives a config node picker. `package.json` lists the file one time; the HTML of both types goes in the `.html` file with the same name.
157
+
158
+ ## Adding the library to an existing node
159
+
160
+ A node that you already have, or that must keep its own constructor, does not need `createNode`. Compose the class with `with()` and create it in your constructor:
161
+
162
+ ```js
163
+ const { NodeRedNodeBase, nodeRedWorker } = require('@nrutils/node-red-utils');
164
+
165
+ module.exports = function (RED) {
166
+ const Base = NodeRedNodeBase.with(nodeRedWorker);
167
+ Base.onRegisterType(RED); // one time, before registerType
168
+
169
+ function MyNode(config) {
170
+ RED.nodes.createNode(this, config); // as before
171
+ new Base(RED, this, config); // adds thisNode.nodeRedWorker and so on
172
+ // your existing code
173
+ }
174
+
175
+ RED.nodes.registerType('my-node', MyNode, { credentials: { token: { type: 'password' } } });
176
+ };
177
+ ```
178
+
179
+ Steps: pick the mixins, call `with()` and `onRegisterType(RED)` once in the module function, call `new Base(RED, this, config)` after `RED.nodes.createNode` in the node constructor. The rest of the node stays as it is. The mixins add their API to `this` (the node) as `this.nodeRedWorker`, `this.nodeRedEditorComms` and so on. For editor mixins, also add the script tag to the HTML file (see the README of each mixin).
180
+
181
+ ## Write a mixin
182
+
183
+ A mixin is a function that takes a class and returns a subclass. Its constructor gets `(RED, thisNode, config)`.
184
+
185
+ ```js
186
+ const { NodeRedNodeBase } = require('@nrutils/node-red-utils');
187
+
188
+ const heartbeat = (superclass) => class extends superclass {
189
+ constructor(RED, thisNode, config) {
190
+ super(RED, thisNode, config);
191
+ let timer = null;
192
+
193
+ thisNode.heartbeat = {
194
+ start(ms) {
195
+ clearInterval(timer);
196
+ timer = setInterval(() => thisNode.send({ payload: Date.now() }), ms);
197
+ }
198
+ };
199
+
200
+ thisNode.on('close', () => clearInterval(timer));
201
+ }
202
+ };
203
+
204
+ module.exports = NodeRedNodeBase.createNode('heartbeat', [heartbeat], (RED, thisNode, config) => {
205
+ thisNode.heartbeat.start(Number(config.interval) || 1000);
206
+ });
207
+ ```
208
+
209
+ Follow the same rules as the built-in mixins:
210
+
211
+ - Put the API on `thisNode` under one name. `setup` receives `thisNode`, not the mixin instance.
212
+ - Release timers, handlers and workers in `thisNode.on('close')`. Node-RED closes a node when a deploy replaces or removes it.
213
+ - `with()` and `createNode()` call each mixin with a dummy class to check it, so a mixin function may be called more than once. Do no work in the arrow function itself; do it in the constructor.
214
+
215
+ [Back to node-red-utils](../README.md)
@@ -0,0 +1,155 @@
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
+ 'use strict';
8
+
9
+ const path = require('path');
10
+ const { NodeRedResourceEndpoint } = require('../node-red-resourceendpoint/NodeRedResourceEndpoint.js');
11
+
12
+ // All copies of this library in one process share this state.
13
+ // Thus one POST route serves the handlers of every copy.
14
+ const shared = globalThis[Symbol.for('node-red-utils.editorcomms')] ??= {};
15
+ shared.handlers ??= new Map(); // nodeId.messageType -> callback
16
+
17
+ /** Serve EditorCommsFrontend.js and add the POST route. Safe to call more than one time. */
18
+ function setup(RED) {
19
+ new NodeRedResourceEndpoint(RED).registerResources('node-red-editorcomms', path.join(__dirname, 'EditorCommsFrontend.js'));
20
+ if (shared.routeRegistered) return;
21
+ shared.routeRegistered = true;
22
+ // Single endpoint for all communication
23
+ RED.httpAdmin.post('/node_red_editorcomms', RED.auth.needsPermission('write'), async (req, res) => {
24
+ const { messageType, nodeId, payload } = req.body || {};
25
+ const handler = shared.handlers.get(nodeId + '.' + messageType);
26
+ if (!handler) {
27
+ // For example a node that is not deployed yet
28
+ res.status(404).json({ status: 'no handler' });
29
+ return;
30
+ }
31
+ let result;
32
+ try {
33
+ result = await handler(payload);
34
+ } catch (error) {
35
+ const message = (error && error.message) || String(error);
36
+ const node = RED.nodes.getNode(nodeId);
37
+ if (node) node.error(`Editor message '${messageType}' failed: ${message}`);
38
+ res.status(500).json({ error: message });
39
+ return;
40
+ }
41
+ res.json({ status: 'ok', result });
42
+ });
43
+ }
44
+
45
+ /**
46
+ * NodeRedEditorComms - Class for managing communication between the Node-RED editor and runtime
47
+ * Allows nodes to publish and subscribe to messages between the editor and backend
48
+ */
49
+ class NodeRedEditorComms {
50
+
51
+ /**
52
+ * Create a new editor communications handler
53
+ * @param {object} RED - The Node-RED runtime instance
54
+ * @param {object} node - The node instance
55
+ */
56
+ constructor(RED, node) {
57
+ this.RED = RED;
58
+ this.node = node;
59
+ this.STATIC_TOPIC = 'node_red_editorcomms';
60
+
61
+ setup(RED);
62
+
63
+ // Remove the handlers of this node when the node closes
64
+ this.ownedKeys = new Map();
65
+ node.on('close', () => {
66
+ for (const [key, callback] of this.ownedKeys) {
67
+ if (shared.handlers.get(key) === callback) shared.handlers.delete(key);
68
+ }
69
+ this.ownedKeys.clear();
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Subscribe to messages of a specific type
75
+ * @param {string} messageType - The type of message to subscribe to
76
+ * @param {Function} callback - Called with the payload. Its return value (or the value of its Promise) goes back to the editor.
77
+ */
78
+ subscribe(messageType, callback) {
79
+ const key = this.node.id + '.' + messageType;
80
+ shared.handlers.set(key, callback);
81
+ this.ownedKeys.set(key, callback);
82
+ }
83
+
84
+ /**
85
+ * Publish a message to subscribers
86
+ * @param {string} messageType - The type of message to publish
87
+ * @param {any} payload - The payload to send with the message
88
+ */
89
+ publish(messageType, payload) {
90
+ const message = {
91
+ id: this.STATIC_TOPIC,
92
+ retain: false,
93
+ payload: {
94
+ messageType,
95
+ nodeId: this.node.id,
96
+ payload
97
+ }
98
+ };
99
+ this.RED.events.emit('runtime-event', message);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Mixin that adds editor communication capabilities to Node-RED nodes
105
+ *
106
+ * Allows nodes to communicate between the editor frontend and the runtime backend
107
+ *
108
+ * @param {class} superclass - The class to extend with editor comms capabilities
109
+ * @returns {class} A new class that extends the superclass with editor comms functionality
110
+ */
111
+ const nodeRedEditorComms = (superclass) => class extends superclass {
112
+ // Serve the frontend script and the POST route before the first deploy
113
+ static onRegisterType(RED) {
114
+ super.onRegisterType?.(RED);
115
+ setup(RED);
116
+ }
117
+
118
+ constructor(RED, thisNode, config) {
119
+ super(RED, thisNode, config);
120
+
121
+ // Create the editor comms handler for this node
122
+ const editorComms = new NodeRedEditorComms(RED, thisNode);
123
+
124
+ const commsInterface = {
125
+ /**
126
+ * Subscribe to messages of a specific type
127
+ * @param {string} messageType - The type of message to subscribe to
128
+ * @param {Function} callback - The callback function to invoke when messages are received
129
+ */
130
+ subscribe: function(messageType, callback) {
131
+ editorComms.subscribe(messageType, callback);
132
+ return this;
133
+ },
134
+
135
+ /**
136
+ * Publish a message to subscribers
137
+ * @param {string} messageType - The type of message to publish
138
+ * @param {any} payload - The payload to send with the message
139
+ */
140
+ publish: function(messageType, payload) {
141
+ editorComms.publish(messageType, payload);
142
+ return this;
143
+ }
144
+ };
145
+
146
+ // Make available on both
147
+ this.nodeRedEditorComms = commsInterface;
148
+ thisNode.nodeRedEditorComms = commsInterface;
149
+
150
+ }
151
+ };
152
+
153
+ // Export both the class and the mixin
154
+ module.exports.nodeRedEditorComms = nodeRedEditorComms;
155
+ module.exports.NodeRedEditorComms = NodeRedEditorComms;
@@ -0,0 +1,105 @@
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
+ // Begin EditorCommsFrontend.js
9
+ //
10
+ // Frontend communication handler for Node-RED editor.
11
+ // Manages pub/sub messaging between frontend and backend components.
12
+ //
13
+
14
+ if (!window.EditorCommsFrontend) {
15
+
16
+ window.EditorCommsFrontend = class EditorCommsFrontend {
17
+
18
+ static #handlers = new Map();
19
+
20
+ constructor() {
21
+ this.nodeId = null;
22
+ this.STATIC_TOPIC = 'node_red_editorcomms';
23
+ return this;
24
+ }
25
+
26
+ static getHandlers() { return this.#handlers }
27
+ static clearHandlers() { this.#handlers.clear() }
28
+ static setHandler(key, handler) { this.#handlers.set(key, handler) }
29
+
30
+ // subscribe(type, callback) listens to this node; subscribe(type, nodeId, callback) to another node
31
+ subscribe(messageType, nodeId, callback) {
32
+ if (typeof nodeId === 'function') { callback = nodeId; nodeId = this.nodeId; }
33
+ const key = nodeId + "." + messageType;
34
+ EditorCommsFrontend.#handlers.set(key, callback);
35
+ }
36
+
37
+ // Returns a Promise that resolves with the return value of the runtime handler.
38
+ // It rejects with an Error that has a status: 404 (no handler, for example
39
+ // the node is not deployed), 500 (the handler threw) or 0 (no connection).
40
+ publish(messageType, payload) {
41
+
42
+ const message = {
43
+ messageType,
44
+ payload,
45
+ nodeId: this.nodeId
46
+ };
47
+
48
+ return new Promise((resolve, reject) => {
49
+ $.ajax({
50
+ url: this.STATIC_TOPIC, // relative: works with httpAdminRoot and proxy paths
51
+ type: 'POST',
52
+ data: JSON.stringify(message),
53
+ contentType: 'application/json'
54
+ }).then((body) => resolve(body && body.result), (jqXHR, textStatus, errorThrown) => {
55
+ const body = jqXHR.responseJSON || {};
56
+ const text = jqXHR.status === 404 ? 'no handler for ' + messageType + ': deploy the node first'
57
+ : body.error || errorThrown || textStatus;
58
+ console.error('Failed to publish:', messageType, text);
59
+ reject(Object.assign(new Error(text), { status: jqXHR.status }));
60
+ });
61
+ });
62
+ }
63
+ }
64
+
65
+ // Hold messages until the editor has drawn the flows. Handlers that
66
+ // touch the view or the DOM fail before that.
67
+ let flowsLoaded = !!RED.workspaces.active();
68
+ const pending = [];
69
+ const dispatch = ({ messageType, nodeId, payload }) => {
70
+ const handler = window.EditorCommsFrontend.getHandlers().get(nodeId + "." + messageType);
71
+ if (handler) handler(payload);
72
+ };
73
+ RED.events.on('flows:loaded', () => {
74
+ flowsLoaded = true;
75
+ pending.splice(0).forEach(dispatch);
76
+ });
77
+ RED.comms.subscribe('notification/node_red_editorcomms', (topic, data) => {
78
+ if (flowsLoaded) dispatch(data);
79
+ else pending.push(data);
80
+ });
81
+
82
+ // Remove the handlers of a node that is deleted in the editor
83
+ RED.events.on('nodes:remove', (node) => {
84
+ const handlers = window.EditorCommsFrontend.getHandlers();
85
+ for (const key of handlers.keys()) {
86
+ if (key.startsWith(node.id + '.')) handlers.delete(key);
87
+ }
88
+ });
89
+
90
+ // add nodeRedEditorComms to every node
91
+ RED.events.on('nodes:add', async (node) => {
92
+
93
+ // Give every node its own comms instance
94
+ node.nodeRedEditorComms = new window.EditorCommsFrontend();
95
+ node.nodeRedEditorComms.nodeId = node.id;
96
+
97
+ // Add developer subscriptions if defined
98
+ if (node?._def?.nodeRedEditorComms_subscriptions) {
99
+ Object.entries(node._def.nodeRedEditorComms_subscriptions).forEach(([type, handler]) =>
100
+ window.EditorCommsFrontend.getHandlers().set(`${node.id}.${type}`, handler.bind(node))
101
+ );
102
+ }
103
+ });
104
+
105
+ }
@@ -0,0 +1,164 @@
1
+ # node-red-editorcomms
2
+
3
+ Send messages between a runtime node and the same node in the editor.
4
+
5
+ ## Usage
6
+
7
+ A node that draws a live chart of its numeric payloads in its edit dialog. When the dialog opens, it asks the runtime for the last 100 values. While the dialog is open, the runtime pushes each new value.
8
+
9
+ `live-chart.js`:
10
+
11
+ ```js
12
+ const { NodeRedNodeBase, nodeRedEditorComms } = require('@nrutils/node-red-utils');
13
+
14
+ module.exports = NodeRedNodeBase.createNode('live-chart', [nodeRedEditorComms], (RED, thisNode, config) => {
15
+ const comms = thisNode.nodeRedEditorComms;
16
+ const history = []; // the last 100 numbers
17
+
18
+ // The edit dialog asks for the history when it opens. The return value is the answer.
19
+ comms.subscribe('history', () => history);
20
+
21
+ thisNode.on('input', (msg, send, done) => {
22
+ const value = Number(msg.payload);
23
+ if (Number.isFinite(value)) {
24
+ history.push(value);
25
+ if (history.length > 100) history.shift();
26
+ comms.publish('value', value); // to every open editor, at once
27
+ }
28
+ send(msg);
29
+ done();
30
+ });
31
+ });
32
+ ```
33
+
34
+ `live-chart.html`:
35
+
36
+ ```html
37
+ <script type="text/javascript" src="node-red-resourceendpoint/node-red-editorcomms/EditorCommsFrontend.js"></script>
38
+
39
+ <script type="text/javascript">
40
+ (function () {
41
+ let open = null; // { id, points } of the live-chart node whose edit dialog is open
42
+
43
+ function draw() {
44
+ const canvas = document.getElementById('live-chart-canvas');
45
+ const min = Math.min(...open.points);
46
+ const span = Math.max(...open.points) - min || 1;
47
+ const ctx = canvas.getContext('2d');
48
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
49
+ ctx.beginPath();
50
+ open.points.forEach((v, i) => ctx.lineTo(i * canvas.width / 99, canvas.height * (1 - (v - min) / span)));
51
+ ctx.stroke();
52
+ }
53
+
54
+ RED.nodes.registerType('live-chart', {
55
+ category: 'function',
56
+ color: '#c7e9c0',
57
+ defaults: { name: { value: '' } },
58
+ inputs: 1,
59
+ outputs: 1,
60
+ label: function () { return this.name || 'live-chart'; },
61
+
62
+ // From the runtime on each message. "this" is the editor node.
63
+ nodeRedEditorComms_subscriptions: {
64
+ value: function (v) {
65
+ if (!open || open.id !== this.id || !open.points) return;
66
+ open.points.push(v);
67
+ if (open.points.length > 100) open.points.shift();
68
+ draw();
69
+ }
70
+ },
71
+
72
+ oneditprepare: function () {
73
+ const state = open = { id: this.id, points: null };
74
+ this.nodeRedEditorComms.publish('history')
75
+ .then((points) => { if (open === state) { state.points = points; draw(); } })
76
+ .catch((err) => $('#live-chart-info').text(err.status === 404 ? 'Deploy the node first' : err.message));
77
+ },
78
+ oneditsave: function () { open = null; },
79
+ oneditcancel: function () { open = null; }
80
+ });
81
+ })();
82
+ </script>
83
+
84
+ <script type="text/html" data-template-name="live-chart">
85
+ <div class="form-row">
86
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
87
+ <input type="text" id="node-input-name" placeholder="Name">
88
+ </div>
89
+ <canvas id="live-chart-canvas" width="460" height="120"></canvas>
90
+ <div id="live-chart-info"></div>
91
+ </script>
92
+ ```
93
+
94
+ The runtime mixin must be in the `createNode` list: its `onRegisterType` serves the editor script, so without it the `<script>` tag in the HTML file gets a 404 and `nodeRedEditorComms` is missing in the editor.
95
+
96
+ Open the edit dialog and the chart shows the values so far. While the dialog is open, each new message adds a point. A node that is not deployed answers 404, and the dialog says so.
97
+
98
+ Runtime data shown in the editor must be set with `.text()` or `textContent`, never with `.html()` or `innerHTML`: a message payload can hold HTML that would run as script in the editor.
99
+
100
+ The first `<script>` tag loads the editor half of this module. The library serves its editor files through [node-red-resourceendpoint](../node-red-resourceendpoint/README.md), at `node-red-resourceendpoint/<name>/<file>` under the editor URL. Here `<name>` is `node-red-editorcomms`. The URL has no leading `/`, so it also works when the editor is not at the root of the server. Load the script one time in each node's HTML file; a second load does nothing.
101
+
102
+ ## API
103
+
104
+ | Side | `publish(type, data)` | `subscribe(type, ...)` |
105
+ | --- | --- | --- |
106
+ | Runtime | goes to this node in every open editor; no answer | `subscribe(type, handler)`: the handler's return value answers the editor's `publish` |
107
+ | Editor | goes to the runtime node with the same id; returns a Promise for the handler's return value | `subscribe(type, [nodeId], cb)`: gets the runtime's `publish` |
108
+
109
+ The runtime does not keep a published message: an editor that opens later does not get it. Publish when the value changes, and let a new editor ask for the current value, as `history` does in the example.
110
+
111
+ ### Runtime: `thisNode.nodeRedEditorComms`
112
+
113
+ - `subscribe(messageType, callback)`: call `callback(payload)` when the editor publishes `messageType` for this node. The return value of `callback` (or the value of its Promise) goes back to the editor as the result of the editor's `publish`. A second `subscribe` with the same type replaces the first.
114
+ - `publish(messageType, [payload])`: send `payload` to this node in every open editor.
115
+
116
+ Both return the API object, so you can chain calls (`thisNode.nodeRedEditorComms.publish('a', 1).publish('b', 2)`).
117
+
118
+ ### Editor: `node.nodeRedEditorComms`
119
+
120
+ The frontend script adds this object to every node in the editor.
121
+
122
+ - `publish(messageType, [payload])`: send to the runtime node with the same id. Returns a native `Promise` that resolves with the return value of the runtime handler (`undefined` if it returns nothing). It rejects with an `Error` that has a `status`; see [Errors](#errors).
123
+ - `subscribe(messageType, callback)`: call `callback(payload)` when the runtime node with the same id publishes `messageType`.
124
+ - `subscribe(messageType, nodeId, callback)`: the same for the runtime node `nodeId`. You need this form only to listen to another node. A second `subscribe` for the same node and type replaces the first. It returns nothing.
125
+
126
+ `nodeRedEditorComms_subscriptions` in the node definition is the short form of `subscribe`. The script registers each entry when a node is added, with `this` bound to the node. Prefer it to `subscribe()`, for the reasons in the next section.
127
+
128
+ ### Limits of editor `subscribe()`
129
+
130
+ - There is no `unsubscribe`. To stop a handler, replace it with a new `subscribe` for the same type, for example with `() => {}`.
131
+ - When you delete a node in the editor, the script removes all of its handlers. If you undo the delete, the entries of `nodeRedEditorComms_subscriptions` come back, but handlers that you added with `subscribe()` do not.
132
+
133
+ ## Errors
134
+
135
+ The Promise of the editor's `publish` rejects with an `Error` that has a `status`:
136
+
137
+ - 404, body `{"status":"no handler"}`: the runtime has no handler for this node and type. The usual cause is a node that is not deployed yet. A node inside a subflow also gets 404: each subflow instance gives its inner nodes new runtime ids that the editor does not know.
138
+ - 500, body `{"error":"<message>"}`: the handler threw or its Promise rejected. `err.message` is the handler's error message. The runtime node also logs `Editor message '<type>' failed: <message>` with `node.error`. It is not a flow message, so a Catch node does not get it.
139
+ - 0: the runtime cannot be reached.
140
+
141
+ Add a `.catch` to each `publish`, also when you ignore the error, so the browser does not report an unhandled rejection.
142
+
143
+ ## Preview or Test button
144
+
145
+ A button in the edit dialog that tries the settings before the user clicks Done:
146
+
147
+ - Read the values from the form fields (`$('#node-input-url').val()`), not from `this`, and send them in the payload. Until Done, `this` holds the saved values.
148
+ - For an expected user error, such as a bad URL, return `{ error: 'text' }` and show it in the dialog. The request resolves, and the Node-RED log stays quiet.
149
+ - A node that is not deployed answers 404. Show "deploy first" for `err.status === 404`.
150
+
151
+ ```js
152
+ // Runtime
153
+ thisNode.nodeRedEditorComms.subscribe('test-url', async ({ url }) => {
154
+ if (!/^https?:\/\//.test(url)) return { error: 'The URL must start with http:// or https://' };
155
+ const res = await fetch(url); // a network failure throws: 500 and a log line
156
+ return { status: res.status };
157
+ });
158
+ ```
159
+
160
+ ## Security
161
+
162
+ Any editor user with the `write` permission can POST any message type with any payload to any node id. Thus treat the payload in a runtime handler as user input: check its type and range before you use it, and do not pass it to a shell, a file path or `eval`. Users with read-only access cannot POST, but they get all messages that the runtime publishes to the editor.
163
+
164
+ [Back to node-red-utils](../README.md)
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ class GpuBuffer {
4
+ constructor(bufferHandle, backend) {
5
+ this._handle = bufferHandle;
6
+ this._backend = backend;
7
+ this._destroyed = false;
8
+ }
9
+
10
+ get view() { return this._handle.view; }
11
+ get count() { return this._handle.count; }
12
+ get dtype() { return this._handle.dtype; }
13
+ get byteLength() { return this._handle.byteLength; }
14
+
15
+ destroy() {
16
+ if (this._destroyed) return;
17
+ this._destroyed = true;
18
+ this._backend.destroyBuffer(this._handle);
19
+ }
20
+
21
+ detach() {
22
+ const ab = this._handle.view.buffer;
23
+ this._handle.view = null;
24
+ this.destroy();
25
+ return ab;
26
+ }
27
+ }
28
+
29
+ module.exports = { GpuBuffer };