@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 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
+ ![Cluster Config Node](documentation/ClusterConfigNode.png)
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
+ ![Cluster Publish Node](documentation/ClusterPublishNode.png)
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
+ ![Cluster Subscribe Node](documentation/ClusterSubscribeNode.png)
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
+ ![Publish example](documentation/cluster_example_publish.png)
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
+ ![Subscribe example](documentation/cluster_example_subscribe.png)
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);