{"id":16259697,"url":"https://github.com/dxdc/node-red-contrib-join-wait","last_synced_at":"2026-05-08T23:20:18.188Z","repository":{"id":118374742,"uuid":"233915143","full_name":"dxdc/node-red-contrib-join-wait","owner":"dxdc","description":"Node-RED module to wait for incoming messages from different input paths to arrive within a fixed time window.","archived":false,"fork":false,"pushed_at":"2022-02-06T01:29:59.000Z","size":642,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-08-09T12:18:20.301Z","etag":null,"topics":["home-automation","homebridge","join","merge","node-red","nodered","parallel","time","timeout","wait"],"latest_commit_sha":null,"homepage":"https://flows.nodered.org/node/node-red-contrib-join-wait","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dxdc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"dxdc","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2020-01-14T19:06:58.000Z","updated_at":"2024-05-31T08:36:04.000Z","dependencies_parsed_at":"2023-05-22T19:30:41.615Z","dependency_job_id":null,"html_url":"https://github.com/dxdc/node-red-contrib-join-wait","commit_stats":{"total_commits":45,"total_committers":1,"mean_commits":45.0,"dds":0.0,"last_synced_commit":"767d98a56da3fdc02bdf8b65c7d259f2e0d8220b"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/dxdc/node-red-contrib-join-wait","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dxdc%2Fnode-red-contrib-join-wait","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dxdc%2Fnode-red-contrib-join-wait/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dxdc%2Fnode-red-contrib-join-wait/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dxdc%2Fnode-red-contrib-join-wait/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dxdc","download_url":"https://codeload.github.com/dxdc/node-red-contrib-join-wait/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dxdc%2Fnode-red-contrib-join-wait/sbom","scorecard":{"id":361805,"data":{"date":"2025-08-11","repo":{"name":"github.com/dxdc/node-red-contrib-join-wait","commit":"767d98a56da3fdc02bdf8b65c7d259f2e0d8220b"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-18T11:02:25.437Z","repository_id":118374742,"created_at":"2025-08-18T11:02:25.437Z","updated_at":"2025-08-18T11:02:25.437Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274146689,"owners_count":25230116,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-08T02:00:09.813Z","response_time":121,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["home-automation","homebridge","join","merge","node-red","nodered","parallel","time","timeout","wait"],"created_at":"2024-10-10T16:04:22.987Z","updated_at":"2026-05-08T23:20:18.181Z","avatar_url":"https://github.com/dxdc.png","language":"JavaScript","funding_links":["https://github.com/sponsors/dxdc","https://paypal.me/ddcaspi"],"categories":[],"sub_categories":[],"readme":"# node-red-contrib-join-wait\n\n[![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)\n[![npm](https://img.shields.io/npm/v/node-red-contrib-join-wait.svg)](https://www.npmjs.com/package/node-red-contrib-join-wait)\n[![npm downloads](https://img.shields.io/npm/dt/node-red-contrib-join-wait.svg)](https://www.npmjs.com/package/node-red-contrib-join-wait)\n[![license](https://img.shields.io/npm/l/node-red-contrib-join-wait.svg)](LICENSE)\n\nA Node-RED node that joins related messages across multiple paths within a\ntime window — with exact-order matching, regex paths, correlation grouping,\nreset paths, and queue persistence. Coordinate parallel flows, synchronize\nevents, and debounce sensors.\n\nIf all the named paths arrive in time, a merged message is emitted on the\n**success** output. Anything left over goes to the **expired** output for\noptional follow-up.\n\n## Use cases\n\n- **Debounce a motion sensor** when a light turning on/off is also tripping\n  it: only fire if `light_off` then `motion` then `light_on` all arrive\n  within 10 s.\n- **Correlate request lifecycle events** — wait for `request_started` and\n  `request_finished` with the same correlation id and emit one log entry\n  with the duration.\n- **Sensor consensus** — only act when both `door_open` and `vibration`\n  fire within 2 s.\n- **Recombine fanned-out API calls** — join the responses from a\n  `split` flow when each branch tags its result with a known path name.\n- **Watchdog gate** — let `start` plus N progress beats arrive within a\n  window, otherwise route to an alarm flow via the expired output.\n\n## Is this the right node? `join-wait` vs. the stock `join` node\n\n| You need to…                                           | Stock `join` | `join-wait` |\n| ------------------------------------------------------ | :----------: | :---------: |\n| Recombine pieces of a `split` message (`msg.parts`)    |      ✅      |      –      |\n| Concatenate strings, arrays, or buffers                |      ✅      |      –      |\n| Count \"any N messages\" into a batch                    |      ✅      |      –      |\n| Wait for **specific named** paths within a time window |      –       |     ✅      |\n| Match path names by regex                              |      –       |     ✅      |\n| Require an **exact order** (with repeats)              |      –       |     ✅      |\n| Drain the queue when a \"reset\" path arrives            |      –       |     ✅      |\n| Group by an arbitrary correlation expression           |      –       |     ✅      |\n\n**Rule of thumb:** if you split a message and want to put it back\ntogether, use the stock `join`. If you have heterogeneous events from\ndifferent sources and want to coordinate them, use `join-wait`.\n\n## Quick start\n\n```sh\ncd ~/.node-red\nnpm install node-red-contrib-join-wait\n```\n\nOr search **`join-wait`** in **Manage palette → Install**.\n\nOpen the editor, drag a `join-wait` node onto a flow, and add the path names\nyou want to wait for. That's it.\n\n\u003e Looking for a working flow? After install, open\n\u003e **Menu → Import → Examples → join-wait** for ready-made flows.\n\n## How it works\n\n```\n              ┌──────────────┐\ninput  ───►   │   join-wait  │  ───► (1) success — merged msg\n              └──────────────┘  ───► (2) expired — anything that didn't make it\n```\n\nEvery incoming message tells the node which path it represents (via a\nconfigurable message property — `msg.topic` by default). The node holds onto\neach one until either:\n\n- **All wait paths arrive within the timeout** → forwards a single merged\n  message on output 1.\n- **The timeout elapses, or a reset path is seen, or `msg.complete` is set**\n  → forwards whatever's queued to output 2.\n\n## Configuration\n\n| Field            | What it controls                                                                                            |\n| ---------------- | ----------------------------------------------------------------------------------------------------------- |\n| **Path field**   | The `msg` property whose value names the path. Default `msg.topic`. Can be a string or an object of paths.  |\n| **Wait paths**   | The path names to wait for. Repeating an entry means \"this path must arrive that many times\".               |\n| **Timeout**      | Window for all wait paths to arrive.                                                                        |\n| **Match order**  | `Any order` (default) or `Exact order`.                                                                     |\n| **Group by**     | Optional — only joins messages whose value at this property matches. Use `msg._msgid` for split-flow joins. |\n| **Output base**  | Use the first or last received message as the base of the merged output.                                    |\n| **Merge values** | Keep original path values, or overwrite each with that message's `msg.payload`.                             |\n\n### Advanced\n\n| Field               | What it controls                                                                             |\n| ------------------- | -------------------------------------------------------------------------------------------- |\n| **Reset paths**     | Path names that immediately drain the queue to the expired output. Each must be unique.      |\n| **Use regex**       | Treat each path entry as a regular expression.                                               |\n| **Unmatched paths** | Log a warning when a message arrives with a path not in either list.                         |\n| **Ignore complete** | Disable the `msg.complete` short-circuit.                                                    |\n| **Preserve queue**  | Keep the queue across deploys and (with a configured store) restarts.                        |\n| **Persist store**   | Name of a context store from `settings.js` (e.g. `localfilesystem`) for restart persistence. |\n\n### Per-message overrides\n\nAny of these on an incoming message change behavior for **that message only**\n(no node-level state is persisted):\n\n- `msg.pathsToWait` — array overriding **Wait paths**.\n- `msg.pathsToExpire` — array overriding **Reset paths**.\n- `msg.useRegex` — boolean overriding **Use regex**.\n- `msg.complete` — drains the queue to the expired output.\n- `msg.reset` (boolean `true`) — silently drains the queue, no output.\n\n### Path field as a string vs. object\n\n```js\n// As a string — one path per message\nmsg.topic = 'path_1';\n\n// As an object — one message represents multiple paths\nmsg.topic = { path_1: true, path_2: true };\n\n// As an object — values are kept in the merged output (when \"Merge values\"\n// is set to keep original)\nmsg.topic = { path_1: { sensor: 'A' }, path_2: { sensor: 'B' } };\n```\n\n## Behavior reference\n\n- When the same wait path arrives more than required, only the latest value\n  is kept in the merged output.\n- In **Exact order** mode, unexpected paths between expected ones are\n  tolerated.\n- In any-order mode with regex, paths are counted greedily left-to-right.\n  `[\"path_[12]\", \"path_2\"]` will never complete because every `path_2`\n  arrival gets attributed to the first regex.\n- Pad **Timeout** with a small overhead (~5–10 ms) when working with very\n  short windows — evaluation isn't free.\n- The success output reuses the chosen base message (first or last) and\n  attaches the merged data on the **Path field** property.\n\n## Persistence\n\nQueues live in [Node-RED's context store](https://nodered.org/docs/user-guide/context).\n**Preserve queue** is on by default, so partial joins survive a redeploy\nout of the box (the default in-memory store keeps state in-process).\n\nTo survive **full Node-RED restarts**, configure a persistent context\nstore in `settings.js`. The simplest setup:\n\n```js\ncontextStorage: {\n    default: { module: 'localfilesystem' },\n}\n```\n\nEvery `join-wait` node then uses the default store automatically.\n\nIf you have a mixed setup with `memory` as the default and a separate\npersistent store (e.g. for archival), `join-wait` is smart about it:\nwhen `Preserve queue` is on but the default store is memory, the node\n**auto-picks** the first non-memory named store and logs which one it\nchose. So this configuration also gets restart persistence for free:\n\n```js\ncontextStorage: {\n    default: { module: 'memory' },\n    file:    { module: 'localfilesystem' },  // auto-picked\n}\n```\n\nSet **Persist store** explicitly on a node to override the auto-pick.\n\n## Example flows\n\nAfter install, **Menu → Import → Examples → join-wait** lists ready-made\nflows:\n\n1.  **Quickstart** — wait for two paths within 5 s.\n2.  **Correlation** — group messages from a split flow via `_msgid`.\n3.  **Reset paths** — abort the queue when a reset path arrives.\n4.  **Regex paths** — match path names by regular expression.\n5.  **Exact order** — require a specific sequence (with a repeated step).\n\n## Compatibility\n\n- Node-RED 3.x and 4.x.\n- Node.js 20 / 22 / 24.\n\n## Migrating from 0.5.x\n\nVersion 0.6 is mostly drop-in compatible, with two intentional behavior\ntweaks:\n\n- `msg.pathsToWait`, `msg.pathsToExpire`, and `msg.useRegex` are now\n  **one-shot** overrides — they affect only the current message and no\n  longer mutate the node's stored config.\n- Persistence moved from a `node-persist` singleton to Node-RED's context\n  store (fixes a multi-node race). For restart persistence, configure a\n  persistent store in `settings.js` and set **Persist store**.\n\nThe configuration field for paths now stores a real array; the legacy JSON\nstring form (`'[\"a\",\"b\"]'`) is still accepted at runtime so existing flows\nkeep working without re-deploying.\n\nSee [CHANGELOG.md](CHANGELOG.md) for the full list of changes.\n\n## Contributing\n\nIssues and PRs welcome — please run `npm test`, `npm run lint`, and\n`npm run spellcheck` locally before sending.\n\n## Support this project\n\nIf this project saved you time and you'd like to send a tip:\n\n- [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/ddcaspi) — one-time PayPal donations.\n- **Venmo** — one-time donations via Venmo:\n\n    ![Venmo QR](docs/venmo.png?raw=true 'Venmo QR Code')\n\nA GitHub star also goes a long way.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## Credits\n\nOriginally inspired by [mauriciom75/node-red-contrib-wait-paths](https://github.com/mauriciom75/node-red-contrib-wait-paths).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdxdc%2Fnode-red-contrib-join-wait","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdxdc%2Fnode-red-contrib-join-wait","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdxdc%2Fnode-red-contrib-join-wait/lists"}