node-red-contrib-join-wait 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/join-wait.html CHANGED
@@ -1,34 +1,33 @@
1
1
  <script type="text/x-red" data-template-name="join-wait">
2
2
  <div class="form-row">
3
- <label for="node-input-pathTopic"><i class="fa fa-bookmark-o"></i> Paths topic</label>
4
- <input type="text" id="node-input-pathTopic" placeholder="topic">
5
- <input type="hidden" id="node-input-pathTopicType">
6
- </div>
7
- <div class="form-row">
8
- <label for="node-input-paths"><i class="fa fa-code-fork"></i> Paths (Wait)</label>
9
- <input type="text" id="node-input-paths" placeholder="[&quot;path_1&quot;, &quot;path_2&quot;]">
3
+ <label for="node-input-name" title="Display name for this node on the canvas."><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-input-name" placeholder="Name">
10
5
  </div>
6
+
11
7
  <div class="form-row">
12
- <label for="node-input-pathsToExpire"><i class="fa fa-code-fork"></i> Paths (Expire)</label>
13
- <input type="text" id="node-input-pathsToExpire" placeholder="[&quot;not_path_3&quot;, &quot;not_path_4&quot;]">
8
+ <label for="node-input-pathTopic" title="Message property whose value identifies the incoming path."><i class="fa fa-bookmark-o"></i> Path field</label>
9
+ <input type="text" id="node-input-pathTopic" placeholder="topic">
10
+ <input type="hidden" id="node-input-pathTopicType">
14
11
  </div>
15
- <div class="form-row">
16
- <label for="node-input-useRegex">Use regex</label>
17
- <label for="node-input-useRegex" style="width:70%">
18
- <input type="checkbox" id="node-input-useRegex" style="display:inline-block; width:22px; vertical-align:baseline;">Interpret path names as regex</label>
12
+ <div class="form-tips" style="margin-bottom:12px">
13
+ The message property whose value identifies an incoming path —
14
+ e.g. <code>msg.topic</code> or <code>msg.paths</code>. May be a string
15
+ or an object with multiple keys.
19
16
  </div>
17
+
20
18
  <div class="form-row">
21
- <label for="node-input-warnUnmatched">Unmatched paths</label>
22
- <label for="node-input-warnUnmatched" style="width:70%">
23
- <input type="checkbox" id="node-input-warnUnmatched" style="display:inline-block; width:22px; vertical-align:baseline;">Warn on unmatched path</label>
19
+ <label title="Path names to wait for; repeat an entry to require it n times."><i class="fa fa-code-fork"></i> Wait paths</label>
20
+ <ol id="node-input-paths-container"></ol>
24
21
  </div>
25
- <div class="form-row">
26
- <label for="node-input-correlationTopic"><i class="fa fa-bookmark-o"></i> Correlation topic</label>
27
- <input type="text" id="node-input-correlationTopic" placeholder="_msgid">
28
- <input type="hidden" id="node-input-correlationTopicType">
22
+ <div class="form-tips" style="margin-bottom:6px">
23
+ The set of paths whose arrival within the time window emits a
24
+ merged success output. Press <kbd>Enter</kbd> in a row to add the
25
+ next one. Repeat an entry to require n-of-the-same.
29
26
  </div>
27
+ <div class="form-tips" id="join-wait-output-preview" style="margin-bottom:12px; font-family:monospace; color:#888"></div>
28
+
30
29
  <div class="form-row">
31
- <label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
30
+ <label for="node-input-timeout" title="How long all wait paths have to arrive before the queue is expired."><i class="fa fa-clock-o"></i> Timeout</label>
32
31
  <input type="text" id="node-input-timeout" style="text-align:end; width:100px !important" placeholder="15">
33
32
  <select id="node-input-timeoutUnits" style="width:200px !important">
34
33
  <option value="1">Milliseconds</option>
@@ -38,224 +37,512 @@
38
37
  <option value="86400000">Days</option>
39
38
  </select>
40
39
  </div>
40
+ <div class="form-tips" id="join-wait-timeout-hint" style="margin-bottom:12px; color:#b07b00; display:none"></div>
41
+
41
42
  <div class="form-row">
42
- <label for="node-input-exactOrder"><i class="fa fa-sort"></i> Sequence order</label>
43
+ <label for="node-input-exactOrder" title="Any order (default) or strict sequence."><i class="fa fa-sort"></i> Match order</label>
43
44
  <select id="node-input-exactOrder" style="width:70%; margin-right:5px;">
44
45
  <option value="false">Any order</option>
45
- <option value="true">Exact order (strict)</option>
46
+ <option value="true">Exact order</option>
46
47
  </select>
47
48
  </div>
