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/CHANGELOG.md +181 -0
- package/README.md +190 -1192
- package/examples/01-quickstart.json +80 -0
- package/examples/02-correlation.json +110 -0
- package/examples/03-reset.json +88 -0
- package/examples/04-regex.json +84 -0
- package/examples/05-exact-order.json +88 -0
- package/join-wait.html +472 -185
- package/join-wait.js +352 -361
- package/lib/config.js +26 -0
- package/lib/matcher.js +114 -0
- package/lib/persist.js +60 -0
- package/lib/store.js +64 -0
- package/package.json +91 -56
- package/.eslintrc.js +0 -24
- package/.nycrc.json +0 -11
- package/.prettierrc +0 -6
- package/.travis.yml +0 -20
- package/docs/_config.yml +0 -1
- package/docs/example1.png +0 -0
- package/docs/example2.png +0 -0
- package/docs/example3.png +0 -0
- package/docs/example4.png +0 -0
- package/docs/example5.png +0 -0
- package/docs/example6.png +0 -0
- package/docs/venmo.png +0 -0
- package/test/flows.js +0 -33
- package/test/test_spec.js +0 -1411
package/README.md
CHANGED
|
@@ -1,1233 +1,231 @@
|
|
|
1
|
-
#
|
|
1
|
+
# node-red-contrib-join-wait
|
|
2
|
+
|
|
3
|
+
[](https://github.com/dxdc/node-red-contrib-join-wait/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/node-red-contrib-join-wait)
|
|
5
|
+
[](https://www.npmjs.com/package/node-red-contrib-join-wait)
|
|
6
|
+
[](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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[](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
|
-

|
|
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
|
-
|
|
56
|
+
Or search **`join-wait`** in **Manage palette → Install**.
|
|
215
57
|
|
|
216
|
-
|
|
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
|
-
|
|
61
|
+
> Looking for a working flow? After install, open
|
|
62
|
+
> **Menu → Import → Examples → join-wait** for ready-made flows.
|
|
219
63
|
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
- Shows an example of setting multiple paths by a single event
|
|
685
|
-
|
|
686
|
-

|
|
81
|
+
## Configuration
|
|
687
82
|
|
|
688
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
158
|
+
Every `join-wait` node then uses the default store automatically.
|
|
835
159
|
|
|
836
|
-
|
|
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
|
-
|
|
839
|
-
|
|
166
|
+
```js
|
|
167
|
+
contextStorage: {
|
|
168
|
+
default: { module: 'memory' },
|
|
169
|
+
file: { module: 'localfilesystem' }, // auto-picked
|
|
170
|
+
}
|
|
171
|
+
```
|
|
840
172
|
|
|
841
|
-
|
|
173
|
+
Set **Persist store** explicitly on a node to override the auto-pick.
|
|
842
174
|
|
|
843
|
-
|
|
175
|
+
## Example flows
|
|
844
176
|
|
|
845
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
186
|
+
## Compatibility
|
|
1009
187
|
|
|
1010
|
-
|
|
188
|
+
- Node-RED 3.x and 4.x.
|
|
189
|
+
- Node.js 20 / 22 / 24.
|
|
1011
190
|
|
|
1012
|
-
|
|
191
|
+
## Migrating from 0.5.x
|
|
1013
192
|
|
|
1014
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
See [CHANGELOG.md](CHANGELOG.md) for the full list of changes.
|
|
1199
208
|
|
|
1200
|
-
##
|
|
209
|
+
## Contributing
|
|
1201
210
|
|
|
1202
|
-
|
|
211
|
+
Issues and PRs welcome — please run `npm test`, `npm run lint`, and
|
|
212
|
+
`npm run spellcheck` locally before sending.
|
|
1203
213
|
|
|
1204
|
-
|
|
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
|
-
|
|
216
|
+
If this project saved you time and you'd like to send a tip:
|
|
1211
217
|
|
|
1212
|
-
|
|
218
|
+
- [](https://paypal.me/ddcaspi) — one-time PayPal donations.
|
|
219
|
+
- **Venmo** — one-time donations via Venmo:
|
|
1213
220
|
|
|
1214
|
-
|
|
221
|
+

|
|
1215
222
|
|
|
1216
|
-
|
|
223
|
+
A GitHub star also goes a long way.
|
|
1217
224
|
|
|
1218
|
-
|
|
225
|
+
## License
|
|
1219
226
|
|
|
1220
|
-
|
|
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
|
-

|
|
1224
|
-
- **Bitcoin**— You can send me Bitcoin at this address: `33sT6xw3tZWAdP2oL4ygbH5TVpVMfk9VW7`
|
|
227
|
+
MIT — see [LICENSE](LICENSE).
|
|
1225
228
|
|
|
1226
229
|
## Credits
|
|
1227
230
|
|
|
1228
|
-
|
|
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).
|