https://github.com/dxdc/node-red-contrib-join-wait
Node-RED module to wait for incoming messages from different input paths to arrive within a fixed time window.
https://github.com/dxdc/node-red-contrib-join-wait
home-automation homebridge join merge node-red nodered parallel time timeout wait
Last synced: 14 days ago
JSON representation
Node-RED module to wait for incoming messages from different input paths to arrive within a fixed time window.
- Host: GitHub
- URL: https://github.com/dxdc/node-red-contrib-join-wait
- Owner: dxdc
- License: mit
- Created: 2020-01-14T19:06:58.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2022-02-06T01:29:59.000Z (over 4 years ago)
- Last Synced: 2025-08-09T12:18:20.301Z (10 months ago)
- Topics: home-automation, homebridge, join, merge, node-red, nodered, parallel, time, timeout, wait
- Language: JavaScript
- Homepage: https://flows.nodered.org/node/node-red-contrib-join-wait
- Size: 627 KB
- Stars: 8
- Watchers: 4
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# node-red-contrib-join-wait
[](https://github.com/dxdc/node-red-contrib-join-wait/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/node-red-contrib-join-wait)
[](https://www.npmjs.com/package/node-red-contrib-join-wait)
[](LICENSE)
A Node-RED node that joins related messages across multiple paths within a
time window — with exact-order matching, regex paths, correlation grouping,
reset paths, and queue persistence. Coordinate parallel flows, synchronize
events, and debounce sensors.
If all the named paths arrive in time, a merged message is emitted on the
**success** output. Anything left over goes to the **expired** output for
optional follow-up.
## Use cases
- **Debounce a motion sensor** when a light turning on/off is also tripping
it: only fire if `light_off` then `motion` then `light_on` all arrive
within 10 s.
- **Correlate request lifecycle events** — wait for `request_started` and
`request_finished` with the same correlation id and emit one log entry
with the duration.
- **Sensor consensus** — only act when both `door_open` and `vibration`
fire within 2 s.
- **Recombine fanned-out API calls** — join the responses from a
`split` flow when each branch tags its result with a known path name.
- **Watchdog gate** — let `start` plus N progress beats arrive within a
window, otherwise route to an alarm flow via the expired output.
## Is this the right node? `join-wait` vs. the stock `join` node
| You need to… | Stock `join` | `join-wait` |
| ------------------------------------------------------ | :----------: | :---------: |
| Recombine pieces of a `split` message (`msg.parts`) | ✅ | – |
| Concatenate strings, arrays, or buffers | ✅ | – |
| Count "any N messages" into a batch | ✅ | – |
| Wait for **specific named** paths within a time window | – | ✅ |
| Match path names by regex | – | ✅ |
| Require an **exact order** (with repeats) | – | ✅ |
| Drain the queue when a "reset" path arrives | – | ✅ |
| Group by an arbitrary correlation expression | – | ✅ |
**Rule of thumb:** if you split a message and want to put it back
together, use the stock `join`. If you have heterogeneous events from
different sources and want to coordinate them, use `join-wait`.
## Quick start
```sh
cd ~/.node-red
npm install node-red-contrib-join-wait
```
Or search **`join-wait`** in **Manage palette → Install**.
Open the editor, drag a `join-wait` node onto a flow, and add the path names
you want to wait for. That's it.
> Looking for a working flow? After install, open
> **Menu → Import → Examples → join-wait** for ready-made flows.
## How it works
```
┌──────────────┐
input ───► │ join-wait │ ───► (1) success — merged msg
└──────────────┘ ───► (2) expired — anything that didn't make it
```
Every incoming message tells the node which path it represents (via a
configurable message property — `msg.topic` by default). The node holds onto
each one until either:
- **All wait paths arrive within the timeout** → forwards a single merged
message on output 1.
- **The timeout elapses, or a reset path is seen, or `msg.complete` is set**
→ forwards whatever's queued to output 2.
## Configuration
| Field | What it controls |
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
| **Path field** | The `msg` property whose value names the path. Default `msg.topic`. Can be a string or an object of paths. |
| **Wait paths** | The path names to wait for. Repeating an entry means "this path must arrive that many times". |
| **Timeout** | Window for all wait paths to arrive. |
| **Match order** | `Any order` (default) or `Exact order`. |
| **Group by** | Optional — only joins messages whose value at this property matches. Use `msg._msgid` for split-flow joins. |
| **Output base** | Use the first or last received message as the base of the merged output. |
| **Merge values** | Keep original path values, or overwrite each with that message's `msg.payload`. |
### Advanced
| Field | What it controls |
| ------------------- | -------------------------------------------------------------------------------------------- |
| **Reset paths** | Path names that immediately drain the queue to the expired output. Each must be unique. |
| **Use regex** | Treat each path entry as a regular expression. |
| **Unmatched paths** | Log a warning when a message arrives with a path not in either list. |
| **Ignore complete** | Disable the `msg.complete` short-circuit. |
| **Preserve queue** | Keep the queue across deploys and (with a configured store) restarts. |
| **Persist store** | Name of a context store from `settings.js` (e.g. `localfilesystem`) for restart persistence. |
### Per-message overrides
Any of these on an incoming message change behavior for **that message only**
(no node-level state is persisted):
- `msg.pathsToWait` — array overriding **Wait paths**.
- `msg.pathsToExpire` — array overriding **Reset paths**.
- `msg.useRegex` — boolean overriding **Use regex**.
- `msg.complete` — drains the queue to the expired output.
- `msg.reset` (boolean `true`) — silently drains the queue, no output.
### Path field as a string vs. object
```js
// As a string — one path per message
msg.topic = 'path_1';
// As an object — one message represents multiple paths
msg.topic = { path_1: true, path_2: true };
// As an object — values are kept in the merged output (when "Merge values"
// is set to keep original)
msg.topic = { path_1: { sensor: 'A' }, path_2: { sensor: 'B' } };
```
## Behavior reference
- When the same wait path arrives more than required, only the latest value
is kept in the merged output.
- In **Exact order** mode, unexpected paths between expected ones are
tolerated.
- In any-order mode with regex, paths are counted greedily left-to-right.
`["path_[12]", "path_2"]` will never complete because every `path_2`
arrival gets attributed to the first regex.
- Pad **Timeout** with a small overhead (~5–10 ms) when working with very
short windows — evaluation isn't free.
- The success output reuses the chosen base message (first or last) and
attaches the merged data on the **Path field** property.
## Persistence
Queues live in [Node-RED's context store](https://nodered.org/docs/user-guide/context).
**Preserve queue** is on by default, so partial joins survive a redeploy
out of the box (the default in-memory store keeps state in-process).
To survive **full Node-RED restarts**, configure a persistent context
store in `settings.js`. The simplest setup:
```js
contextStorage: {
default: { module: 'localfilesystem' },
}
```
Every `join-wait` node then uses the default store automatically.
If you have a mixed setup with `memory` as the default and a separate
persistent store (e.g. for archival), `join-wait` is smart about it:
when `Preserve queue` is on but the default store is memory, the node
**auto-picks** the first non-memory named store and logs which one it
chose. So this configuration also gets restart persistence for free:
```js
contextStorage: {
default: { module: 'memory' },
file: { module: 'localfilesystem' }, // auto-picked
}
```
Set **Persist store** explicitly on a node to override the auto-pick.
## Example flows
After install, **Menu → Import → Examples → join-wait** lists ready-made
flows:
1. **Quickstart** — wait for two paths within 5 s.
2. **Correlation** — group messages from a split flow via `_msgid`.
3. **Reset paths** — abort the queue when a reset path arrives.
4. **Regex paths** — match path names by regular expression.
5. **Exact order** — require a specific sequence (with a repeated step).
## Compatibility
- Node-RED 3.x and 4.x.
- Node.js 20 / 22 / 24.
## Migrating from 0.5.x
Version 0.6 is mostly drop-in compatible, with two intentional behavior
tweaks:
- `msg.pathsToWait`, `msg.pathsToExpire`, and `msg.useRegex` are now
**one-shot** overrides — they affect only the current message and no
longer mutate the node's stored config.
- Persistence moved from a `node-persist` singleton to Node-RED's context
store (fixes a multi-node race). For restart persistence, configure a
persistent store in `settings.js` and set **Persist store**.
The configuration field for paths now stores a real array; the legacy JSON
string form (`'["a","b"]'`) is still accepted at runtime so existing flows
keep working without re-deploying.
See [CHANGELOG.md](CHANGELOG.md) for the full list of changes.
## Contributing
Issues and PRs welcome — please run `npm test`, `npm run lint`, and
`npm run spellcheck` locally before sending.
## Support this project
If this project saved you time and you'd like to send a tip:
- [](https://paypal.me/ddcaspi) — one-time PayPal donations.
- **Venmo** — one-time donations via Venmo:

A GitHub star also goes a long way.
## License
MIT — see [LICENSE](LICENSE).
## Credits
Originally inspired by [mauriciom75/node-red-contrib-wait-paths](https://github.com/mauriciom75/node-red-contrib-wait-paths).