49
+
50
+ <div class="form-row">
51
+ <label for="node-input-correlationTopic" title="Optional. Property whose value groups related messages (e.g. msg._msgid)."><i class="fa fa-bookmark-o"></i> Group by</label>
52
+ <input type="text" id="node-input-correlationTopic" placeholder="(none)">
53
+ <input type="hidden" id="node-input-correlationTopicType">
54
+ </div>
55
+ <div class="form-tips" style="margin-bottom:12px">
56
+ Optional. Group messages so unrelated streams don't merge —
57
+ e.g. <code>msg._msgid</code> when waiting on a single split flow.
58
+ </div>
59
+
60
+ <div class="form-row" style="margin-bottom:14px">
61
+ <label></label>
62
+ <button type="button" id="join-wait-open-examples" class="red-ui-button">
63
+ <i class="fa fa-folder-open-o"></i> Open example flows
64
+ </button>
65
+ </div>
66
+
67
+ <details style="margin-top:14px">
68
+ <summary style="cursor:pointer; font-weight:bold; padding:6px 0">Advanced</summary>
69
+
70
+ <div class="form-row" style="margin-top:10px">
71
+ <label title="Paths that immediately drain the queue to the expired output."><i class="fa fa-times-circle-o"></i> Reset paths</label>
72
+ <ol id="node-input-pathsToExpire-container"></ol>
73
+ </div>
74
+ <div class="form-tips" style="margin-bottom:12px">
75
+ Optional. Any of these paths immediately drains the queue (with the
76
+ drained items going to the second output). Each entry must be unique.
77
+ </div>
78
+
48
79
  <div class="form-row">
49
- <label for="node-input-firstMsg"><i class="fa fa-comments-o"></i> Base message</label>
80
+ <label for="node-input-firstMsg" title="Use the first or last message in the matched window as the base of the merged output."><i class="fa fa-comments-o"></i> Output base</label>
50
81
  <select id="node-input-firstMsg" style="width:70%; margin-right:5px;">
51
- <option value="true">First received</option>
52
- <option value="false">Last received</option>
82
+ <option value="true">First received message</option>
83
+ <option value="false">Last received message</option>
53
84
  </select>
54
85
  </div>
86
+
55
87
  <div class="form-row">
56
- <label for="node-input-mapPayload"><i class="fa fa-arrow-right"></i> Merged data</label>
88
+ <label for="node-input-mapPayload" title="Keep the original Path-field values, or overwrite each with that message's payload."><i class="fa fa-arrow-right"></i> Merge values</label>
57
89
  <select id="node-input-mapPayload" style="width:70%; margin-right:5px;">
58
- <option value="false">Original Paths topic</option>
59
- <option value="true">Overwrite Paths topic with msg.payload</option>
90
+ <option value="false">Original Path field values</option>
91
+ <option value="true">Each msg.payload</option>
60
92
  </select>
61
93
  </div>
62
- <div class="form-row">
63
- <label for="node-input-disableComplete"><i class="fa fa-refresh"></i> Reset queue</label>
64
- <label for="node-input-disableComplete" style="width:70%">
65
- <input type="checkbox" id="node-input-disableComplete" style="display:inline-block; width:22px; vertical-align:baseline;">Ignore <code>msg.complete</code></label>
94
+
95
+ <div class="form-row" title="Treat each entry in Wait paths and Reset paths as a regular expression.">
96
+ <label for="node-input-useRegex"></label>
97
+ <input type="checkbox" id="node-input-useRegex" style="display:inline-block; width:22px; vertical-align:baseline; margin-right:6px;">
98
+ <label for="node-input-useRegex" style="width:auto">Treat path entries as regular expressions</label>
66
99
  </div>
67
- <div class="form-row">
68
- <label for="node-input-persistOnRestart"><i class="fa fa-floppy-o"></i> Persist</label>
69
- <label for="node-input-persistOnRestart" style="width:70%">
70
- <input type="checkbox" id="node-input-persistOnRestart" style="display:inline-block; width:22px; vertical-align:baseline;">Preserve queue on redeploy or restart</label>
100
+
101
+ <div class="form-row" title="Log a warning when a message arrives with a path that isn't in either list.">
102
+ <label for="node-input-warnUnmatched"></label>
103
+ <input type="checkbox" id="node-input-warnUnmatched" style="display:inline-block; width:22px; vertical-align:baseline; margin-right:6px;">
104
+ <label for="node-input-warnUnmatched" style="width:auto">Log a warning for unmatched paths</label>
105
+ </div>
106
+
107
+ <div class="form-row" title="Disable the msg.complete short-circuit.">
108
+ <label for="node-input-disableComplete"></label>
109
+ <input type="checkbox" id="node-input-disableComplete" style="display:inline-block; width:22px; vertical-align:baseline; margin-right:6px;">
110
+ <label for="node-input-disableComplete" style="width:auto">Ignore <code>msg.complete</code></label>
111
+ </div>
112
+
113
+ <div class="form-row" title="Save the queue to the context store on close so it survives redeploys (and full restarts when a persistent store is configured).">
114
+ <label for="node-input-persistOnRestart"></label>
115
+ <input type="checkbox" id="node-input-persistOnRestart" style="display:inline-block; width:22px; vertical-align:baseline; margin-right:6px;">
116
+ <label for="node-input-persistOnRestart" style="width:auto">Preserve queue across deploys / restarts</label>
71
117
  </div>
