@theotherwillembotha/node-red-cluster 0.0.55
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/LICENSE +15 -0
- package/README.md +337 -0
- package/build/GenerateNodes.js +19 -0
- package/build/Nodes.html +653 -0
- package/build/Nodes.js +22 -0
- package/build/Plugins.js +70 -0
- package/build/cluster/node/ClusterConfigNode.js +89 -0
- package/build/cluster/node/ClusterPublishNode.js +118 -0
- package/build/cluster/node/ClusterSubscribeNode.js +104 -0
- package/build/cluster/service/ClusterClient.js +149 -0
- package/build/cluster/service/ClusterService.js +160 -0
- package/build/icons/cluster.png +0 -0
- package/build/index.js +19 -0
- package/documentation/ClusterConfigNode.png +0 -0
- package/documentation/ClusterPublishNode.png +0 -0
- package/documentation/ClusterSubscribeNode.png +0 -0
- package/documentation/cluster_example_publish.png +0 -0
- package/documentation/cluster_example_subscribe.png +0 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Willem Botha (@theotherwillembotha)
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# @theotherwillembotha/node-red-cluster
|
|
2
|
+
|
|
3
|
+
Connect multiple Node-RED instances into a coordinated cluster. Instances discover each other, publish messages on named subjects, and subscribe to messages from other instances - with full delivery guarantees even when nodes are temporarily offline. Built on [@theotherwillembotha/node-red-plugincore](https://github.com/theotherwillembotha/nodered_plugincore) and powered by [NATS JetStream](https://docs.nats.io/nats-concepts/jetstream).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> **This plugin requires [`@theotherwillembotha/node-red-plugincore`](https://github.com/theotherwillembotha/nodered_plugincore) to be installed.**
|
|
9
|
+
>
|
|
10
|
+
> `node-red-plugincore` is declared as a dependency and npm will install it automatically. However, due to a [known Node-RED limitation](https://github.com/node-red/node-red/issues/3529), packages that arrive as transitive npm dependencies are only discovered by the Node-RED runtime on the **next startup**.
|
|
11
|
+
>
|
|
12
|
+
> **Two options:**
|
|
13
|
+
> - Install [`@theotherwillembotha/node-red-plugincore`](https://flows.nodered.org/node/@theotherwillembotha/node-red-plugincore) via the palette manager or `npm install` **first**, then install this plugin.
|
|
14
|
+
> - Install this plugin directly, then **restart Node-RED once** and both packages will be fully loaded.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
> **Early development** - this package is still in development. APIs and configuration may change between releases.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## What it does
|
|
23
|
+
|
|
24
|
+
Each Node-RED instance in the cluster connects to a NATS server. A **Cluster Publish** node sends a message onto a named subject; any other instance with a matching **Cluster Subscribe** node receives it. Subjects are automatically namespaced by instance, so you can subscribe to `field-a.sensors.temperature` for data from one specific instance, or `*.sensors.temperature` to receive it from all of them simultaneously.
|
|
25
|
+
|
|
26
|
+
Messages published in **Durable** mode are stored in NATS JetStream. If a subscriber is offline when the message is sent, it will be delivered when the subscriber comes back - nothing is lost. **Ephemeral** mode is fire-and-forget with a configurable TTL, useful for time-sensitive data that becomes irrelevant if not consumed quickly.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## NATS - what it is and why you need it
|
|
31
|
+
|
|
32
|
+
NATS is a lightweight, high-performance messaging server. Think of it as the backbone that all your Node-RED instances connect to: when one instance publishes a message, NATS routes it to all the instances that have subscribed to that subject.
|
|
33
|
+
|
|
34
|
+
**You do not need to understand NATS deeply to use this plugin.** You just need a NATS server running somewhere that all your Node-RED instances can reach. The sections below show you exactly how to do that.
|
|
35
|
+
|
|
36
|
+
### JetStream
|
|
37
|
+
|
|
38
|
+
JetStream is NATS's persistence layer. Without it, messages are delivered only if a subscriber is listening at the exact moment the message is sent. With JetStream, messages are written to disk and replayed to subscribers when they reconnect. This plugin uses JetStream for all Durable-mode messages, so **JetStream must be enabled** on your NATS server.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Setting up NATS
|
|
43
|
+
|
|
44
|
+
### Option 1 - Single server (simplest, good for getting started)
|
|
45
|
+
|
|
46
|
+
A single NATS server is the easiest way to get going. It has no redundancy - if NATS goes down, all cluster communication stops until it restarts - but it is perfectly adequate for development and non-critical deployments.
|
|
47
|
+
|
|
48
|
+
**`nats.conf`**
|
|
49
|
+
```
|
|
50
|
+
listen=0.0.0.0:4222
|
|
51
|
+
http=0.0.0.0:8222
|
|
52
|
+
|
|
53
|
+
jetstream {
|
|
54
|
+
store_dir=/data/storage
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
accounts {
|
|
58
|
+
APP {
|
|
59
|
+
jetstream: enabled
|
|
60
|
+
users: [
|
|
61
|
+
{ user: "nodered", password: "your_password_here" }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
$SYS {}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> To generate a bcrypt-hashed password (recommended for anything beyond local dev):
|
|
69
|
+
> ```bash
|
|
70
|
+
> docker run --rm nats nats-server --mkpasswd
|
|
71
|
+
> ```
|
|
72
|
+
|
|
73
|
+
**`docker-compose.yml`**
|
|
74
|
+
```yaml
|
|
75
|
+
version: "3.5"
|
|
76
|
+
services:
|
|
77
|
+
nats:
|
|
78
|
+
image: nats
|
|
79
|
+
hostname: nats
|
|
80
|
+
command: "-config /data/nats.conf"
|
|
81
|
+
ports:
|
|
82
|
+
- "4222:4222" # client connections
|
|
83
|
+
- "8222:8222" # monitoring dashboard → http://localhost:8222
|
|
84
|
+
volumes:
|
|
85
|
+
- ./nats:/data
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Start it:
|
|
89
|
+
```bash
|
|
90
|
+
docker compose up -d
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
In your **Cluster Config** node, set the NATS Address to `localhost:4222` and enter the username and password from your config.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### Option 2 - Three-server cluster (high availability)
|
|
98
|
+
|
|
99
|
+
A clustered NATS setup runs multiple servers that synchronise with each other. If one server goes down, the others keep routing messages. Node-RED instances can connect to any server in the cluster - NATS handles the routing internally.
|
|
100
|
+
|
|
101
|
+
Each server needs its own config file. The only differences between them are the `server_name` and the `routes` list (each server points to the *other* two).
|
|
102
|
+
|
|
103
|
+
**`nats/server1/nats.conf`**
|
|
104
|
+
```
|
|
105
|
+
server_name=nats_server1
|
|
106
|
+
listen=0.0.0.0:4222
|
|
107
|
+
http=0.0.0.0:8222
|
|
108
|
+
|
|
109
|
+
jetstream {
|
|
110
|
+
store_dir=/data/storage
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
accounts {
|
|
114
|
+
APP {
|
|
115
|
+
jetstream: enabled
|
|
116
|
+
users: [
|
|
117
|
+
{ user: "nodered", password: "your_password_here" }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
$SYS {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
cluster {
|
|
124
|
+
name: NATS
|
|
125
|
+
listen: 0.0.0.0:6222
|
|
126
|
+
|
|
127
|
+
# Credentials required from peer servers connecting to this server's route port.
|
|
128
|
+
authorization {
|
|
129
|
+
user: route_user
|
|
130
|
+
password: "route_secret"
|
|
131
|
+
timeout: 2
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
routes: [
|
|
135
|
+
nats://route_user:route_secret@nats_server2:6222
|
|
136
|
+
nats://route_user:route_secret@nats_server3:6222
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
For `server2` and `server3`, use identical configs but change `server_name` and swap the route URLs so each server points to its two peers.
|
|
142
|
+
|
|
143
|
+
**`nats-compose.yml`**
|
|
144
|
+
```yaml
|
|
145
|
+
version: "3.5"
|
|
146
|
+
services:
|
|
147
|
+
nats_server1:
|
|
148
|
+
image: nats
|
|
149
|
+
hostname: nats_server1
|
|
150
|
+
command: "-config /data/nats.conf"
|
|
151
|
+
ports:
|
|
152
|
+
- "4222:4222"
|
|
153
|
+
- "8222:8222"
|
|
154
|
+
volumes:
|
|
155
|
+
- ./nats/server1:/data
|
|
156
|
+
|
|
157
|
+
nats_server2:
|
|
158
|
+
image: nats
|
|
159
|
+
hostname: nats_server2
|
|
160
|
+
command: "-config /data/nats.conf"
|
|
161
|
+
ports:
|
|
162
|
+
- "4223:4222"
|
|
163
|
+
- "8223:8222"
|
|
164
|
+
volumes:
|
|
165
|
+
- ./nats/server2:/data
|
|
166
|
+
depends_on: ["nats_server1"]
|
|
167
|
+
|
|
168
|
+
nats_server3:
|
|
169
|
+
image: nats
|
|
170
|
+
hostname: nats_server3
|
|
171
|
+
command: "-config /data/nats.conf"
|
|
172
|
+
ports:
|
|
173
|
+
- "4224:4222"
|
|
174
|
+
- "8224:8222"
|
|
175
|
+
volumes:
|
|
176
|
+
- ./nats/server3:/data
|
|
177
|
+
depends_on: ["nats_server1"]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Start it:
|
|
181
|
+
```bash
|
|
182
|
+
docker compose -f nats-compose.yml up -d
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Each server is reachable on a different host port (`4222`, `4223`, `4224`) - connect any Node-RED instance to whichever one is nearest or most reliable. The NATS cluster handles message routing between them transparently.
|
|
186
|
+
|
|
187
|
+
> A ready-to-run three-server cluster configuration matching this layout is included in the `nats/` and `nats-compose.yml` files in this repository.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Installation
|
|
192
|
+
|
|
193
|
+
In your Node-RED user directory (typically `~/.node-red`):
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npm install @theotherwillembotha/node-red-cluster
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Or via the Node-RED **Manage Palette** - search for `node-red-cluster`.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Nodes
|
|
204
|
+
|
|
205
|
+
### Cluster Config
|
|
206
|
+
|
|
207
|
+
Defines a cluster connection. Add one per cluster per Node-RED instance. All Publish and Subscribe nodes reference a config node to know which cluster they belong to.
|
|
208
|
+
|
|
209
|
+
A single Node-RED instance can participate in more than one cluster simultaneously by adding multiple config nodes with different Root Paths.
|
|
210
|
+
|
|
211
|
+

|
|
212
|
+
|
|
213
|
+
| Property | Description |
|
|
214
|
+
|----------|-------------|
|
|
215
|
+
| **Name** | Display label for this config node. |
|
|
216
|
+
| **Instance ID** | Unique name for this Node-RED instance within the cluster - e.g. `field-a` or `control-center`. Must be unique across all nodes sharing the same Root Path. |
|
|
217
|
+
| **Role** | **Member** - full participation; can publish and subscribe. **Observer** - subscribe only; all Cluster Publish nodes linked to this config are silently disabled at runtime. |
|
|
218
|
+
| **NATS Address** | `host:port` of your NATS server or cluster entry point. Default: `localhost:4222`. |
|
|
219
|
+
| **Root Path** | Path prefix that identifies this cluster - e.g. `/nodered-cluster`. All instances in the same cluster must use the same value. Multiple independent clusters can share a NATS server by using different Root Paths. |
|
|
220
|
+
| **Username / Password** | NATS credentials. Leave blank for unauthenticated servers. |
|
|
221
|
+
|
|
222
|
+
The **Test Connection** button verifies NATS connectivity using the address and credentials currently entered in the form - before saving.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### Cluster Publish
|
|
227
|
+
|
|
228
|
+
Publishes the incoming Node-RED message to the cluster when triggered. Other instances with a matching Cluster Subscribe pattern will receive it.
|
|
229
|
+
|
|
230
|
+

|
|
231
|
+
|
|
232
|
+
| Property | Description |
|
|
233
|
+
|----------|-------------|
|
|
234
|
+
| **Cluster** | The Cluster Config node that defines which cluster to publish to. |
|
|
235
|
+
| **Subject** | Subject identifier for this publisher - e.g. `commands.opengate` or `sensors.temperature`. Dot-notation is supported for hierarchy. |
|
|
236
|
+
| **Mode** | **Durable** - message is persisted in JetStream and delivered even if the subscriber is offline at the time of publishing. **Ephemeral** - message is discarded if not consumed within the TTL window; use for data that becomes stale quickly. |
|
|
237
|
+
| **TTL** | Ephemeral mode only. Seconds before an undelivered message is discarded. Default: 30. |
|
|
238
|
+
|
|
239
|
+
The full NATS subject is composed automatically:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
<root-path>.<instance-id>.<subject>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
For example: Root Path `/nodered-cluster`, Instance ID `field-a`, Subject `commands.opengate` → `nodered-cluster.field-a.commands.opengate`
|
|
246
|
+
|
|
247
|
+
If the linked Cluster Config is set to **Observer** role, the node displays a warning in the editor and silently discards all messages at runtime.
|
|
248
|
+
|
|
249
|
+
**Input**
|
|
250
|
+
|
|
251
|
+
| Property | Description |
|
|
252
|
+
|----------|-------------|
|
|
253
|
+
| `msg.payload` | Serialised as JSON and published to the cluster subject. |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### Cluster Subscribe
|
|
258
|
+
|
|
259
|
+
Subscribes to one or more cluster subjects and emits a Node-RED message each time a matching message arrives from any instance in the cluster.
|
|
260
|
+
|
|
261
|
+

|
|
262
|
+
|
|
263
|
+
| Property | Description |
|
|
264
|
+
|----------|-------------|
|
|
265
|
+
| **Cluster** | The Cluster Config node that defines which cluster to subscribe to. |
|
|
266
|
+
| **Subject Pattern** | One or more subject patterns, comma-separated. Supports NATS wildcards. |
|
|
267
|
+
|
|
268
|
+
**Output**
|
|
269
|
+
|
|
270
|
+
| Property | Description |
|
|
271
|
+
|----------|-------------|
|
|
272
|
+
| `msg.payload` | The payload published by the remote Cluster Publish node. |
|
|
273
|
+
| `msg.topic` | The full NATS subject the message arrived on - useful for identifying which instance sent it. |
|
|
274
|
+
|
|
275
|
+
#### Subject pattern syntax
|
|
276
|
+
|
|
277
|
+
Patterns are scoped to the cluster's Root Path automatically. You only write the part *after* the root:
|
|
278
|
+
|
|
279
|
+
| Pattern | Matches |
|
|
280
|
+
|---------|---------|
|
|
281
|
+
| `*.commands.opengate` | Open-gate commands from **any** instance |
|
|
282
|
+
| `field-a.sensors.temperature` | Temperature readings from `field-a` only |
|
|
283
|
+
| `field-a.>` | **All** messages from `field-a` |
|
|
284
|
+
| `>` | Every message in the cluster |
|
|
285
|
+
| `*.commands.opengate, *.commands.closegate` | Both gate commands from any instance (two subscriptions) |
|
|
286
|
+
|
|
287
|
+
#### Live autocomplete
|
|
288
|
+
|
|
289
|
+
When the editor panel opens, the Cluster Subscribe node fetches a live list of all currently connected instances and the subjects they are publishing. Start typing in the Subject Pattern field - the dropdown shows matching suggestions grouped by specificity:
|
|
290
|
+
|
|
291
|
+
- `>` - match everything
|
|
292
|
+
- `*.subject` - any instance, specific subject
|
|
293
|
+
- `instance-id.>` - all subjects from one instance
|
|
294
|
+
- `instance-id.subject` - exact match
|
|
295
|
+
|
|
296
|
+
Use arrow keys to navigate, Enter or Tab to select. After selecting a suggestion, type `, ` to add a second pattern.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Example flows
|
|
301
|
+
|
|
302
|
+
### Publishing sensor data
|
|
303
|
+
|
|
304
|
+

|
|
305
|
+
|
|
306
|
+
An Inject node sends a payload on activation to the subject `test` in Durable mode. The message is stored in NATS JetStream under `<clustername>.<memberid>.test` and delivered to any subscriber that matches.
|
|
307
|
+
|
|
308
|
+
### Subscribing to sensor data
|
|
309
|
+
|
|
310
|
+

|
|
311
|
+
|
|
312
|
+
A Cluster Subscribe node with pattern `*.test` receives all messages from all instances in the cluster that contains the subject `test`. The subject pattern `*.test` expands to `*.*.test.heartbeat` at runtime - the `*` wildcard matches any single token, so messages from `memberA`, `memberB`, and any other instance are all delivered to this one node.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Subject routing reference
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
Published as: nodered-cluster . memberA . test
|
|
320
|
+
└── root path ──┘ └─ id ─┘ └ subject ┘
|
|
321
|
+
|
|
322
|
+
Subscribe with: nodered-cluster . * . test ← any instance
|
|
323
|
+
nodered-cluster . memberA . > ← all from memberA
|
|
324
|
+
nodered-cluster . > ← everything
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## References
|
|
330
|
+
|
|
331
|
+
- [NATS documentation](https://docs.nats.io)
|
|
332
|
+
- [NATS JetStream](https://docs.nats.io/nats-concepts/jetstream)
|
|
333
|
+
- [node-red-plugincore](https://github.com/theotherwillembotha/nodered_plugincore)
|
|
334
|
+
|
|
335
|
+
## License
|
|
336
|
+
|
|
337
|
+
[ISC](LICENSE)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_red_plugincore_1 = require("@theotherwillembotha/node-red-plugincore");
|
|
4
|
+
// services.
|
|
5
|
+
const ClusterService_1 = require("./cluster/service/ClusterService");
|
|
6
|
+
// nodes.
|
|
7
|
+
const ClusterConfigNode_1 = require("./cluster/node/ClusterConfigNode");
|
|
8
|
+
const ClusterPublishNode_1 = require("./cluster/node/ClusterPublishNode");
|
|
9
|
+
const ClusterSubscribeNode_1 = require("./cluster/node/ClusterSubscribeNode");
|
|
10
|
+
new node_red_plugincore_1.NodeGenerator("./src/")
|
|
11
|
+
// services.
|
|
12
|
+
.registerService(ClusterService_1.ClusterService)
|
|
13
|
+
// nodes
|
|
14
|
+
.registerNode(ClusterConfigNode_1.ClusterConfigNode)
|
|
15
|
+
.registerNode(ClusterPublishNode_1.ClusterPublishNode)
|
|
16
|
+
.registerNode(ClusterSubscribeNode_1.ClusterSubscribeNode)
|
|
17
|
+
// done.
|
|
18
|
+
.generate("./build/Nodes", "./build/Plugins");
|
|
19
|
+
process.exit(0);
|