node-red-contrib-join-wait 0.5.2 → 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/README.md CHANGED
@@ -1,1233 +1,231 @@
1
- # Node RED join-wait
1
+ # node-red-contrib-join-wait
2
+
3
+ [![CI](https://github.com/dxdc/node-red-contrib-join-wait/actions/workflows/ci.yml/badge.svg)](https://github.com/dxdc/node-red-contrib-join-wait/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/node-red-contrib-join-wait.svg)](https://www.npmjs.com/package/node-red-contrib-join-wait)
5
+ [![npm downloads](https://img.shields.io/npm/dt/node-red-contrib-join-wait.svg)](https://www.npmjs.com/package/node-red-contrib-join-wait)
6
+ [![license](https://img.shields.io/npm/l/node-red-contrib-join-wait.svg)](LICENSE)
7
+
8
+ A Node-RED node that joins related messages across multiple paths within a
9
+ time window — with exact-order matching, regex paths, correlation grouping,
10
+ reset paths, and queue persistence. Coordinate parallel flows, synchronize
11
+ events, and debounce sensors.
12
+
13
+ If all the named paths arrive in time, a merged message is emitted on the
14
+ **success** output. Anything left over goes to the **expired** output for
15
+ optional follow-up.
16
+
17
+ ## Use cases
18
+
19
+ - **Debounce a motion sensor** when a light turning on/off is also tripping
20
+ it: only fire if `light_off` then `motion` then `light_on` all arrive
21
+ within 10 s.
22
+ - **Correlate request lifecycle events** — wait for `request_started` and
23
+ `request_finished` with the same correlation id and emit one log entry
24
+ with the duration.
25
+ - **Sensor consensus** — only act when both `door_open` and `vibration`
26
+ fire within 2 s.
27
+ - **Recombine fanned-out API calls** — join the responses from a
28
+ `split` flow when each branch tags its result with a known path name.
29
+ - **Watchdog gate** — let `start` plus N progress beats arrive within a
30
+ window, otherwise route to an alarm flow via the expired output.
31
+
32
+ ## Is this the right node? `join-wait` vs. the stock `join` node
33
+
34
+ | You need to… | Stock `join` | `join-wait` |
35
+ | ------------------------------------------------------ | :----------: | :---------: |
36
+ | Recombine pieces of a `split` message (`msg.parts`) | ✅ | – |
37
+ | Concatenate strings, arrays, or buffers | ✅ | – |
38
+ | Count "any N messages" into a batch | ✅ | – |
39
+ | Wait for **specific named** paths within a time window | – | ✅ |
40
+ | Match path names by regex | – | ✅ |
41
+ | Require an **exact order** (with repeats) | – | ✅ |
42
+ | Drain the queue when a "reset" path arrives | – | ✅ |
43
+ | Group by an arbitrary correlation expression | – | ✅ |
44
+
45
+ **Rule of thumb:** if you split a message and want to put it back
46
+ together, use the stock `join`. If you have heterogeneous events from
47
+ different sources and want to coordinate them, use `join-wait`.
48
+
49
+ ## Quick start
2
50
 
3
- [![mit license](https://badgen.net/badge/license/MIT/red)](https://github.com/dxdc/node-red-contrib-join-wait/blob/master/LICENSE)
4
- [![npm](https://badgen.net/npm/v/node-red-contrib-join-wait)](https://www.npmjs.com/package/node-red-contrib-join-wait)
5
- [![npm](https://badgen.net/npm/dt/node-red-contrib-join-wait)](https://www.npmjs.com/package/node-red-contrib-join-wait)
6
- [![Donate](https://badgen.net/badge/Donate/PayPal/91BE09)](https://paypal.me/ddcaspi)
7
-
8
- This Node-RED module waits for incoming messages from different input paths to arrive within a fixed time window.
9
-
10
- > Node-RED is a tool for wiring together hardware devices, APIs and online services in new and interesting ways.
11
-
12
- ## Description
13
-
14
- This node waits for messages from all items in the `Paths (Wait)` array, which must be received inside of a designated time window.
15
-
16
- If all of the messages are received in that interval, a merged output is sent to the `success` output. Otherwise, any expired messages are sent to the `timeout` output. Either output can be optionally connected for further processing.
17
-
18
- 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.
19
-
20
- Memory is managed to delete objects after they reach the `Timeout`.
21
-
22
- ## Configuration
23
-
24
- - Each item in the `Paths (Wait)` array corresponds with an input path to wait for. E.g., `["path_1", "path_2", "other_path"]`. Each path item must have a unique name.
25
-
26
- > This can also be configured at runtime by passing an array using `msg.pathsToWait`.
27
-
28
- - Each item in the `Paths (Expire)` array corresponds with an input path that will immediately expire all messages in the queue without further processing. This acts as a reset. Each path item must have a unique name.
29
-
30
- > This can also be configured at runtime by passing an array using `msg.pathsToExpire`.
31
-
32
- - If the `Use regex` option is enabled, each item in the Paths array will be treated as a regular expression.
33
-
34
- > This can also be configured at runtime by passing `msg.useRegex` as a boolean.
35
-
36
- - `Paths topic` must be set to a `msg` property, which is used to check each flow to see if all of the elements in `Paths (Wait)` are matched. This can be `msg.topic`, `msg.paths`, etc. If this is not specified, `msg.paths` is the default.
37
-
38
- Note that `Paths topic` can be set in one of two ways:
39
-
40
- 1. As a string, set to the path to check, e.g., `msg.paths = "path_1";`
41
- 2. As an object, set to any value (e.g., `msg.paths["path_1"] = {"example": "data"};` or `msg.paths["path_1"] = 42;`).
42
-
43
- > If the object format is used, multiple paths can be specified. For example, `msg.paths = {"path_1": true, "path_2": true};` This can be useful if one flow needs to trigger multiple paths.
44
-
45
- - `Correlation topic` can be set, if desired, to ensure that only related messages are grouped. E.g., `msg._msgid` can be used to ensure that only messages from a _single_ split flow are grouped together.
46
-
47
- > If left blank, all messages will be assumed to be related.
48
-
49
- - `Timeout` is required to designate the time window to receive all of the messages from `Paths (Wait)`.
50
-
51
- - `Sequence order` defines the criteria to evaluate the received messages. An _exact_ match can be specified, otherwise, it will match them in any order.
52
-
53
- > To determine the order, the timestamp on the _latest_ valid `Paths (Wait)` is used, even if multiple messages arrived earlier. In this case of waiting for `["path_1", "path_2", "path_3"]`, the `*` indicates which messages are used: `["path_1", "path_2", "path_1"*, "path_2"*, "path_3"*]`.
54
-
55
- - `Base message` defines which message object should be returned as the base message. Either the first message in a sequence or the last.
56
-
57
- - `Merged data` defines how the data from `msg.paths` (or, another designed `Paths topic`) will be returned. Either, it can be merged in its original form, or, it can be overwritten with each respective `msg.payload`. This merged data is then appended to the `Base message`.
58
-
59
- > In the event that multiple messages arrive in this time interval with the same `Paths (Wait)`, only the data from the latest item is returned. For instance, if `Paths (Wait)` = `["path_1", "path_2", "path_3"]`, the `*` indicates which messages are used in this sequence: `["path_1", "path_2", "path_1", "path_2", "path_1"*, "path_2"*, "path_3"*]`. These additional messages (not starred) **will** be expired.
60
-
61
- ## Notes and Caveats
62
-
63
- - There is support for repeated paths. For example, `["path_1", "path_2", "path_1", "path_2"]`.
64
-
65
- > - If any order is used, `Paths (Wait)` 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, `["path_[12]", "path_2"]` would never complete because all instances of "path_1" and "path_2" would be counted for the first path.
66
-
67
- > - If exact order is used, note that unexpected paths would still be tolerated.
68
-
69
- > - In the case of duplicate paths, only the data from the latest path(s) will be used.
70
-
71
- - If the `regex` option is enabled, each path will be treated as a regular expression. So, `["^path\d+$"]` would match any path1, path2, path3, etc. Note that `^$` are not required, and if omitted, would just perform a partial match. For example `["path\d+"]` would match "my_path1_test". This property can also be set at runtime by passing `msg.useRegex`.
72
-
73
- - If the `msg.complete` 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.
74
-
75
- - All values within `Paths topic` must be contained by either `Paths (Wait)` or `Paths (Expire)`, or an error will be thrown. The `Unmatched paths` error notification can be disabled within the settings.
76
-
77
- - If `msg.pathsToWait` is used instead of setting `Paths (Wait)`, note that each successive `msg.pathsToWait` will overwrite the previously stored global value. Due to the nature of the timeout, `Paths (Wait)` needs to be evaluated even after a message has arrived. Changing the value of `msg.pathsToWait` between messages may cause unexpected behavior.
78
-
79
- - `Timeout` 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.
80
-
81
- ## Example 1: Wait 5 seconds for input from 2 flows (in any order)
82
-
83
- ![Example 1](/docs/example1.png?raw=true 'Example 1')
84
-
85
- <details><summary>Flow</summary>
86
-
87
- ```json
88
- [
89
- {
90
- "id": "7382168d.c47858",
91
- "type": "inject",
92
- "z": "fb783323.7e308",
93
- "name": "",
94
- "topic": "topic1",
95
- "payload": "{\"brightness\":\"20\"}",
96
- "payloadType": "json",
97
- "repeat": "",
98
- "crontab": "",
99
- "once": false,
100
- "onceDelay": "",
101
- "x": 950,
102
- "y": 1460,
103
- "wires": [["93c1545b.dca6f8"]]
104
- },
105
- {
106
- "id": "3b8f6807.956d78",
107
- "type": "debug",
108
- "z": "fb783323.7e308",
109
- "name": "",
110
- "active": true,
111
- "tosidebar": true,
112
- "console": false,
113
- "complete": "true",
114
- "x": 1610,
115
- "y": 1320,
116
- "wires": []
117
- },
118
- {
119
- "id": "5866d421.7eb66c",
120
- "type": "inject",
121
- "z": "fb783323.7e308",
122
- "name": "",
123
- "topic": "topic1",
124
- "payload": "",
125
- "payloadType": "date",
126
- "repeat": "",
127
- "crontab": "",
128
- "once": false,
129
- "onceDelay": "",
130
- "x": 960,
131
- "y": 1380,
132
- "wires": [["8d0b69cc.b2b228"]]
133
- },
134
- {
135
- "id": "337256a2.04446a",
136
- "type": "debug",
137
- "z": "fb783323.7e308",
138
- "name": "",
139
- "active": true,
140
- "tosidebar": true,
141
- "console": false,
142
- "tostatus": false,
143
- "complete": "payload",
144
- "targetType": "msg",
145
- "x": 1610,
146
- "y": 1480,
147
- "wires": []
148
- },
149
- {
150
- "id": "8d0b69cc.b2b228",
151
- "type": "change",
152
- "z": "fb783323.7e308",
153
- "name": "Set path_1",
154
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_1", "tot": "str" }],
155
- "action": "",
156
- "property": "",
157
- "from": "",
158
- "to": "",
159
- "reg": false,
160
- "x": 1210,
161
- "y": 1380,
162
- "wires": [["959a4717.0b5138"]]
163
- },
164
- {
165
- "id": "93c1545b.dca6f8",
166
- "type": "change",
167
- "z": "fb783323.7e308",
168
- "name": "Set path_2",
169
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_2", "tot": "str" }],
170
- "action": "",
171
- "property": "",
172
- "from": "",
173
- "to": "",
174
- "reg": false,
175
- "x": 1210,
176
- "y": 1460,
177
- "wires": [["959a4717.0b5138"]]
178
- },
179
- {
180
- "id": "959a4717.0b5138",
181
- "type": "join-wait",
182
- "z": "fb783323.7e308",
183
- "name": "",
184
- "paths": "[\"path_1\", \"path_2\"]",
185
- "pathsToExpire": "",
186
- "ignoreUnmatched": false,
187
- "pathTopic": "paths",
188
- "pathTopicType": "msg",
189
- "correlationTopic": "",
190
- "correlationTopicType": "msg",
191
- "timeout": "5",
192
- "timeoutUnits": "1000",
193
- "exactOrder": "false",
194
- "firstMsg": "true",
195
- "mapPayload": "true",
196
- "disableComplete": false,
197
- "x": 1420,
198
- "y": 1400,
199
- "wires": [["3b8f6807.956d78"], ["337256a2.04446a"]]
200
- },
201
- {
202
- "id": "6ae4802.e40238",
203
- "type": "comment",
204
- "z": "fb783323.7e308",
205
- "name": "(optional) expired messages",
206
- "info": "",
207
- "x": 1660,
208
- "y": 1520,
209
- "wires": []
210
- }
211
- ]
51
+ ```sh
52
+ cd ~/.node-red
53
+ npm install node-red-contrib-join-wait
212
54
  ```
213
55
 
214
- </details>
56
+ Or search **`join-wait`** in **Manage palette → Install**.
215
57
 
216
- ## Example 2: Wait 5 seconds for input from a split flow (in any order); one message does not arrive in time
58
+ Open the editor, drag a `join-wait` node onto a flow, and add the path names
59
+ you want to wait for. That's it.
217
60
 
218
- - `_msgid` is used as the **Correlation topic**, so that flows from split queues can be tracked.
61
+ > Looking for a working flow? After install, open
62
+ > **Menu → Import → Examples → join-wait** for ready-made flows.
219
63
 
220
- ![Example 2](/docs/example2.png?raw=true 'Example 2')
64
+ ## How it works
221
65
 
222
- <details><summary>Flow</summary>
223
-
224
- ```json
225
- [
226
- {
227
- "id": "c5aae6a2.78f4d8",
228
- "type": "debug",
229
- "z": "fb783323.7e308",
230
- "name": "",
231
- "active": true,
232
- "tosidebar": true,
233
- "console": false,
234
- "complete": "true",
235
- "x": 1590,
236
- "y": 1440,
237
- "wires": []
238
- },
239
- {
240
- "id": "4644d839.170ac8",
241
- "type": "inject",
242
- "z": "fb783323.7e308",
243
- "name": "",
244
- "topic": "",
245
- "payload": "",
246
- "payloadType": "date",
247
- "repeat": "",
248
- "crontab": "",
249
- "once": false,
250
- "onceDelay": "",
251
- "x": 800,
252
- "y": 1500,
253
- "wires": [["f785671e.2f1ac8", "2869e698.5a1daa", "373d1571.a4d5fa"]]
254
- },
255
- {
256
- "id": "3ed2d37b.3a852c",
257
- "type": "debug",
258
- "z": "fb783323.7e308",
259
- "name": "",
260
- "active": true,
261
- "tosidebar": true,
262
- "console": false,
263
- "tostatus": false,
264
- "complete": "payload",
265
- "targetType": "msg",
266
- "x": 1590,
267
- "y": 1600,
268
- "wires": []
269
- },
270
- {
271
- "id": "f785671e.2f1ac8",
272
- "type": "change",
273
- "z": "fb783323.7e308",
274
- "name": "Set path_1",
275
- "rules": [
276
- { "t": "set", "p": "paths", "pt": "msg", "to": "path_1", "tot": "str" },
277
- { "t": "set", "p": "payload", "pt": "msg", "to": "true", "tot": "bool" }
278
- ],
279
- "action": "",
280
- "property": "",
281
- "from": "",
282
- "to": "",
283
- "reg": false,
284
- "x": 1190,
285
- "y": 1500,
286
- "wires": [["c492296e.f80428"]]
287
- },
288
- {
289
- "id": "84493c9a.b98f9",
290
- "type": "change",
291
- "z": "fb783323.7e308",
292
- "name": "Set path_2",
293
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_2", "tot": "str" }],
294
- "action": "",
295
- "property": "",
296
- "from": "",
297
- "to": "",
298
- "reg": false,
299
- "x": 1190,
300
- "y": 1580,
301
- "wires": [["c492296e.f80428"]]
302
- },
303
- {
304
- "id": "c492296e.f80428",
305
- "type": "join-wait",
306
- "z": "fb783323.7e308",
307
- "name": "",
308
- "paths": "[\"path_1\", \"path_2\"]",
309
- "pathsToExpire": "",
310
- "pathTopic": "paths",
311
- "pathTopicType": "msg",
312
- "correlationTopic": "_msgid",
313
- "correlationTopicType": "msg",
314
- "timeout": "5",
315
- "timeoutUnits": "1000",
316
- "exactOrder": "false",
317
- "firstMsg": "true",
318
- "mapPayload": "true",
319
- "x": 1400,
320
- "y": 1520,
321
- "wires": [["c5aae6a2.78f4d8"], ["3ed2d37b.3a852c"]]
322
- },
323
- {
324
- "id": "a73abdd0.1bd6",
325
- "type": "comment",
326
- "z": "fb783323.7e308",
327
- "name": "(optional) expired messages",
328
- "info": "",
329
- "x": 1640,
330
- "y": 1640,
331
- "wires": []
332
- },
333
- {
334
- "id": "2869e698.5a1daa",
335
- "type": "delay",
336
- "z": "fb783323.7e308",
337
- "name": "",
338
- "pauseType": "delay",
339
- "timeout": "4",
340
- "timeoutUnits": "seconds",
341
- "rate": "1",
342
- "nbRateUnits": "1",
343
- "rateUnits": "second",
344
- "randomFirst": "1",
345
- "randomLast": "5",
346
- "randomUnits": "seconds",
347
- "drop": false,
348
- "x": 1000,
349
- "y": 1580,
350
- "wires": [["84493c9a.b98f9"]]
351
- },
352
- {
353
- "id": "373d1571.a4d5fa",
354
- "type": "delay",
355
- "z": "fb783323.7e308",
356
- "name": "",
357
- "pauseType": "delay",
358
- "timeout": "7",
359
- "timeoutUnits": "seconds",
360
- "rate": "1",
361
- "nbRateUnits": "1",
362
- "rateUnits": "second",
363
- "randomFirst": "1",
364
- "randomLast": "5",
365
- "randomUnits": "seconds",
366
- "drop": false,
367
- "x": 1000,
368
- "y": 1640,
369
- "wires": [["84493c9a.b98f9"]]
370
- }
371
- ]
372
66
  ```
373
-
374
- </details>
375
-
376
- ## Example 3: Wait 5 seconds for input from Events 1A and 1B; Wait 5 seconds for input from Events 2A and 2B; Wait 1 minute for both event groups to complete.
377
-
378
- - Shows an example of how multiple `join-wait` nodes can be chained
379
- - If Event 3A is received, reset queue
380
-
381
- ![Example 3](/docs/example3.png?raw=true 'Example 3')
382
-
383
- <details><summary>Flow</summary>
384
-
385
- ```json
386
- [
387
- {
388
- "id": "ecf4478b.5bcf28",
389
- "type": "change",
390
- "z": "ee7b2f38.64383",
391
- "name": "Set event_1B",
392
- "rules": [{ "t": "set", "p": "name", "pt": "msg", "to": "event_1B", "tot": "str" }],
393
- "action": "",
394
- "property": "",
395
- "from": "",
396
- "to": "",
397
- "reg": false,
398
- "x": 760,
399
- "y": 740,
400
- "wires": [["fb17fdab.13bc6"]]
401
- },
402
- {
403
- "id": "fb17fdab.13bc6",
404
- "type": "join-wait",
405
- "z": "ee7b2f38.64383",
406
- "name": "",
407
- "paths": "[\"event_1A\", \"event_1B\"]",
408
- "pathsToExpire": "",
409
- "ignoreUnmatched": false,
410
- "pathTopic": "name",
411
- "pathTopicType": "msg",
412
- "correlationTopic": "",
413
- "correlationTopicType": "msg",
414
- "timeout": "5",
415
- "timeoutUnits": "1000",
416
- "exactOrder": "false",
417
- "firstMsg": "true",
418
- "mapPayload": "true",
419
- "disableComplete": false,
420
- "x": 960,
421
- "y": 680,
422
- "wires": [["8d4ce0e0.362b7"], []]
423
- },
424
- {
425
- "id": "c264c765.36e3c8",
426
- "type": "change",
427
- "z": "ee7b2f38.64383",
428
- "name": "Set event_1A",
429
- "rules": [{ "t": "set", "p": "name", "pt": "msg", "to": "event_1A", "tot": "str" }],
430
- "action": "",
431
- "property": "",
432
- "from": "",
433
- "to": "",
434
- "reg": false,
435
- "x": 750,
436
- "y": 660,
437
- "wires": [["fb17fdab.13bc6"]]
438
- },
439
- {
440
- "id": "6a032c55.883394",
441
- "type": "delay",
442
- "z": "ee7b2f38.64383",
443
- "name": "",
444
- "pauseType": "delay",
445
- "timeout": "2",
446
- "timeoutUnits": "seconds",
447
- "rate": "1",
448
- "nbRateUnits": "1",
449
- "rateUnits": "second",
450
- "randomFirst": "1",
451
- "randomLast": "5",
452
- "randomUnits": "seconds",
453
- "drop": false,
454
- "x": 560,
455
- "y": 740,
456
- "wires": [["ecf4478b.5bcf28"]]
457
- },
458
- {
459
- "id": "8d4ce0e0.362b7",
460
- "type": "join-wait",
461
- "z": "ee7b2f38.64383",
462
- "name": "",
463
- "paths": "[\"event_1A\", \"event_1B\", \"event_2A\", \"event_2B\"]",
464
- "pathsToExpire": "[\"event_3A\"]",
465
- "ignoreUnmatched": false,
466
- "pathTopic": "name",
467
- "pathTopicType": "msg",
468
- "correlationTopic": "",
469
- "correlationTopicType": "msg",
470
- "timeout": "1",
471
- "timeoutUnits": "60000",
472
- "exactOrder": "false",
473
- "firstMsg": "true",
474
- "mapPayload": "false",
475
- "disableComplete": false,
476
- "x": 1200,
477
- "y": 800,
478
- "wires": [["f4c5e7c1.831528"], ["830ebf9c.f1588"]]
479
- },
480
- {
481
- "id": "2830e00c.06bde",
482
- "type": "inject",
483
- "z": "ee7b2f38.64383",
484
- "name": "",
485
- "topic": "",
486
- "payload": "",
487
- "payloadType": "date",
488
- "repeat": "",
489
- "crontab": "",
490
- "once": false,
491
- "onceDelay": "",
492
- "x": 300,
493
- "y": 780,
494
- "wires": [["4213bd84.2c68b4", "6a032c55.883394", "c47ba83d.c17548", "d2839a03.b24e98"]]
495
- },
496
- {
497
- "id": "7e51b1f.75d755",
498
- "type": "change",
499
- "z": "ee7b2f38.64383",
500
- "name": "Set event_2B",
501
- "rules": [{ "t": "set", "p": "name", "pt": "msg", "to": "event_2B", "tot": "str" }],
502
- "action": "",
503
- "property": "",
504
- "from": "",
505
- "to": "",
506
- "reg": false,
507
- "x": 760,
508
- "y": 900,
509
- "wires": [["4d466044.9e7ba"]]
510
- },
511
- {
512
- "id": "4213bd84.2c68b4",
513
- "type": "change",
514
- "z": "ee7b2f38.64383",
515
- "name": "Set event_2A",
516
- "rules": [{ "t": "set", "p": "name", "pt": "msg", "to": "event_2A", "tot": "str" }],
517
- "action": "",
518
- "property": "",
519
- "from": "",
520
- "to": "",
521
- "reg": false,
522
- "x": 750,
523
- "y": 840,
524
- "wires": [["4d466044.9e7ba"]]
525
- },
526
- {
527
- "id": "c47ba83d.c17548",
528
- "type": "delay",
529
- "z": "ee7b2f38.64383",
530
- "name": "",
531
- "pauseType": "delay",
532
- "timeout": "4",
533
- "timeoutUnits": "seconds",
534
- "rate": "1",
535
- "nbRateUnits": "1",
536
- "rateUnits": "second",
537
- "randomFirst": "1",
538
- "randomLast": "5",
539
- "randomUnits": "seconds",
540
- "drop": false,
541
- "x": 560,
542
- "y": 900,
543
- "wires": [["7e51b1f.75d755"]]
544
- },
545
- {
546
- "id": "4d466044.9e7ba",
547
- "type": "join-wait",
548
- "z": "ee7b2f38.64383",
549
- "name": "",
550
- "paths": "[\"event_2A\", \"event_2B\"]",
551
- "pathsToExpire": "",
552
- "ignoreUnmatched": false,
553
- "pathTopic": "name",
554
- "pathTopicType": "msg",
555
- "correlationTopic": "",
556
- "correlationTopicType": "msg",
557
- "timeout": "5",
558
- "timeoutUnits": "1000",
559
- "exactOrder": "false",
560
- "firstMsg": "true",
561
- "mapPayload": "true",
562
- "disableComplete": false,
563
- "x": 940,
564
- "y": 860,
565
- "wires": [["89331b9.e9fbce8"], []]
566
- },
567
- {
568
- "id": "a335f9be.14d698",
569
- "type": "change",
570
- "z": "ee7b2f38.64383",
571
- "name": "Set event_3A",
572
- "rules": [{ "t": "set", "p": "name", "pt": "msg", "to": "event_3A", "tot": "str" }],
573
- "action": "",
574
- "property": "",
575
- "from": "",
576
- "to": "",
577
- "reg": false,
578
- "x": 950,
579
- "y": 1040,
580
- "wires": [["8d4ce0e0.362b7"]]
581
- },
582
- {
583
- "id": "b3d43a9c.4f0558",
584
- "type": "inject",
585
- "z": "ee7b2f38.64383",
586
- "name": "",
587
- "topic": "",
588
- "payload": "",
589
- "payloadType": "date",
590
- "repeat": "",
591
- "crontab": "",
592
- "once": false,
593
- "onceDelay": "",
594
- "x": 780,
595
- "y": 1040,
596
- "wires": [["a335f9be.14d698"]]
597
- },
598
- {
599
- "id": "830ebf9c.f1588",
600
- "type": "debug",
601
- "z": "ee7b2f38.64383",
602
- "name": "Expired",
603
- "active": true,
604
- "tosidebar": true,
605
- "console": false,
606
- "tostatus": true,
607
- "complete": "true",
608
- "targetType": "full",
609
- "x": 1360,
610
- "y": 840,
611
- "wires": []
612
- },
613
- {
614
- "id": "f4c5e7c1.831528",
615
- "type": "debug",
616
- "z": "ee7b2f38.64383",
617
- "name": "Success",
618
- "active": true,
619
- "tosidebar": true,
620
- "console": false,
621
- "tostatus": false,
622
- "complete": "true",
623
- "targetType": "full",
624
- "x": 1360,
625
- "y": 760,
626
- "wires": []
627
- },
628
- {
629
- "id": "89331b9.e9fbce8",
630
- "type": "delay",
631
- "z": "ee7b2f38.64383",
632
- "name": "",
633
- "pauseType": "delay",
634
- "timeout": "10",
635
- "timeoutUnits": "seconds",
636
- "rate": "1",
637
- "nbRateUnits": "1",
638
- "rateUnits": "second",
639
- "randomFirst": "1",
640
- "randomLast": "5",
641
- "randomUnits": "seconds",
642
- "drop": false,
643
- "x": 1000,
644
- "y": 800,
645
- "wires": [["8d4ce0e0.362b7"]]
646
- },
647
- {
648
- "id": "d2839a03.b24e98",
649
- "type": "delay",
650
- "z": "ee7b2f38.64383",
651
- "name": "",
652
- "pauseType": "delay",
653
- "timeout": "1",
654
- "timeoutUnits": "seconds",
655
- "rate": "1",
656
- "nbRateUnits": "1",
657
- "rateUnits": "second",
658
- "randomFirst": "1",
659
- "randomLast": "5",
660
- "randomUnits": "seconds",
661
- "drop": false,
662
- "x": 560,
663
- "y": 660,
664
- "wires": [["c264c765.36e3c8"]]
665
- },
666
- {
667
- "id": "69bf4156.8d7b8",
668
- "type": "comment",
669
- "z": "ee7b2f38.64383",
670
- "name": "Expire all messages with event_3A",
671
- "info": "",
672
- "x": 860,
673
- "y": 1000,
674
- "wires": []
675
- }
676
- ]
67
+ ┌──────────────┐
68
+ input ───► │ join-wait │ ───► (1) success — merged msg
69
+ └──────────────┘ ───► (2) expired — anything that didn't make it
677
70
  ```
678
71
 
679
- </details>
72
+ Every incoming message tells the node which path it represents (via a
73
+ configurable message property — `msg.topic` by default). The node holds onto
74
+ each one until either:
680
75
 
681
- ## Example 4: Wait 10 seconds for Event 1 to fire 3 times, followed by Event 3. Must be in this exact order.
76
+ - **All wait paths arrive within the timeout** forwards a single merged
77
+ message on output 1.
78
+ - **The timeout elapses, or a reset path is seen, or `msg.complete` is set**
79
+ → forwards whatever's queued to output 2.
682
80
 
683
- - Shows an example of repeated path names
684
- - Shows an example of setting multiple paths by a single event
685
-
686
- ![Example 4](/docs/example4.png?raw=true 'Example 4')
81
+ ## Configuration
687
82
 
688
- <details><summary>Flow</summary>
83
+ | Field | What it controls |
84
+ | ---------------- | ----------------------------------------------------------------------------------------------------------- |
85
+ | **Path field** | The `msg` property whose value names the path. Default `msg.topic`. Can be a string or an object of paths. |
86
+ | **Wait paths** | The path names to wait for. Repeating an entry means "this path must arrive that many times". |
87
+ | **Timeout** | Window for all wait paths to arrive. |
88
+ | **Match order** | `Any order` (default) or `Exact order`. |
89
+ | **Group by** | Optional — only joins messages whose value at this property matches. Use `msg._msgid` for split-flow joins. |
90
+ | **Output base** | Use the first or last received message as the base of the merged output. |
91
+ | **Merge values** | Keep original path values, or overwrite each with that message's `msg.payload`. |
92
+
93
+ ### Advanced
94
+
95
+ | Field | What it controls |
96
+ | ------------------- | -------------------------------------------------------------------------------------------- |
97
+ | **Reset paths** | Path names that immediately drain the queue to the expired output. Each must be unique. |
98
+ | **Use regex** | Treat each path entry as a regular expression. |
99
+ | **Unmatched paths** | Log a warning when a message arrives with a path not in either list. |
100
+ | **Ignore complete** | Disable the `msg.complete` short-circuit. |
101
+ | **Preserve queue** | Keep the queue across deploys and (with a configured store) restarts. |
102
+ | **Persist store** | Name of a context store from `settings.js` (e.g. `localfilesystem`) for restart persistence. |
103
+
104
+ ### Per-message overrides
105
+
106
+ Any of these on an incoming message change behavior for **that message only**
107
+ (no node-level state is persisted):
108
+
109
+ - `msg.pathsToWait` — array overriding **Wait paths**.
110
+ - `msg.pathsToExpire` — array overriding **Reset paths**.
111
+ - `msg.useRegex` — boolean overriding **Use regex**.
112
+ - `msg.complete` — drains the queue to the expired output.
113
+ - `msg.reset` (boolean `true`) — silently drains the queue, no output.
114
+
115
+ ### Path field as a string vs. object
116
+
117
+ ```js
118
+ // As a string — one path per message
119
+ msg.topic = 'path_1';
120
+
121
+ // As an object — one message represents multiple paths
122
+ msg.topic = { path_1: true, path_2: true };
123
+
124
+ // As an object — values are kept in the merged output (when "Merge values"
125
+ // is set to keep original)
126
+ msg.topic = { path_1: { sensor: 'A' }, path_2: { sensor: 'B' } };
127
+ ```
689
128
 
690
- ```json
691
- [
692
- {
693
- "id": "cc9d6391.878ea",
694
- "type": "debug",
695
- "z": "d21c3caf.1c4a6",
696
- "name": "success",
697
- "active": true,
698
- "tosidebar": true,
699
- "console": false,
700
- "tostatus": false,
701
- "complete": "true",
702
- "targetType": "full",
703
- "statusVal": "",
704
- "statusType": "auto",
705
- "x": 800,
706
- "y": 120,
707
- "wires": []
708
- },
709
- {
710
- "id": "98108d1f.bc6aa",
711
- "type": "join-wait",
712
- "z": "d21c3caf.1c4a6",
713
- "name": "",
714
- "paths": "[\"path_1\",\"path_1\",\"path_1\",\"path_3\"]",
715
- "pathsToExpire": "",
716
- "useRegex": false,
717
- "warnUnmatched": false,
718
- "pathTopic": "paths",
719
- "pathTopicType": "msg",
720
- "correlationTopic": "",
721
- "correlationTopicType": "undefined",
722
- "timeout": "10",
723
- "timeoutUnits": "1000",
724
- "exactOrder": "true",
725
- "firstMsg": "true",
726
- "mapPayload": "true",
727
- "disableComplete": false,
728
- "x": 600,
729
- "y": 180,
730
- "wires": [["cc9d6391.878ea"], ["38e19f0c.ff2a9"]]
731
- },
732
- {
733
- "id": "38e19f0c.ff2a9",
734
- "type": "debug",
735
- "z": "d21c3caf.1c4a6",
736
- "name": "expired",
737
- "active": true,
738
- "tosidebar": true,
739
- "console": false,
740
- "tostatus": false,
741
- "complete": "true",
742
- "targetType": "full",
743
- "statusVal": "",
744
- "statusType": "auto",
745
- "x": 800,
746
- "y": 260,
747
- "wires": []
748
- },
749
- {
750
- "id": "9d4f0eed.15eb3",
751
- "type": "change",
752
- "z": "d21c3caf.1c4a6",
753
- "name": "Set path_1 and path_3",
754
- "rules": [
755
- { "t": "set", "p": "paths", "pt": "msg", "to": "{\"path_1\": true, \"path_3\":true}", "tot": "json" }
756
- ],
757
- "action": "",
758
- "property": "",
759
- "from": "",
760
- "to": "",
761
- "reg": false,
762
- "x": 360,
763
- "y": 260,
764
- "wires": [["98108d1f.bc6aa"]]
765
- },
766
- {
767
- "id": "c1809b24.8990f8",
768
- "type": "delay",
769
- "z": "d21c3caf.1c4a6",
770
- "name": "",
771
- "pauseType": "delay",
772
- "timeout": "1",
773
- "timeoutUnits": "seconds",
774
- "rate": "1",
775
- "nbRateUnits": "1",
776
- "rateUnits": "second",
777
- "randomFirst": "1",
778
- "randomLast": "5",
779
- "randomUnits": "seconds",
780
- "drop": false,
781
- "x": 160,
782
- "y": 260,
783
- "wires": [["9d4f0eed.15eb3"]]
784
- },
785
- {
786
- "id": "b69f3131.98969",
787
- "type": "inject",
788
- "z": "d21c3caf.1c4a6",
789
- "name": "",
790
- "repeat": "",
791
- "crontab": "",
792
- "once": false,
793
- "onceDelay": "",
794
- "topic": "",
795
- "payload": "",
796
- "payloadType": "date",
797
- "x": 140,
798
- "y": 120,
799
- "wires": [["bb0b9bc5.c65ee8", "c1809b24.8990f8", "8969e7ba.e36038"]]
800
- },
801
- {
802
- "id": "bb0b9bc5.c65ee8",
803
- "type": "change",
804
- "z": "d21c3caf.1c4a6",
805
- "name": "Set path_1",
806
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_1", "tot": "str" }],
807
- "action": "",
808
- "property": "",
809
- "from": "",
810
- "to": "",
811
- "reg": false,
812
- "x": 330,
813
- "y": 120,
814
- "wires": [["98108d1f.bc6aa"]]
815
- },
816
- {
817
- "id": "8969e7ba.e36038",
818
- "type": "change",
819
- "z": "d21c3caf.1c4a6",
820
- "name": "Set path_1",
821
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_1", "tot": "str" }],
822
- "action": "",
823
- "property": "",
824
- "from": "",
825
- "to": "",
826
- "reg": false,
827
- "x": 330,
828
- "y": 180,
829
- "wires": [["98108d1f.bc6aa"]]
830
- }
831
- ]
129
+ ## Behavior reference
130
+
131
+ - When the same wait path arrives more than required, only the latest value
132
+ is kept in the merged output.
133
+ - In **Exact order** mode, unexpected paths between expected ones are
134
+ tolerated.
135
+ - In any-order mode with regex, paths are counted greedily left-to-right.
136
+ `["path_[12]", "path_2"]` will never complete because every `path_2`
137
+ arrival gets attributed to the first regex.
138
+ - Pad **Timeout** with a small overhead (~5–10 ms) when working with very
139
+ short windows — evaluation isn't free.
140
+ - The success output reuses the chosen base message (first or last) and
141
+ attaches the merged data on the **Path field** property.
142
+
143
+ ## Persistence
144
+
145
+ Queues live in [Node-RED's context store](https://nodered.org/docs/user-guide/context).
146
+ **Preserve queue** is on by default, so partial joins survive a redeploy
147
+ out of the box (the default in-memory store keeps state in-process).
148
+
149
+ To survive **full Node-RED restarts**, configure a persistent context
150
+ store in `settings.js`. The simplest setup:
151
+
152
+ ```js
153
+ contextStorage: {
154
+ default: { module: 'localfilesystem' },
155
+ }
832
156
  ```
833
157
 
834
- </details>
158
+ Every `join-wait` node then uses the default store automatically.
835
159
 
836
- ## Example 5: Wait 10 seconds for the exact event order "path_1", "path_2", "path_1", "path_2".
160
+ If you have a mixed setup with `memory` as the default and a separate
161
+ persistent store (e.g. for archival), `join-wait` is smart about it:
162
+ when `Preserve queue` is on but the default store is memory, the node
163
+ **auto-picks** the first non-memory named store and logs which one it
164
+ chose. So this configuration also gets restart persistence for free:
837
165
 
838
- - Shows an example of repeated path names
839
- - Shows an example of an unexpected path name being observed. This can be caught by another node, if desired.
166
+ ```js
167
+ contextStorage: {
168
+ default: { module: 'memory' },
169
+ file: { module: 'localfilesystem' }, // auto-picked
170
+ }
171
+ ```
840
172
 
841
- ![Example 5](/docs/example5.png?raw=true 'Example 5')
173
+ Set **Persist store** explicitly on a node to override the auto-pick.
842
174
 
843
- <details><summary>Flow</summary>
175
+ ## Example flows
844
176
 
845
- ```json
846
- [
847
- {
848
- "id": "cc9d6391.878ea",
849
- "type": "debug",
850
- "z": "d21c3caf.1c4a6",
851
- "name": "success",
852
- "active": true,
853
- "tosidebar": true,
854
- "console": false,
855
- "tostatus": false,
856
- "complete": "true",
857
- "targetType": "full",
858
- "statusVal": "",
859
- "statusType": "auto",
860
- "x": 800,
861
- "y": 120,
862
- "wires": []
863
- },
864
- {
865
- "id": "98108d1f.bc6aa",
866
- "type": "join-wait",
867
- "z": "d21c3caf.1c4a6",
868
- "name": "",
869
- "paths": "[\"path_1\",\"path_2\",\"path_1\",\"path_2\"]",
870
- "pathsToExpire": "",
871
- "useRegex": false,
872
- "warnUnmatched": true,
873
- "pathTopic": "paths",
874
- "pathTopicType": "msg",
875
- "correlationTopic": "",
876
- "correlationTopicType": "undefined",
877
- "timeout": "10",
878
- "timeoutUnits": "1000",
879
- "exactOrder": "true",
880
- "firstMsg": "true",
881
- "mapPayload": "true",
882
- "disableComplete": false,
883
- "x": 640,
884
- "y": 160,
885
- "wires": [["cc9d6391.878ea"], ["38e19f0c.ff2a9"]]
886
- },
887
- {
888
- "id": "38e19f0c.ff2a9",
889
- "type": "debug",
890
- "z": "d21c3caf.1c4a6",
891
- "name": "expired",
892
- "active": true,
893
- "tosidebar": true,
894
- "console": false,
895
- "tostatus": false,
896
- "complete": "true",
897
- "targetType": "full",
898
- "statusVal": "",
899
- "statusType": "auto",
900
- "x": 800,
901
- "y": 240,
902
- "wires": []
903
- },
904
- {
905
- "id": "c1809b24.8990f8",
906
- "type": "delay",
907
- "z": "d21c3caf.1c4a6",
908
- "name": "",
909
- "pauseType": "delay",
910
- "timeout": "1",
911
- "timeoutUnits": "seconds",
912
- "rate": "1",
913
- "nbRateUnits": "1",
914
- "rateUnits": "second",
915
- "randomFirst": "1",
916
- "randomLast": "5",
917
- "randomUnits": "seconds",
918
- "drop": false,
919
- "x": 160,
920
- "y": 200,
921
- "wires": [["bb0b9bc5.c65ee8", "2f5d90da.fabdf", "77c09e21.8cb11"]]
922
- },
923
- {
924
- "id": "b69f3131.98969",
925
- "type": "inject",
926
- "z": "d21c3caf.1c4a6",
927
- "name": "",
928
- "repeat": "",
929
- "crontab": "",
930
- "once": false,
931
- "onceDelay": "",
932
- "topic": "",
933
- "payload": "",
934
- "payloadType": "date",
935
- "x": 140,
936
- "y": 120,
937
- "wires": [["bb0b9bc5.c65ee8", "2f5d90da.fabdf", "c1809b24.8990f8"]]
938
- },
939
- {
940
- "id": "bb0b9bc5.c65ee8",
941
- "type": "change",
942
- "z": "d21c3caf.1c4a6",
943
- "name": "Set path_1",
944
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_1", "tot": "str" }],
945
- "action": "",
946
- "property": "",
947
- "from": "",
948
- "to": "",
949
- "reg": false,
950
- "x": 430,
951
- "y": 100,
952
- "wires": [["98108d1f.bc6aa"]]
953
- },
954
- {
955
- "id": "8969e7ba.e36038",
956
- "type": "change",
957
- "z": "d21c3caf.1c4a6",
958
- "name": "Set path_2",
959
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_2", "tot": "str" }],
960
- "action": "",
961
- "property": "",
962
- "from": "",
963
- "to": "",
964
- "reg": false,
965
- "x": 470,
966
- "y": 220,
967
- "wires": [["98108d1f.bc6aa"]]
968
- },
969
- {
970
- "id": "2f5d90da.fabdf",
971
- "type": "delay",
972
- "z": "d21c3caf.1c4a6",
973
- "name": "",
974
- "pauseType": "delay",
975
- "timeout": "100",
976
- "timeoutUnits": "milliseconds",
977
- "rate": "1",
978
- "nbRateUnits": "1",
979
- "rateUnits": "second",
980
- "randomFirst": "1",
981
- "randomLast": "5",
982
- "randomUnits": "seconds",
983
- "drop": false,
984
- "x": 430,
985
- "y": 160,
986
- "wires": [["8969e7ba.e36038"]]
987
- },
988
- {
989
- "id": "77c09e21.8cb11",
990
- "type": "change",
991
- "z": "d21c3caf.1c4a6",
992
- "name": "Set path_3",
993
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_3", "tot": "str" }],
994
- "action": "",
995
- "property": "",
996
- "from": "",
997
- "to": "",
998
- "reg": false,
999
- "x": 470,
1000
- "y": 260,
1001
- "wires": [["98108d1f.bc6aa"]]
1002
- }
1003
- ]
1004
- ```
177
+ After install, **Menu → Import → Examples → join-wait** lists ready-made
178
+ flows:
1005
179
 
1006
- </details>
180
+ 1. **Quickstart** — wait for two paths within 5 s.
181
+ 2. **Correlation** — group messages from a split flow via `_msgid`.
182
+ 3. **Reset paths** — abort the queue when a reset path arrives.
183
+ 4. **Regex paths** — match path names by regular expression.
184
+ 5. **Exact order** — require a specific sequence (with a repeated step).
1007
185
 
1008
- ## Example 6: Wait 10 seconds for two "path_1" events and one "path_2" event in any order.
186
+ ## Compatibility
1009
187
 
1010
- ![Example 6](/docs/example6.png?raw=true 'Example 6')
188
+ - Node-RED 3.x and 4.x.
189
+ - Node.js 20 / 22 / 24.
1011
190
 
1012
- <details><summary>Flow</summary>
191
+ ## Migrating from 0.5.x
1013
192
 
1014
- ```json
1015
- [
1016
- {
1017
- "id": "cc9d6391.878ea",
1018
- "type": "debug",
1019
- "z": "d21c3caf.1c4a6",
1020
- "name": "success",
1021
- "active": true,
1022
- "tosidebar": true,
1023
- "console": false,
1024
- "tostatus": false,
1025
- "complete": "true",
1026
- "targetType": "full",
1027
- "statusVal": "",
1028
- "statusType": "auto",
1029
- "x": 800,
1030
- "y": 120,
1031
- "wires": []
1032
- },
1033
- {
1034
- "id": "98108d1f.bc6aa",
1035
- "type": "join-wait",
1036
- "z": "d21c3caf.1c4a6",
1037
- "name": "",
1038
- "paths": "[\"path_1\",\"path_2\",\"path_1\"]",
1039
- "pathsToExpire": "",
1040
- "useRegex": false,
1041
- "warnUnmatched": true,
1042
- "pathTopic": "paths",
1043
- "pathTopicType": "msg",
1044
- "correlationTopic": "",
1045
- "correlationTopicType": "undefined",
1046
- "timeout": "10",
1047
- "timeoutUnits": "1000",
1048
- "exactOrder": "false",
1049
- "firstMsg": "true",
1050
- "mapPayload": "true",
1051
- "disableComplete": false,
1052
- "x": 640,
1053
- "y": 160,
1054
- "wires": [["cc9d6391.878ea"], ["38e19f0c.ff2a9"]]
1055
- },
1056
- {
1057
- "id": "38e19f0c.ff2a9",
1058
- "type": "debug",
1059
- "z": "d21c3caf.1c4a6",
1060
- "name": "expired",
1061
- "active": true,
1062
- "tosidebar": true,
1063
- "console": false,
1064
- "tostatus": false,
1065
- "complete": "true",
1066
- "targetType": "full",
1067
- "statusVal": "",
1068
- "statusType": "auto",
1069
- "x": 800,
1070
- "y": 240,
1071
- "wires": []
1072
- },
1073
- {
1074
- "id": "b69f3131.98969",
1075
- "type": "inject",
1076
- "z": "d21c3caf.1c4a6",
1077
- "name": "",
1078
- "repeat": "",
1079
- "crontab": "",
1080
- "once": false,
1081
- "onceDelay": "",
1082
- "topic": "",
1083
- "payload": "",
1084
- "payloadType": "date",
1085
- "x": 140,
1086
- "y": 180,
1087
- "wires": [["1546292e.21d097", "25f3441b.c9fd9c", "3c1f3edc.6a53c2"]]
1088
- },
1089
- {
1090
- "id": "bb0b9bc5.c65ee8",
1091
- "type": "change",
1092
- "z": "d21c3caf.1c4a6",
1093
- "name": "Set path_1",
1094
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_1", "tot": "str" }],
1095
- "action": "",
1096
- "property": "",
1097
- "from": "",
1098
- "to": "",
1099
- "reg": false,
1100
- "x": 450,
1101
- "y": 140,
1102
- "wires": [["98108d1f.bc6aa"]]
1103
- },
1104
- {
1105
- "id": "8969e7ba.e36038",
1106
- "type": "change",
1107
- "z": "d21c3caf.1c4a6",
1108
- "name": "Set path_1",
1109
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_1", "tot": "str" }],
1110
- "action": "",
1111
- "property": "",
1112
- "from": "",
1113
- "to": "",
1114
- "reg": false,
1115
- "x": 450,
1116
- "y": 180,
1117
- "wires": [["98108d1f.bc6aa"]]
1118
- },
1119
- {
1120
- "id": "77c09e21.8cb11",
1121
- "type": "change",
1122
- "z": "d21c3caf.1c4a6",
1123
- "name": "Set path_2",
1124
- "rules": [{ "t": "set", "p": "paths", "pt": "msg", "to": "path_2", "tot": "str" }],
1125
- "action": "",
1126
- "property": "",
1127
- "from": "",
1128
- "to": "",
1129
- "reg": false,
1130
- "x": 450,
1131
- "y": 220,
1132
- "wires": [["98108d1f.bc6aa"]]
1133
- },
1134
- {
1135
- "id": "25f3441b.c9fd9c",
1136
- "type": "delay",
1137
- "z": "d21c3caf.1c4a6",
1138
- "name": "",
1139
- "pauseType": "random",
1140
- "timeout": "1",
1141
- "timeoutUnits": "seconds",
1142
- "rate": "1",
1143
- "nbRateUnits": "1",
1144
- "rateUnits": "second",
1145
- "randomFirst": "1",
1146
- "randomLast": "25",
1147
- "randomUnits": "milliseconds",
1148
- "drop": false,
1149
- "x": 300,
1150
- "y": 140,
1151
- "wires": [["bb0b9bc5.c65ee8"]]
1152
- },
1153
- {
1154
- "id": "1546292e.21d097",
1155
- "type": "delay",
1156
- "z": "d21c3caf.1c4a6",
1157
- "name": "",
1158
- "pauseType": "random",
1159
- "timeout": "1",
1160
- "timeoutUnits": "seconds",
1161
- "rate": "1",
1162
- "nbRateUnits": "1",
1163
- "rateUnits": "second",
1164
- "randomFirst": "1",
1165
- "randomLast": "25",
1166
- "randomUnits": "milliseconds",
1167
- "drop": false,
1168
- "x": 300,
1169
- "y": 180,
1170
- "wires": [["8969e7ba.e36038"]]
1171
- },
1172
- {
1173
- "id": "3c1f3edc.6a53c2",
1174
- "type": "delay",
1175
- "z": "d21c3caf.1c4a6",
1176
- "name": "",
1177
- "pauseType": "random",
1178
- "timeout": "1",
1179
- "timeoutUnits": "seconds",
1180
- "rate": "1",
1181
- "nbRateUnits": "1",
1182
- "rateUnits": "second",
1183
- "randomFirst": "1",
1184
- "randomLast": "25",
1185
- "randomUnits": "milliseconds",
1186
- "drop": false,
1187
- "x": 300,
1188
- "y": 220,
1189
- "wires": [["77c09e21.8cb11"]]
1190
- }
1191
- ]
1192
- ```
193
+ Version 0.6 is mostly drop-in compatible, with two intentional behavior
194
+ tweaks:
1193
195
 
1194
- </details>
196
+ - `msg.pathsToWait`, `msg.pathsToExpire`, and `msg.useRegex` are now
197
+ **one-shot** overrides — they affect only the current message and no
198
+ longer mutate the node's stored config.
199
+ - Persistence moved from a `node-persist` singleton to Node-RED's context
200
+ store (fixes a multi-node race). For restart persistence, configure a
201
+ persistent store in `settings.js` and set **Persist store**.
1195
202
 
1196
- ## :question: Get Help
203
+ The configuration field for paths now stores a real array; the legacy JSON
204
+ string form (`'["a","b"]'`) is still accepted at runtime so existing flows
205
+ keep working without re-deploying.
1197
206
 
1198
- For bug reports and feature requests, open issues. :bug:
207
+ See [CHANGELOG.md](CHANGELOG.md) for the full list of changes.
1199
208
 
1200
- ## Installation
209
+ ## Contributing
1201
210
 
1202
- First of all, install [Node-RED](http://nodered.org/docs/getting-started/installation)
211
+ Issues and PRs welcome — please run `npm test`, `npm run lint`, and
212
+ `npm run spellcheck` locally before sending.
1203
213
 
1204
- ```sh
1205
- # Then open the user data directory `~/.node-red` and install the package
1206
- $ cd ~/.node-red
1207
- $ npm install node-red-contrib-join-wait
1208
- ```
214
+ ## Support this project
1209
215
 
1210
- Or search for `join-wait` in the manage palette menu
216
+ If this project saved you time and you'd like to send a tip:
1211
217
 
1212
- ## How to contribute
218
+ - [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/ddcaspi) one-time PayPal donations.
219
+ - **Venmo** — one-time donations via Venmo:
1213
220
 
1214
- Have an idea? Found a bug? Contributions and pull requests are welcome.
221
+ ![Venmo QR](docs/venmo.png?raw=true 'Venmo QR Code')
1215
222
 
1216
- ## Support my projects
223
+ A GitHub star also goes a long way.
1217
224
 
1218
- I try to reply to everyone needing help using these projects. Obviously, this takes time. However, if you get some profit from this or just want to encourage me to continue creating stuff, there are few ways you can do it:
225
+ ## License
1219
226
 
1220
- - Starring and sharing the projects you like :rocket:
1221
- - [![PayPal][badge_paypal]][paypal-donations] **PayPal**— You can make one-time donations via PayPal.
1222
- - **Venmo**— You can make one-time donations via Venmo.
1223
- ![Venmo QR Code](/docs/venmo.png?raw=true 'Venmo QR Code')
1224
- - **Bitcoin**— You can send me Bitcoin at this address: `33sT6xw3tZWAdP2oL4ygbH5TVpVMfk9VW7`
227
+ MIT see [LICENSE](LICENSE).
1225
228
 
1226
229
  ## Credits
1227
230
 
1228
- - Thanks to mauriciom75's https://github.com/mauriciom75/node-red-contrib-wait-paths
1229
-
1230
- ## MIT License
1231
-
1232
- [badge_paypal]: https://img.shields.io/badge/Donate-PayPal-blue.svg
1233
- [paypal-donations]: https://paypal.me/ddcaspi
231
+ Originally inspired by [mauriciom75/node-red-contrib-wait-paths](https://github.com/mauriciom75/node-red-contrib-wait-paths).