118
+
72
119
  <div class="form-row">
73
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
74
- <input type="text" id="node-input-name" placeholder="Name">
120
+ <label for="node-input-persistStore" title="Override the context store used to persist the queue."><i class="fa fa-database"></i> Persist store</label>
121
+ <select id="node-input-persistStore" style="width:70%; margin-right:5px;">
122
+ <option value="">(default context store)</option>
123
+ </select>
75
124
  </div>
76
- <div class="form-row node-input-rule-container-row">
77
- <ol id="node-input-rule-container"></ol>
125
+ <div class="form-tips" style="margin-bottom:12px">
126
+ Override the context store this node uses. Leave on default — set
127
+ the default to a persistent store (e.g. <code>localfilesystem</code>)
128
+ in <code>settings.js</code> and every join-wait node automatically
129
+ survives full restarts.
78
130
  </div>
131
+ </details>
79
132
  </script>
80
133
 
81
134
  <script type="text/javascript">
82
- RED.nodes.registerType('join-wait', {
83
- category: 'function',
84
- color: '#8baac7',
85
- defaults: {
86
- name: {
87
- value: '',
88
- },
89
- paths: {
90
- value: '',
91
- validate: function (v) {
92
- if (v === '') {
93
- return true;
94
- }
135
+ (function () {
136
+ function arrayValidator(allowEmpty, requireUnique) {
137
+ return function (v) {
138
+ // Accept legacy JSON-string format too, for back-compat with old flows.
139
+ var arr = v;
140
+ if (typeof v === 'string') {
141
+ if (v === '') return allowEmpty;
95
142
  try {
96
- const json = JSON.parse(v);
97
- return Array.isArray(json) && json.length > 0;
143
+ arr = JSON.parse(v);
98
144
  } catch (err) {
99
- console.log(err);
145
+ return false;
100
146
  }
147
+ }
148
+ if (!Array.isArray(arr)) return false;
149
+ if (arr.length === 0) return allowEmpty;
150
+ if (
151
+ !arr.every(function (s) {
152
+ return typeof s === 'string' && s.length > 0;
153
+ })
154
+ )
101
155
  return false;
102
- },
103
- },
104
- pathsToExpire: {
105
- value: '',
106
- validate: function (v) {
107
- if (v === '') {
108
- return true;
156
+ if (requireUnique) {
157
+ var seen = Object.create(null);
158
+ for (var i = 0; i < arr.length; i++) {
159
+ if (seen[arr[i]]) return false;
160
+ seen[arr[i]] = true;
109
161
  }
110
- try {
111
- const json = JSON.parse(v);
112
- if (Array.isArray(json) && json.length > 0) {
113
- return !json.some(function (p, index) {
114
- return json.indexOf(p) !== index;
115
- });
162
+ }
163
+ return true;
164
+ };
165
+ }
166
+
167
+ function correlationValidator(v) {
168
+ // Empty / undefined / non-jsonata: nothing to validate here.
169
+ if (this.correlationTopicType !== 'jsonata' || !v) return true;
170
+ // jsonata is exposed as a global by the Node-RED editor.
171
+ if (typeof jsonata !== 'function') return true;
172
+ try {
173
+ jsonata(v);
174
+ return true;
175
+ } catch (err) {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ function isValidRegex(s) {
181
+ try {
182
+ new RegExp(s);
183
+ return true;
184
+ } catch (err) {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ function setRowValid(input, ok, message) {
190
+ input.css('border-color', ok ? '' : '#d9534f');
191
+ input.attr('title', ok ? '' : message || '');
192
+ }
193
+
194
+ function validateRow(input) {
195
+ var $useRegex = $('#node-input-useRegex');
196
+ var v = String(input.val() || '');
197
+ if (v === '') {
198
+ setRowValid(input, false, 'path name cannot be empty');
199
+ return false;
200
+ }
201
+ if ($useRegex.is(':checked') && !isValidRegex(v)) {
202
+ setRowValid(input, false, 'invalid regular expression');
203
+ return false;
204
+ }
205
+ setRowValid(input, true);
206
+ return true;
207
+ }
208
+
209
+ function initEditableList(containerId, items, opts) {
210
+ var $container = $('#' + containerId);
211
+ $container.css({ 'min-height': '100px' });
212
+ var placeholder = (opts && opts.placeholder) || 'e.g. sensor_1';
213
+ $container.editableList({
214
+ addItem: function (row, index, data) {
215
+ var value = (data && data.value) || '';
216
+ row.css({ overflow: 'hidden', whiteSpace: 'nowrap' });
217
+ var input = $('<input/>', {
218
+ type: 'text',
219
+ placeholder: placeholder,
220
+ style: 'width:100%',
221
+ }).val(value);
222
+ input.on('input change blur', function () {
223
+ validateRow(input);
224
+ if (opts && opts.onChange) opts.onChange();
225
+ });
226
+ // Pressing Enter in a row adds the next one.
227
+ input.on('keydown', function (e) {
228
+ if (e.key === 'Enter' || e.keyCode === 13) {
229
+ e.preventDefault();
230
+ $container.editableList('addItem', { value: '' });
231
+ // Focus the newly-added row.
232
+ setTimeout(function () {
233
+ $container.editableList('items').last().find('input').trigger('focus');
234
+ }, 0);
116
235
  }
117
- } catch (err) {
118
- console.log(err);
119
- }
120
- return false;
236
+ });
237
+ row.append(input);
238
+ validateRow(input);
239
+ if (opts && opts.onChange) opts.onChange();
121
240
  },
122
- },
123
- useRegex: {
124
- value: false,
125
- },
126
- warnUnmatched: {
127
- value: true,
128
- },
129
- pathTopic: {
130
- value: 'topic',
131
- },
132
- pathTopicType: {
133
- value: 'msg',
134
- },
135
- correlationTopic: {
136
- value: '',
137
- },
138
- correlationTopicType: {
139
- value: 'undefined',
140
- },
141
- timeout: {
142
- value: 15000,
143
- required: true,
144
- validate: function (v) {
145
- return (Number(v) || 0) > 0;
241
+ removeItem: function () {
242
+ if (opts && opts.onChange) opts.onChange();
146
243
  },
147
- },
148
- timeoutUnits: {
149
- value: 1,
150
- validate: RED.validators.number(),
151
- },
152
- exactOrder: {
153
- value: 'false',
154
- required: true,
155
- },
156
- firstMsg: {
157
- value: 'true',
158
- required: true,
159
- },
160
- mapPayload: {
161
- value: 'false',
162
- required: true,
163
- },
164
- disableComplete: {
165
- value: false,
166
- },
167
- persistOnRestart: {
168
- value: false,
169
- },
170
- },
171
- inputs: 1,
172
- outputs: 2,
173
- outputLabels: ['success', 'expired'],
174
- icon: 'node-red-contrib-join-wait.png',
175
- label: function () {
176
- return this.name || 'join-wait';
177
- },
178
- labelStyle: function () {
179
- return this.name ? 'node_label_italic' : '';
180
- },
181
- oneditprepare: function () {
182
- var node = this;
183
- var previousValueType = {
184
- value: 'prev',
185
- label: this._('join-wait.previous'),
186
- hasValue: false,
187
- };
188
- $('#node-input-pathTopic').typedInput({
189
- default: this.pathTopicType || 'msg',
190
- typeField: $('#node-input-pathTopicType'),
191
- types: ['msg', 'flow', 'global'],
244
+ removable: true,
245
+ sortable: true,
246
+ height: 'auto',
192
247
  });
193
- $('#node-input-correlationTopic').typedInput({
194
- default: this.correlationTopicType || types.Undefined,
195
- typeField: $('#node-input-correlationTopicType'),
196
- types: [
197
- { value: 'undefined', label: RED._('common.type.undefined'), hasValue: false },
198
- 'msg',
199
- 'flow',
200
- 'global',
201
- 'jsonata',
202
- ],
248
+ (items || []).forEach(function (v) {
249
+ $container.editableList('addItem', { value: v });
203
250
  });
204
- $('#node-input-timeout').spinner({
205
- min: 1,
251
+ }
252
+
253
+ // Re-validate every editableList row when the regex toggle flips.
254
+ function rebindRegexValidation() {
255
+ $('#node-input-useRegex').on('change', function () {
256
+ $(
257
+ '#node-input-paths-container input[type=text], ' +
258
+ '#node-input-pathsToExpire-container input[type=text]',
259
+ ).each(function () {
260
+ validateRow($(this));
261
+ });
206
262
  });
207
- },
208
- });
209
- </script>
263
+ }
210
264
 
211
- <script type="text/x-red" data-help-name="join-wait">
212
- <p>This node waits for messages from all items in the <code>Paths (Wait)</code> array, which must be received inside of a designated time window.</p>
265
+ function readEditableList(containerId) {
266
+ var values = [];
267
+ $('#' + containerId)
268
+ .editableList('items')
269
+ .each(function () {
270
+ var v = $(this).find('input').val();
271
+ if (v != null && v !== '') values.push(v);
272
+ });
273
+ return values;
274
+ }
275
+
276
+ // Coerce legacy string-array values into a plain array for editing.
277
+ function toArray(v) {
278
+ if (Array.isArray(v)) return v;
279
+ if (typeof v === 'string' && v !== '') {
280
+ try {
281
+ var parsed = JSON.parse(v);
282
+ return Array.isArray(parsed) ? parsed : [];
283
+ } catch (err) {
284
+ return [];
285
+ }
286
+ }
287
+ return [];
288
+ }
213
289
 
214
- <p>If all of the messages are received in that interval, a merged output is sent to the <code>success</code> output. Otherwise, any expired messages are sent to the <code>timeout</code> output. Either output can be optionally connected for further processing.</p>
290
+ // Render a tiny preview of what msg.<pathField> looks like on the
291
+ // success output, derived from the current Wait paths list.
292
+ function updateOutputPreview() {
293
+ var paths = readEditableList('node-input-paths-container');
294
+ var unique = [];
295
+ paths.forEach(function (p) {
296
+ if (unique.indexOf(p) === -1) unique.push(p);
297
+ });
298
+ var pathField = $('#node-input-pathTopic').val() || 'topic';
299
+ var $tip = $('#join-wait-output-preview');
300
+ if (unique.length === 0) {
301
+ $tip.text('');
302
+ return;
303
+ }
304
+ var entries = unique.slice(0, 4).map(function (p) {
305
+ return p + ': …';
306
+ });
307
+ if (unique.length > 4) entries.push('…');
308
+ $tip.text('→ msg.' + pathField + ' = { ' + entries.join(', ') + ' }');
309
+ }
215
310
 
216
- <p>In the event of multiple messages, the time window is adjusted as needed to continue evaluation on subsequent messages. This node has several potential applications, including home automation. For instance, to handle a case where the light turning on/off is also triggering a motion sensor: IF a) light turned OFF, b) motion sensor activated, c) light turned ON all occur within 10 seconds, then turn light OFF.</p>
311
+ // Warn when the resolved timeout is impractically short (the README
312
+ // recommends padding ~5–10 ms for evaluation overhead).
313
+ function updateTimeoutHint() {
314
+ var t = Number($('#node-input-timeout').val()) || 0;
315
+ var u = Number($('#node-input-timeoutUnits').val()) || 1;
316
+ var ms = t * u;
317
+ var $hint = $('#join-wait-timeout-hint');
318
+ if (ms > 0 && ms < 50) {
319
+ $hint.text('Very short — pad ~10 ms to leave room for evaluation overhead.').show();
320
+ } else {
321
+ $hint.hide();
322
+ }
323
+ }
217
324
 
218
- <p>Memory is managed to delete objects after they reach the <code>Timeout</code>.</p>
325
+ // Populate the Persist store <select> from the runtime via our
326
+ // /join-wait/stores admin route. Falls back gracefully if the route
327
+ // is unreachable or returns nothing useful.
328
+ function populatePersistStores(currentValue) {
329
+ var $sel = $('#node-input-persistStore');
330
+ $.getJSON('join-wait/stores')
331
+ .done(function (stores) {
332
+ if (!Array.isArray(stores) || stores.length === 0) return;
333
+ stores.forEach(function (s) {
334
+ if (s.name === 'default') return; // already represented as the blank option
335
+ var label = s.module ? s.name + ' (' + s.module + ')' : s.name;
336
+ $('<option/>').val(s.name).text(label).appendTo($sel);
337
+ });
338
+ // If the saved value isn't in the list, append it so we
339
+ // don't silently drop a user-provided override.
340
+ if (currentValue && !$sel.find('option[value="' + currentValue + '"]').length) {
341
+ $('<option/>')
342
+ .val(currentValue)
343
+ .text(currentValue + ' (not configured)')
344
+ .appendTo($sel);
345
+ }
346
+ $sel.val(currentValue || '');
347
+ })
348
+ .fail(function () {
349
+ // Admin route not reachable — leave the select with just
350
+ // its default option.
351
+ });
352
+ }
219
353
 
220
- <h3>Details</h3>
354
+ function openExamplesDialog() {
355
+ // Open the import dialog; the user picks Examples → join-wait.
356
+ // RED.actions exposes core actions across recent Node-RED versions.
357
+ if (RED.actions && typeof RED.actions.invoke === 'function') {
358
+ RED.actions.invoke('core:show-import-dialog');
359
+ }
360
+ }
361
+
362
+ RED.nodes.registerType('join-wait', {
363
+ category: 'function',
364
+ color: '#8baac7',
365
+ defaults: {
366
+ name: { value: '' },
367
+ paths: { value: [], validate: arrayValidator(false, false) },
368
+ pathsToExpire: { value: [], validate: arrayValidator(true, true) },
369
+ useRegex: { value: false },
370
+ warnUnmatched: { value: true },
371
+ pathTopic: { value: 'topic', required: true },
372
+ pathTopicType: { value: 'msg' },
373
+ correlationTopic: { value: '', validate: correlationValidator },
374
+ correlationTopicType: { value: 'undefined' },
375
+ timeout: {
376
+ value: 15000,
377
+ required: true,
378
+ validate: function (v) {
379
+ return (Number(v) || 0) > 0;
380
+ },
381
+ },
382
+ timeoutUnits: { value: 1, validate: RED.validators.number() },
383
+ exactOrder: { value: 'false', required: true },
384
+ firstMsg: { value: 'true', required: true },
385
+ mapPayload: { value: 'false', required: true },
386
+ disableComplete: { value: false },
387
+ // Defaults to true so the queue survives a redeploy out of
388
+ // the box (uses the default in-memory context store).
389
+ // For full Node-RED restart persistence the user still needs
390
+ // to point Persist store at a configured persistent store.
391
+ persistOnRestart: { value: true },
392
+ persistStore: { value: '' },
393
+ },
394
+ inputs: 1,
395
+ outputs: 2,
396
+ outputLabels: ['success', 'expired'],
397
+ icon: 'node-red-contrib-join-wait.png',
398
+ label: function () {
399
+ return this.name || 'join-wait';
400
+ },
401
+ labelStyle: function () {
402
+ return this.name ? 'node_label_italic' : '';
403
+ },
404
+ oneditprepare: function () {
405
+ $('#node-input-pathTopic').typedInput({
406
+ default: this.pathTopicType || 'msg',
407
+ typeField: $('#node-input-pathTopicType'),
408
+ types: ['msg', 'flow', 'global'],
409
+ });
410
+ $('#node-input-correlationTopic').typedInput({
411
+ default: this.correlationTopicType || 'undefined',
412
+ typeField: $('#node-input-correlationTopicType'),
413
+ types: [
414
+ { value: 'undefined', label: RED._('common.type.undefined'), hasValue: false },
415
+ 'msg',
416
+ 'flow',
417
+ 'global',
418
+ 'jsonata',
419
+ ],
420
+ });
421
+ $('#node-input-timeout').spinner({ min: 1 });
221
422
 
222
- <p>Each item in the <code>Paths (Wait)</code> array corresponds with an input path to wait for. E.g., <code>[&quot;path_1&quot;, &quot;path_2&quot;, &quot;other_path&quot;]</code>. This can also be configured at runtime by passing an array using <code>msg.pathsToWait</code>.</p>
423
+ initEditableList('node-input-paths-container', toArray(this.paths), {
424
+ placeholder: 'e.g. sensor_1',
425
+ onChange: updateOutputPreview,
426
+ });
427
+ initEditableList('node-input-pathsToExpire-container', toArray(this.pathsToExpire), {
428
+ placeholder: 'e.g. abort',
429
+ });
430
+ rebindRegexValidation();
223
431
 
224
- <p>Each item in the <code>Paths (Expire)</code> array corresponds with an input path that will immediately expire all messages in the queue without further processing. This acts as a reset. This can also be configured at runtime by passing an array using <code>msg.pathsToExpire</code>. Each item must have a unique name.</p>
432
+ $('#node-input-pathTopic').on('change input', updateOutputPreview);
433
+ updateOutputPreview();
225
434
 
226
- <p><code>Paths topic</code> must be set to a <code>msg</code> property, which is used to check each flow to see if all of the elements in <code>Paths (Wait)</code> are matched. This can be <code>msg.topic</code>, <code>msg.paths</code>, etc. If this is not specified, <code>msg.paths</code> is the default.</p>
435
+ $('#node-input-timeout, #node-input-timeoutUnits').on('change input', updateTimeoutHint);
436
+ updateTimeoutHint();
227
437
 
228
- <p>Note that <code>Paths topic</code> can be set in one of two ways:</p>
229
- <p>1. As a string, set to the path to check, e.g., <code>msg.paths = &quot;path_1&quot;;</code></p>
230
- <p>2. As an object, set to any value (e.g., <code>msg.paths[&quot;path_1&quot;] = {&quot;example&quot;: &quot;data&quot;};</code> or <code>msg.paths[&quot;path_1&quot;] = 42;</code>). If the object format is used, multiple paths can be specified. For example, <code>msg.paths = {&quot;path_1&quot;: true, &quot;path_2&quot;: true};</code> This can be useful if one flow needs to trigger multiple paths.</p>
438
+ populatePersistStores(this.persistStore);
231
439
 
232
- <p><code>Correlation topic</code> can be set, if desired, to ensure that only related messages are grouped. E.g., <code>msg._msgid</code> can be used to ensure that only messages from a <i>single</i> split flow are grouped together. If left blank, all messages will be assumed to be related.</p>
440
+ $('#join-wait-open-examples').on('click', openExamplesDialog);
441
+ },
442
+ oneditsave: function () {
443
+ this.paths = readEditableList('node-input-paths-container');
444
+ this.pathsToExpire = readEditableList('node-input-pathsToExpire-container');
445
+ },
446
+ oneditresize: function () {
447
+ $('#node-input-paths-container').css('min-height', '100px');
448
+ $('#node-input-pathsToExpire-container').css('min-height', '60px');
449
+ },
450
+ });
451
+ })();
452
+ </script>
453
+
454
+ <script type="text/x-red" data-help-name="join-wait">
455
+ <p>Waits for a set of related messages to arrive on different paths within
456
+ a time window, then emits a single merged message. Optionally treats some
457
+ paths as resets that drain the queue.</p>
233
458
 
234
- <p><code>Timeout</code> is required to designate the time window to receive all of the messages from <code>Paths (Wait)</code>.</p>
459
+ <h3>Inputs</h3>
460
+ <dl class="message-properties">
461
+ <dt>payload<span class="property-type">any</span></dt>
462
+ <dd>Forwarded as-is (and optionally used to merge values — see "Merge values").</dd>
235
463
 
236
- <p><code>Sequence order</code> defines the criteria to evaluate the received messages. An <i>exact</i> match can be specified, otherwise, it will match them in any order. To determine the order, the timestamp on the <i>latest</i> valid <code>Paths (Wait)</code> is used, even if multiple messages arrived earlier. In this case of waiting for <code>[&quot;path_1&quot;, &quot;path_2&quot;, &quot;path_3&quot;]</code>, the <code>*</code> indicates which messages are used: <code>[&quot;path_1&quot;, &quot;path_2&quot;, &quot;path_1&quot;*, &quot;path_2&quot;*, &quot;path_3&quot;*]</code>.</p>
464
+ <dt class="optional">path field<span class="property-type">string | object</span></dt>
465
+ <dd>The message property named in the <b>Path field</b> setting (default <code>msg.topic</code>).
466
+ Either a single path name (<code>"path_1"</code>) or an object whose
467
+ keys are path names (<code>{path_1: ..., path_2: ...}</code>) when one
468
+ message represents multiple paths.</dd>
237
469
 
238
- <p><code>Base message</code> defines which message object should be returned as the base message. Either the first message in a sequence or the last.</p>
470
+ <dt class="optional">complete<span class="property-type">any</span></dt>
471
+ <dd>If present, the queue is evaluated for completion and then drained.
472
+ Disable via "Ignore <code>msg.complete</code>".</dd>
239
473
 
240
- <p><code>Merged data</code> defines how the data from <code>msg.paths</code> (or, another designed <code>Paths topic</code>) will be returned. Either, it can be merged in its original form, or, it can be overwritten with each respective <code>msg.payload</code>. This merged data is then appended to the <code>Base message</code>. In the event that multiple messages arrive in this time interval with the same <code>Paths (Wait)</code>, only the data from the latest item is returned. For instance, if <code>Paths (Wait)</code> = <code>[&quot;path_1&quot;, &quot;path_2&quot;, &quot;path_3&quot;]</code>, the <code>*</code> indicates which messages are used in this sequence: <code>[&quot;path_1&quot;, &quot;path_2&quot;, &quot;path_1&quot;, &quot;path_2&quot;, &quot;path_1&quot;*, &quot;path_2&quot;*, &quot;path_3&quot;*]</code>. These additional messages will <b></b> be expired.</p>
474
+ <dt class="optional">reset<span class="property-type">boolean</span></dt>
475
+ <dd>If <code>true</code>, the queue for the current group is silently
476
+ cleared (no output emitted). The other message fields are ignored.</dd>
241
477
 
242
- <h3>Notes and Caveats</h3>
478
+ <dt class="optional">pathsToWait<span class="property-type">string[]</span></dt>
479
+ <dd>Per-message override of "Wait paths" (one-shot — does not change
480
+ the node's stored config).</dd>
481
+
482
+ <dt class="optional">pathsToExpire<span class="property-type">string[]</span></dt>
483
+ <dd>Per-message override of "Reset paths" (one-shot).</dd>
484
+
485
+ <dt class="optional">useRegex<span class="property-type">boolean</span></dt>
486
+ <dd>Per-message override of the regex setting (one-shot).</dd>
487
+ </dl>
488
+
489
+ <h3>Outputs</h3>
490
+ <ol class="node-ports">
491
+ <li>Success — emitted when all "Wait paths" arrive within the timeout.
492
+ The merged data is attached on the <b>Path field</b> property.</li>
493
+ <li>Expired — drained messages from queues that timed out, hit a reset
494
+ path, or that <code>msg.complete</code> evaluated.</li>
495
+ </ol>
496
+
497
+ <h3>Details</h3>
243
498
 
244
- <p>There is support for repeated paths. For example, <code>["path_1", "path_2", "path_1", "path_2"]</code>.</p>
499
+ <p><b>Path field</b> the property whose value identifies the path. Can
500
+ be set as a string (one path) or an object (multiple paths from one
501
+ message). Defaults to <code>msg.topic</code>.</p>
245
502
 
246
- <p>If any order is used, <code>Paths (Wait)</code> is evaluated to determine the count for repeated paths. If *regex* is used, paths will be counted in a greedy fashion from left to right. For example, <code>["path_[12]", "path_2"]</code> would never complete because all instances of "path_1" and "path_2" would be counted for the first path.</p>
503
+ <p><b>Wait paths</b> list of paths to wait for. Repeating an entry means
504
+ "this path must arrive that many times". Configurable at runtime via
505
+ <code>msg.pathsToWait</code>.</p>
247
506
 
248
- <p>If exact order is used, note that unexpected paths would still be tolerated.</p>
507
+ <p><b>Reset paths</b> paths that immediately drain the queue. Each must
508
+ be unique. Configurable at runtime via <code>msg.pathsToExpire</code>.</p>
249
509
 
250
- <p>In the case of duplicate paths, only the data from the latest path(s) will be used.</p>
510
+ <p><b>Group by</b> optional correlation property. When set, only
511
+ messages with the same value are joined together. Useful with
512
+ <code>msg._msgid</code> when waiting on a single split flow. Supports
513
+ <code>msg</code>, <code>flow</code>, <code>global</code>, and
514
+ <code>jsonata</code> property types.</p>
251
515
 
252
- <p>If the <code>regex</code> option is enabled, each path will be treated as a regular expression. So, <code>[&quot;^path\d+$&quot;]</code> would match any path1, path2, path3, etc. Note that <code>^$</code> are not required, and if omitted, would just perform a partial match. For example <code>[&quot;path\d+&quot;]</code> would match &quot;my_path1_test&quot;. This property can also be set at runtime by passing <code>msg.useRegex</code>.</p>
516
+ <p><b>Match order</b> "Any order" (default) or "Exact order". In exact
517
+ mode, unexpected paths between expected ones are tolerated. To determine
518
+ the matching window, the timestamp of the <i>latest</i> required path is
519
+ used. For example, with wait paths <code>[path_1, path_2, path_3]</code>
520
+ and arrivals <code>[path_1, path_2, path_1*, path_2*, path_3*]</code>,
521
+ the starred entries form the match.</p>
253
522
 
254
- <p>If the <code>msg.complete</code> property is set, the message queue will be evaluated for completion, and then any remaining items in the queue will be immediately expired. This feature can be disabled in the settings, if desired.</p>
523
+ <p><b>Output base</b> whether to use the first or last received message
524
+ as the base for the merged output.</p>
255
525
 
256
- <p>All values within <code>Paths topic</code> must be contained by either <code>Paths (Wait)</code> or <code>Paths (Expire)</code>, or an error will be thrown. The <code>Unmatched paths</code> error notification can be disabled within the settings.</p>
526
+ <p><b>Merge values</b> keep the original values from the path field, or
527
+ overwrite each with that message's <code>msg.payload</code>.</p>
257
528
 
258
- <p>If <code>msg.pathsToWait</code> is used instead of setting <code>Paths (Wait)</code>, note that each successive <code>msg.pathsToWait</code> will overwrite the previously stored global value. Due to the nature of the timeout, <code>Paths (Wait)</code> needs to be evaluated even after a message has arrived. Changing the value of <code>msg.pathsToWait</code> between messages may cause unexpected behavior.</p>
529
+ <h3>Notes</h3>
259
530
 
260
- <p><code>Timeout</code> should be padded with a small amount of overhead (i.e., ~5-10 ms or so) for the time it takes to evaluate all of the messages and conditions. This may become critical under very short timeouts.</p>
531
+ <ul>
532
+ <li>If <b>Use regex</b> is on, each entry in Wait/Reset paths is
533
+ compiled as a regular expression. <code>^</code>/<code>$</code>
534
+ anchors are not implicit — <code>path\d+</code> matches
535
+ <code>my_path1_test</code>.</li>
536
+ <li>For repeated paths in any-order mode with regex, paths are counted
537
+ greedily left-to-right. <code>["path_[12]", "path_2"]</code> never
538
+ completes because <code>path_2</code> arrivals get attributed to the
539
+ first regex.</li>
540
+ <li>When the same path arrives multiple times, only the latest value
541
+ is kept in the merged output.</li>
542
+ <li>Pad <b>Timeout</b> with a small overhead (~5–10&nbsp;ms) for
543
+ evaluation time when working with very short windows.</li>
544
+ <li>When <b>Preserve queue</b> is on, the queue is saved to a JSON
545
+ file under <code>{userDir}/join-wait/{nodeId}.json</code> on close
546
+ and reloaded on next start.</li>
547
+ </ul>
261
548
  </script>