@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.
@@ -0,0 +1,653 @@
1
+
2
+ <!--
3
+
4
+ This file was automatically generated using the NODERED Core utility.
5
+ Any modifications to his file will be overwritten the next time the code is regenerated.
6
+
7
+ You have been warned.
8
+
9
+ -->
10
+
11
+ <!-- ClusterConfigNode -->
12
+ <style>
13
+ </style>
14
+ <script type="text/javascript">
15
+ RED.nodes.registerType('ClusterConfigNode', {
16
+ category: 'config',
17
+ label: function() {
18
+ return this.name;
19
+ },
20
+ paletteLabel: 'Cluster Config',
21
+ defaults: {
22
+ name: {
23
+ value: 'My Cluster',
24
+ required: true
25
+ },
26
+ instanceId: {
27
+ value: '',
28
+ required: true
29
+ },
30
+ zkRootPath: {
31
+ value: '/nodered-cluster',
32
+ required: true
33
+ },
34
+ natsAddress: {
35
+ value: 'localhost:4222',
36
+ required: true
37
+ },
38
+ natsUser: {
39
+ value: '',
40
+ required: false
41
+ },
42
+ natsPass: {
43
+ value: '',
44
+ required: false
45
+ },
46
+ role: {
47
+ value: 'Member',
48
+ required: true
49
+ },
50
+ },
51
+ oneditprepare: function() {
52
+ // **** ClusterConfigNode **** //
53
+ {
54
+ let node = this;
55
+ $("#node-config-input-role").typedInput({
56
+ type: "role",
57
+ types: [{
58
+ value: "role",
59
+ options: [{
60
+ value: "Member",
61
+ label: "Member"
62
+ }, {
63
+ value: "Observer",
64
+ label: "Observer"
65
+ }]
66
+ }]
67
+ });
68
+ $("#cluster-test-connection-btn").on("click", function() {
69
+ let $btn = $(this);
70
+ let natsText = $("#cluster-test-nats-text");
71
+ $btn.prop("disabled", true);
72
+ natsText.text("Testing...").css("color", "gray");
73
+ $.ajax({
74
+ url: "cluster/testconnection",
75
+ type: "POST",
76
+ contentType: "application/json",
77
+ data: JSON.stringify({
78
+ natsAddress: $("#node-config-input-natsAddress").val(),
79
+ natsUser: $("#node-config-input-natsUser").val() || undefined,
80
+ natsPass: $("#node-config-input-natsPass").val() || undefined
81
+ }),
82
+ success: function(r) {
83
+ natsText.text((r.nats.ok ? "\u2714 " : "\u2718 ") + (r.nats.message || r.nats.error)).css("color", r.nats.ok ? "green" : "red");
84
+ },
85
+ error: function(xhr) {
86
+ let msg = "Request failed";
87
+ try {
88
+ msg = JSON.parse(xhr.responseText).error || msg;
89
+ } catch (e) {}
90
+ natsText.text("\u2718 " + msg).css("color", "red");
91
+ },
92
+ complete: function() {
93
+ $btn.prop("disabled", false);
94
+ }
95
+ });
96
+ });
97
+ }
98
+ },
99
+ oneditsave: function() {
100
+ // **** ClusterConfigNode **** //
101
+ {
102
+ let node = this;
103
+ }
104
+ },
105
+ oneditcancel: function() {},
106
+ oneditdelete: function() {},
107
+ });
108
+
109
+ </script>
110
+
111
+
112
+ <script type="text/html" data-template-name='ClusterConfigNode'>
113
+ <div id='section_ClusterConfigNode'>
114
+ <div class="form-row">
115
+ <label for="node-config-input-name" class="towb_editorlabel"><i class="fa fa-tag"></i> Name</label>
116
+ <input type="text" id="node-config-input-name" />
117
+ </div>
118
+
119
+ <div class="form-row editorsectionheading">
120
+ <i class="w-16 fa fa-sitemap"></i> <span>Identity</span>
121
+ </div>
122
+
123
+ <div class="editorgroupborder">
124
+ <div class="form-row nomargin">
125
+ <label for="node-config-input-instanceId" class="towb_editorlabel"><i class="fa fa-id-badge"></i> Instance ID</label>
126
+ <input type="text" id="node-config-input-instanceId" placeholder="e.g. field-a, control-center" />
127
+ </div>
128
+ <div class="form-row nomargin">
129
+ <label for="node-config-input-role" class="towb_editorlabel"><i class="fa fa-user-circle"></i> Role</label>
130
+ <input type="text" id="node-config-input-role" />
131
+ </div>
132
+ </div>
133
+
134
+ <div class="form-row editorsectionheading">
135
+ <i class="w-16 fa fa-plug"></i> <span>Connections</span>
136
+ </div>
137
+
138
+ <div class="editorgroupborder">
139
+ <div class="form-row nomargin">
140
+ <label for="node-config-input-natsAddress" class="towb_editorlabel"><i class="fa fa-exchange"></i> NATS Address</label>
141
+ <input type="text" id="node-config-input-natsAddress" placeholder="localhost:4222" />
142
+ </div>
143
+ <div class="form-row nomargin">
144
+ <label for="node-config-input-zkRootPath" class="towb_editorlabel"><i class="fa fa-folder"></i> Root Path</label>
145
+ <input type="text" id="node-config-input-zkRootPath" placeholder="/nodered-cluster" />
146
+ </div>
147
+ <div class="form-row nomargin">
148
+ <label for="node-config-input-natsUser" class="towb_editorlabel"><i class="fa fa-user"></i> Username</label>
149
+ <input type="text" id="node-config-input-natsUser" placeholder="(optional)" autocomplete="off" />
150
+ </div>
151
+ <div class="form-row nomargin">
152
+ <label for="node-config-input-natsPass" class="towb_editorlabel"><i class="fa fa-lock"></i> Password</label>
153
+ <input type="password" id="node-config-input-natsPass" placeholder="(optional)" autocomplete="new-password" />
154
+ </div>
155
+ <div class="form-row nomargin">
156
+ <label class="towb_editorlabel"></label>
157
+ <button type="button" id="cluster-test-connection-btn" class="red-ui-button"><i class="fa fa-plug"></i> Test Connection</button>
158
+ <span id="cluster-test-nats-text" style="font-size: 12px; margin-left: 8px;"></span>
159
+ </div>
160
+ </div>
161
+
162
+ </div>
163
+ </script>
164
+
165
+ <script type="text/markdown" data-help-name='ClusterConfigNode'>
166
+ > **Early development** this package is still in development. APIs and configuration may change.
167
+
168
+ Defines a cluster connection. Each Node-RED instance that participates in a cluster needs one **Cluster Config** node per cluster it belongs to.
169
+
170
+ A single Node-RED instance can belong to multiple clusters simultaneously by creating multiple config nodes with different Root Paths.
171
+
172
+ ### Identity
173
+
174
+ : *name* (string) : Display label for this config node.
175
+ : *instance id* (string) : Unique name for this Node-RED instance within the cluster (e.g. `field-a`, `control-center`). Must be unique across all instances sharing the same Root Path.
176
+ : *role* (select) : **Member** full participation; can publish and subscribe. **Observer** subscribe only; all Cluster Publish nodes linked to this config are silently disabled at runtime.
177
+
178
+ ### Connections
179
+
180
+ : *NATS address* (string) : Address of the NATS server or leaf node (`host:port`). Defaults to `localhost:4222`.
181
+ : *Root Path* (string) : 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.
182
+ : *username* (string) : NATS username for authentication. Leave blank for unauthenticated connections.
183
+ : *password* (string) : NATS password for authentication. Leave blank for unauthenticated connections.
184
+
185
+ ### Status indicators
186
+
187
+ | Colour | Meaning |
188
+ |--------|---------|
189
+ | Yellow ring | Connecting to NATS |
190
+ | Green dot | Connected and registered |
191
+ | Red ring | Connection error or disconnected |
192
+ | Grey ring | Idle (not yet started) |
193
+ </script>
194
+ <!-- ClusterPublishNode -->
195
+ <style>
196
+ </style>
197
+ <script type="text/javascript">
198
+ RED.nodes.registerType('ClusterPublishNode', {
199
+ category: 'cluster',
200
+ icon: 'cluster.png',
201
+ color: '#4A90D9',
202
+ label: function() {
203
+ return this.name;
204
+ },
205
+ paletteLabel: 'Cluster Publish',
206
+ inputs: 1,
207
+ inputLabels: (i) => 'message',
208
+ outputs: 0,
209
+ outputLabels: (i) => [][i],
210
+ defaults: {
211
+ name: {
212
+ value: 'Cluster Publish',
213
+ required: true
214
+ },
215
+ clusterConfig: {
216
+ type: 'ClusterConfigNode',
217
+ required: true
218
+ },
219
+ subject: {
220
+ value: '',
221
+ required: true
222
+ },
223
+ mode: {
224
+ value: 'Durable',
225
+ required: true
226
+ },
227
+ ttl: {
228
+ value: '30',
229
+ required: false
230
+ },
231
+ },
232
+ oneditprepare: function() {
233
+ // **** ClusterPublishNode **** //
234
+ {
235
+ let node = this;
236
+ let modeSelect = $("#node-input-mode");
237
+ let updateTtlVisibility = function() {
238
+ let isEphemeral = modeSelect.typedInput("value") === "Ephemeral";
239
+ $("#cluster-publish-ttl-row").toggle(isEphemeral);
240
+ };
241
+ modeSelect.typedInput({
242
+ type: "mode",
243
+ types: [{
244
+ value: "mode",
245
+ options: [{
246
+ value: "Durable",
247
+ label: "Durable (delivered eventually)"
248
+ }, {
249
+ value: "Ephemeral",
250
+ label: "Ephemeral (discard if stale)"
251
+ }]
252
+ }]
253
+ });
254
+ modeSelect.on("change", updateTtlVisibility);
255
+ updateTtlVisibility();
256
+ // show observer warning if the linked config is in Observer role.
257
+ let clusterConfigInput = $("#node-input-clusterConfig");
258
+ clusterConfigInput.on("change", function() {
259
+ let configId = clusterConfigInput.val();
260
+ let configNode;
261
+ RED.nodes.eachConfig(function(cfg) {
262
+ if (cfg.id === configId) configNode = cfg;
263
+ });
264
+ let isObserver = configNode && configNode.role === "Observer";
265
+ $("#cluster-publish-observer-warning").toggle(!!isObserver);
266
+ });
267
+ }
268
+ },
269
+ oneditsave: function() {
270
+ // **** ClusterPublishNode **** //
271
+ {
272
+ let node = this;
273
+ }
274
+ },
275
+ oneditcancel: function() {},
276
+ oneditdelete: function() {},
277
+ });
278
+
279
+ </script>
280
+
281
+
282
+ <script type="text/html" data-template-name='ClusterPublishNode'>
283
+ <div id='section_ClusterPublishNode'>
284
+ <div class="form-row">
285
+ <label for="node-input-name" class="towb_editorlabel"><i class="fa fa-tag"></i> Name</label>
286
+ <input type="text" id="node-input-name" />
287
+ </div>
288
+
289
+ <div class="form-row nomargin">
290
+ <label for="node-input-clusterConfig" class="towb_editorlabel"><i class="fa fa-sitemap"></i> Cluster</label>
291
+ <input type="text" id="node-input-clusterConfig" placeholder="config" />
292
+ </div>
293
+
294
+ <div id="cluster-publish-observer-warning" style="display:none;" class="form-row">
295
+ <div style="padding: 6px 8px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; color: #856404;">
296
+ <i class="fa fa-exclamation-triangle"></i>
297
+ This node is <strong>disabled</strong> the linked cluster config is set to <strong>Observer</strong> role. Publishing will be silently ignored at runtime.
298
+ </div>
299
+ </div>
300
+
301
+ <div class="form-row editorsectionheading">
302
+ <i class="w-16 fa fa-paper-plane"></i> <span>Publish</span>
303
+ </div>
304
+
305
+ <div class="editorgroupborder">
306
+ <div class="form-row nomargin">
307
+ <label for="node-input-subject" class="towb_editorlabel"><i class="fa fa-tag"></i> Subject</label>
308
+ <input type="text" id="node-input-subject" placeholder="e.g. commands.opengate" />
309
+ </div>
310
+ <div class="form-row nomargin">
311
+ <label for="node-input-mode" class="towb_editorlabel"><i class="fa fa-send"></i> Mode</label>
312
+ <input type="text" id="node-input-mode" />
313
+ </div>
314
+ <div class="form-row nomargin" id="cluster-publish-ttl-row">
315
+ <label for="node-input-ttl" class="towb_editorlabel"><i class="fa fa-clock-o"></i> TTL (seconds)</label>
316
+ <input type="number" id="node-input-ttl" min="1" style="width: 80px;" />
317
+ </div>
318
+ </div>
319
+
320
+ </div>
321
+ </script>
322
+
323
+ <script type="text/markdown" data-help-name='ClusterPublishNode'>
324
+ > **Early development** this package is still in development. APIs and configuration may change.
325
+
326
+ Publishes a Node-RED message to the cluster. Any instance with a matching **Cluster Subscribe** pattern will receive it.
327
+
328
+ If the linked Cluster Config is set to **Observer** role, this node displays a warning in the editor and silently discards all messages at runtime.
329
+
330
+ ### Properties
331
+
332
+ : *cluster* (config) : The Cluster Config node that defines which cluster to publish to.
333
+ : *subject* (string) : Subject identifier for this publisher e.g. `commands.opengate` or `sensors.temperature`. Dot-notation is supported.
334
+ : *mode* (select) : **Durable** persisted in JetStream; delivered even if the subscriber is offline. **Ephemeral** discarded if not consumed within the TTL window; use for time-sensitive data.
335
+ : *TTL* (number) : Ephemeral mode only. Seconds before an undelivered message is discarded. Default: 30.
336
+
337
+ ### Subject routing
338
+
339
+ The full NATS subject is composed automatically as `<rootPath>.<instanceId>.<subject>`.
340
+
341
+ Example Root Path `/nodered-cluster`, Instance ID `field-a`, Subject `commands.opengate`:
342
+
343
+ ```
344
+ nodered-cluster.field-a.commands.opengate
345
+ ```
346
+
347
+ Subscribers use wildcards to match across instances:
348
+ - `*.commands.opengate` receives this subject from any instance
349
+ - `field-a.>` receives all subjects from `field-a`
350
+
351
+ ### Input
352
+
353
+ : *msg.payload* : Serialised as JSON and published to the cluster subject.
354
+ </script>
355
+ <!-- ClusterSubscribeNode -->
356
+ <style>
357
+ .cluster-sub-ac-dropdown {
358
+ position: fixed;
359
+ background: var(--red-ui-primary-background, #fff);
360
+ border: 1px solid var(--red-ui-form-input-border-color, #ccc);
361
+ border-top: none;
362
+ border-radius: 0 0 4px 4px;
363
+ max-height: 220px;
364
+ overflow-y: auto;
365
+ z-index: 2000;
366
+ display: none;
367
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
368
+ }
369
+
370
+ .cluster-sub-ac-item {
371
+ padding: 5px 10px;
372
+ cursor: pointer;
373
+ display: flex;
374
+ justify-content: space-between;
375
+ align-items: center;
376
+ font-size: 12px;
377
+ border-bottom: 1px solid var(--red-ui-secondary-border-color, #eee);
378
+ }
379
+
380
+ .cluster-sub-ac-item:last-child {
381
+ border-bottom: none;
382
+ }
383
+
384
+ .cluster-sub-ac-item:hover,
385
+ .cluster-sub-ac-item.cluster-sub-ac-active {
386
+ background: var(--red-ui-secondary-background-selected, #e5f3ff);
387
+ }
388
+
389
+ .cluster-sub-ac-value {
390
+ font-family: monospace;
391
+ font-weight: 500;
392
+ }
393
+
394
+ .cluster-sub-ac-desc {
395
+ color: var(--red-ui-tertiary-text-color, #999);
396
+ font-size: 11px;
397
+ white-space: nowrap;
398
+ margin-left: 8px;
399
+ }
400
+
401
+ </style>
402
+ <script type="text/javascript">
403
+ RED.nodes.registerType('ClusterSubscribeNode', {
404
+ category: 'cluster',
405
+ icon: 'cluster.png',
406
+ color: '#4A90D9',
407
+ label: function() {
408
+ return this.name;
409
+ },
410
+ paletteLabel: 'Cluster Subscribe',
411
+ inputs: 0,
412
+ inputLabels: (i) => 'undefined',
413
+ outputs: 1,
414
+ outputLabels: (i) => ['message'][i],
415
+ defaults: {
416
+ name: {
417
+ value: 'Cluster Subscribe',
418
+ required: true
419
+ },
420
+ clusterConfig: {
421
+ type: 'ClusterConfigNode',
422
+ required: true
423
+ },
424
+ subjectPattern: {
425
+ value: '',
426
+ required: true
427
+ },
428
+ },
429
+ oneditprepare: function() {
430
+ // **** ClusterSubscribeNode **** //
431
+ {
432
+ let node = this;
433
+ let _suggestions = [];
434
+ let _currentMatches = [];
435
+ let _activeIdx = -1;
436
+ // Clean up any leftover dropdown from a previous edit
437
+ $('.cluster-sub-ac-dropdown').remove();
438
+ let $input = $('#node-input-subjectPattern');
439
+ let $dropdown = $('<div class="cluster-sub-ac-dropdown">').appendTo('body');
440
+ // ── Build suggestion list from discovery data ─────────────────────────────
441
+ function buildSuggestions(data) {
442
+ let result = [];
443
+ let endpoints = data.endpoints || {};
444
+ let allSubjects = new Set();
445
+ Object.values(endpoints).forEach(function(subjects) {
446
+ subjects.forEach(function(s) {
447
+ allSubjects.add(s);
448
+ });
449
+ });
450
+ result.push({
451
+ value: '>',
452
+ desc: 'all messages'
453
+ });
454
+ [...allSubjects].sort().forEach(function(subject) {
455
+ result.push({
456
+ value: '*.' + subject,
457
+ desc: 'any instance'
458
+ });
459
+ });
460
+ Object.keys(endpoints).sort().forEach(function(instanceId) {
461
+ result.push({
462
+ value: instanceId + '.>',
463
+ desc: 'all from ' + instanceId
464
+ });
465
+ (endpoints[instanceId] || []).sort().forEach(function(subject) {
466
+ result.push({
467
+ value: instanceId + '.' + subject,
468
+ desc: instanceId
469
+ });
470
+ });
471
+ });
472
+ return result;
473
+ }
474
+ // ── Token helpers ─────────────────────────────────────────────────────────
475
+ function getCurrentToken() {
476
+ let parts = ($input.val() || '').split(',');
477
+ return parts[parts.length - 1].trimStart();
478
+ }
479
+
480
+ function replaceCurrentToken(value) {
481
+ let val = $input.val() || '';
482
+ let commaIdx = val.lastIndexOf(',');
483
+ $input.val(commaIdx === -1 ? value : val.substring(0, commaIdx) + ', ' + value);
484
+ $input.focus();
485
+ }
486
+
487
+ function filterSuggestions(token) {
488
+ let lower = token.toLowerCase();
489
+ let pool = token.length === 0 ? _suggestions : _suggestions.filter(function(s) {
490
+ return s.value.toLowerCase().startsWith(lower);
491
+ });
492
+ return pool.slice(0, 12);
493
+ }
494
+ // ── Dropdown rendering ────────────────────────────────────────────────────
495
+ function showDropdown(matches) {
496
+ _currentMatches = matches;
497
+ _activeIdx = -1;
498
+ $dropdown.empty();
499
+ if (matches.length === 0) {
500
+ $dropdown.hide();
501
+ return;
502
+ }
503
+ matches.forEach(function(s) {
504
+ $('<div class="cluster-sub-ac-item">').append($('<span class="cluster-sub-ac-value">').text(s.value)).append($('<span class="cluster-sub-ac-desc">').text(s.desc)).on('mousedown', function(e) {
505
+ e.preventDefault();
506
+ replaceCurrentToken(s.value);
507
+ $dropdown.hide();
508
+ _activeIdx = -1;
509
+ }).appendTo($dropdown);
510
+ });
511
+ let rect = $input[0].getBoundingClientRect();
512
+ $dropdown.css({
513
+ top: rect.bottom + 'px',
514
+ left: rect.left + 'px',
515
+ width: rect.width + 'px'
516
+ }).show();
517
+ }
518
+ // ── Input event handlers ──────────────────────────────────────────────────
519
+ $input.on('input', function() {
520
+ let matches = filterSuggestions(getCurrentToken());
521
+ matches.length > 0 ? showDropdown(matches) : $dropdown.hide();
522
+ });
523
+ $input.on('focus', function() {
524
+ if (_suggestions.length > 0) showDropdown(filterSuggestions(getCurrentToken()));
525
+ });
526
+ $input.on('blur', function() {
527
+ setTimeout(function() {
528
+ $dropdown.hide();
529
+ }, 150);
530
+ });
531
+ $input.on('keydown', function(e) {
532
+ if ($dropdown.is(':hidden')) {
533
+ if (e.key === 'ArrowDown' && _suggestions.length > 0) {
534
+ showDropdown(filterSuggestions(getCurrentToken()));
535
+ }
536
+ return;
537
+ }
538
+ let items = $dropdown.find('.cluster-sub-ac-item');
539
+ if (e.key === 'ArrowDown') {
540
+ e.preventDefault();
541
+ _activeIdx = Math.min(_activeIdx + 1, _currentMatches.length - 1);
542
+ items.removeClass('cluster-sub-ac-active').eq(_activeIdx).addClass('cluster-sub-ac-active');
543
+ items[_activeIdx] && items[_activeIdx].scrollIntoView({
544
+ block: 'nearest'
545
+ });
546
+ } else if (e.key === 'ArrowUp') {
547
+ e.preventDefault();
548
+ _activeIdx = Math.max(_activeIdx - 1, 0);
549
+ items.removeClass('cluster-sub-ac-active').eq(_activeIdx).addClass('cluster-sub-ac-active');
550
+ items[_activeIdx] && items[_activeIdx].scrollIntoView({
551
+ block: 'nearest'
552
+ });
553
+ } else if ((e.key === 'Enter' || e.key === 'Tab') && _activeIdx >= 0) {
554
+ e.preventDefault();
555
+ replaceCurrentToken(_currentMatches[_activeIdx].value);
556
+ $dropdown.hide();
557
+ _activeIdx = -1;
558
+ } else if (e.key === 'Escape') {
559
+ $dropdown.hide();
560
+ _activeIdx = -1;
561
+ }
562
+ });
563
+ // ── Fetch discovery data ──────────────────────────────────────────────────
564
+ let configId = node.clusterConfig;
565
+ if (configId) {
566
+ $.ajax({
567
+ url: 'cluster/discovery/' + configId,
568
+ type: 'GET',
569
+ success: function(data) {
570
+ _suggestions = buildSuggestions(data);
571
+ },
572
+ error: function() {
573
+ console.warn('[ClusterSubscribe] Discovery unavailable autocomplete disabled');
574
+ }
575
+ });
576
+ }
577
+ }
578
+ },
579
+ oneditsave: function() {
580
+ // **** ClusterSubscribeNode **** //
581
+ {
582
+ let node = this;
583
+ $('.cluster-sub-ac-dropdown').remove();
584
+ }
585
+ },
586
+ oneditcancel: function() {},
587
+ oneditdelete: function() {},
588
+ });
589
+
590
+ </script>
591
+
592
+
593
+ <script type="text/html" data-template-name='ClusterSubscribeNode'>
594
+ <div id='section_ClusterSubscribeNode'>
595
+ <div class="form-row">
596
+ <label for="node-input-name" class="towb_editorlabel"><i class="fa fa-tag"></i> Name</label>
597
+ <input type="text" id="node-input-name" />
598
+ </div>
599
+
600
+ <div class="form-row nomargin">
601
+ <label for="node-input-clusterConfig" class="towb_editorlabel"><i class="fa fa-sitemap"></i> Cluster</label>
602
+ <input type="text" id="node-input-clusterConfig" placeholder="config" />
603
+ </div>
604
+
605
+ <div class="form-row editorsectionheading">
606
+ <i class="w-16 fa fa-inbox"></i> <span>Subscribe</span>
607
+ </div>
608
+
609
+ <div class="editorgroupborder">
610
+ <div class="form-row nomargin">
611
+ <label for="node-input-subjectPattern" class="towb_editorlabel"><i class="fa fa-filter"></i> Subject Pattern</label>
612
+ <input type="text" id="node-input-subjectPattern" placeholder="e.g. *.commands.opengate, *.events.closegate" autocomplete="off" />
613
+ </div>
614
+ </div>
615
+
616
+ </div>
617
+ </script>
618
+
619
+ <script type="text/markdown" data-help-name='ClusterSubscribeNode'>
620
+ > **Early development** this package is still in development. APIs and configuration may change.
621
+
622
+ 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.
623
+
624
+ ### Properties
625
+
626
+ : *cluster* (config) : The Cluster Config node that defines which cluster to subscribe to.
627
+ : *subject pattern* (string) : One or more subject patterns, comma-separated. Each pattern creates an independent subscription. Supports NATS wildcards.
628
+
629
+ ### Subject pattern syntax
630
+
631
+ Patterns are scoped to the cluster's Root Path automatically you only write the part after the root prefix.
632
+
633
+ | Pattern | Matches |
634
+ |---------|---------|
635
+ | `*.commands.opengate` | Open-gate command from **any** instance |
636
+ | `field-a.sensors.temperature` | Temperature from `field-a` only |
637
+ | `field-a.>` | **All** messages from `field-a` |
638
+ | `>` | Every message in the cluster |
639
+ | `*.commands.opengate, *.commands.closegate` | Both gate commands from any instance |
640
+
641
+ `*` matches a single subject token. `>` matches one or more tokens and must be the last token in the pattern.
642
+
643
+ ### Autocomplete
644
+
645
+ When the editor opens, the node fetches live discovery data from the cluster and populates an autocomplete dropdown as you type. Suggestions are grouped from broadest to most specific. Use arrow keys to navigate, Enter or Tab to select.
646
+
647
+ ### Outputs
648
+
649
+ : *msg.payload* : The payload published by the remote Cluster Publish node.
650
+ : *msg.topic* : The full NATS subject the message arrived on use this to identify which instance sent the message.
651
+ </script>
652
+
653
+
package/build/Nodes.js ADDED
@@ -0,0 +1,22 @@
1
+
2
+ /*
3
+
4
+ This file was automatically generated using the NODERED Core utility.
5
+ Any modifications to his file will be overwritten the next time the code is regenerated.
6
+
7
+ You have been warned.
8
+
9
+ */
10
+ "use strict";
11
+ const NodeManager = require("@theotherwillembotha/node-red-plugincore").NodeManager;
12
+ const ClusterConfigNode = require("@theotherwillembotha/node-red-cluster").ClusterConfigNode;
13
+ const ClusterPublishNode = require("@theotherwillembotha/node-red-cluster").ClusterPublishNode;
14
+ const ClusterSubscribeNode = require("@theotherwillembotha/node-red-cluster").ClusterSubscribeNode;
15
+ module.exports = (RED) => {
16
+ let manager = new NodeManager(RED);
17
+ manager.registerNodeType("ClusterConfigNode", ClusterConfigNode);
18
+ manager.registerNodeType("ClusterPublishNode", ClusterPublishNode);
19
+ manager.registerNodeType("ClusterSubscribeNode", ClusterSubscribeNode);
20
+ }
21
+
22
+