node-red-contrib-join-wait 0.6.1 → 0.6.3

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 CHANGED
@@ -4,178 +4,202 @@ All notable changes to this project are documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.3] - 2026-05-09
8
+
9
+ ### Fixed
10
+
11
+ - **Open example flows button** is finally working. The 0.6.2 fix
12
+ invoked the import action via `RED.tray.close(cb)`'s callback, but
13
+ that callback fires before the `editor:close` event — so
14
+ clipboard.js's `disabled` guard was still set when the action ran,
15
+ and the dialog never appeared. Now listens for `editor:close`
16
+ explicitly and defers the action one tick past the synchronous
17
+ listener chain that clears the flag. Adds a guard against fast
18
+ double-clicks and a feature-detect fallback to
19
+ `core:show-import-dialog` for Node-RED < 3.1.
20
+
21
+ ### Changed
22
+
23
+ - **`engines.node` lowered to `>=18.5`** to match the Node-RED 4.x
24
+ minimum (per the contrib-node packaging spec). CI matrix gains
25
+ Node 18 so the claim is verified, not asserted.
26
+ - **Editor template updated to `const`/`let` and arrow functions**.
27
+ Pure stylistic refactor of `join-wait.html`'s IIFE; no behaviour
28
+ change. `function` is preserved where Node-RED binds `this`
29
+ (validators, `oneditprepare`/`save`/`resize`, jQuery `.each`/`.map`
30
+ callbacks).
31
+ - Dropped the `node-red.examples` field from `package.json` — not
32
+ part of the packaging spec; examples auto-discover from
33
+ `examples/`.
34
+
35
+ ## [0.6.2] - 2026-05-09
36
+
37
+ ### Fixed
38
+
39
+ - **Open example flows button** was a no-op. Now invokes
40
+ `core:show-examples-import-dialog` directly, opening the dialog as a
41
+ modal above the edit tray.
42
+ - **Wait paths editableList overlapped the form rows below it** for
43
+ long lists. Resized via the canonical Node-RED pattern (numeric
44
+ height + `oneditresize` filling the tray, no upper clamp); re-flows
45
+ on the Advanced `<details>` toggle.
46
+ - **Persist store admin call** now prefixes `RED.settings.apiRootUrl`,
47
+ so it works under a custom `httpAdminRoot` or behind a reverse
48
+ proxy.
49
+ - **Help text** no longer references the file-based persistence path
50
+ removed in 0.6.0; corrected to describe the context-store backend.
51
+
52
+ ### Added
53
+
54
+ - **Bulk paste in path lists** — paste newline-separated text into any
55
+ Wait paths or Reset paths row to fill the row + append one row per
56
+ remaining line.
57
+ - **Richer inline validation** — output preview reflects
58
+ `pathTopicType` and shows repeat counts as `path_1 (×2): …`;
59
+ duplicate Reset paths and overlap between Wait/Reset paths are
60
+ flagged inline; Timeout accepts fractional values.
61
+ - Tests for the `/join-wait/stores` admin endpoint and drift fences
62
+ in `editor_spec.js` so the help text never re-acquires legacy keys.
63
+
64
+ ### Changed
65
+
66
+ - Persist store row hides when Preserve queue is off; output-preview
67
+ and Advanced summary use Node-RED CSS variables (theme-aware);
68
+ Timeout field widths cleaned up.
69
+
70
+ ## [0.6.1] - 2026-05-08
71
+
72
+ ### Fixed
73
+
74
+ - Shipped example flows wouldn't import — inject nodes were missing
75
+ the standard top-level `topic`, `payload`, `repeat`, `crontab`,
76
+ `once`, and `onceDelay` fields. All five examples regenerated in
77
+ the canonical inject format.
78
+ - First attempt at fixing the **Open example flows** button (closed
79
+ the edit tray before invoking the import dialog) — didn't reliably
80
+ surface; see 0.6.2 for the working fix.
81
+
7
82
  ## [0.6.0] - 2026-05-08
8
83
 
9
84
  ### Added
10
85
 
11
- - **Modern Node-RED `(msg, send, done)` input handler signature.** The
12
- runtime now gets a proper `done()` / `done(err)` signal for every
13
- message meaningful for async-message tracking and graceful
14
- shutdown. The async input handler previously relied on auto-completion,
15
- which fired before any awaited work resolved.
16
- - **Smarter status indicator.** A single-group node shows progress toward
17
- completion (e.g. `2/3 received`) rather than the raw queue depth.
18
- Multi-group nodes still show aggregate counts.
19
- - Two more shipped example flows: `04-regex.json` (regex path matching)
20
- and `05-exact-order.json` (exact-order with a repeated step).
21
- - **Editor UX overhaul.** Wait paths and reset paths now use Node-RED's
22
- `editableList` widget add/remove/reorder rows instead of typing JSON
23
- arrays into a text box. Each row now validates inline (empty / invalid
24
- regex are flagged red with a tooltip). Pressing <kbd>Enter</kbd> in a
25
- row adds the next one.
26
- - **Persist store as a dropdown.** A new `/join-wait/stores` admin route
27
- exposes the configured context stores; the editor populates the
28
- Persist store field as a `<select>` showing each store's resolved
29
- module (e.g. `file (localfilesystem)`). Eliminates typos and surfaces
30
- available stores.
31
- - **Tooltips on every field label.** Hover-discoverable explanations
32
- (`title` attributes) for every form row.
33
- - **Inline output-shape preview.** Under Wait paths, a small monospace
34
- line shows what the merged success message looks like
35
- (`→ msg.topic = { path_1: …, path_2: … }`), updating as the list
36
- changes.
37
- - **Smart timeout warning.** When the resolved timeout is under 50 ms,
38
- a yellow tip surfaces the README guidance about padding evaluation
39
- overhead.
40
- - **Inline jsonata validation on Group by.** When the field type is
41
- `jsonata`, the expression is parsed at edit-time and the field marks
42
- invalid before the first message even arrives.
43
- - **Quick "Open example flows" button** in the editor opens the import
44
- dialog directly.
45
- - Path-row placeholders changed from generic `path name` to a worked
46
- example (`e.g. sensor_1`, `e.g. abort`) so empty rows hint at intent.
47
- - **Status-indicator error states.** Validation, regex compile, and
48
- correlator-evaluation failures now show a red ring on the node with a
49
- short reason in addition to the usual `node.error` log entry.
50
- - **Unhandled-rejection guard** around the async input handler so a future
51
- bug or upstream throw can't escape without being logged.
52
- - End-to-end smoke test for **all five** shipped example flows, plus a
53
- static config-shape sanity test on `join-wait.html` to catch drift
54
- between the editor defaults and the runtime fields.
55
- - **Advanced settings** are collapsed under a single `<details>` group so
56
- the editor presents the common options first.
57
- - **`msg.reset`** — sending `msg.reset = true` silently drains the queue
58
- for the current correlation group with no output.
59
- - **`node.status()`** indicator showing the number of queued messages.
60
- - **Examples** — the package now ships three ready-made flows
61
- (`Quickstart`, `Correlation`, `Reset paths`) discoverable via
62
- **Menu → Import → Examples → join-wait**.
63
- - **Per-instance persistence store** — new `Persist store` field selects a
64
- named context store from `settings.js` for restart-survival.
65
- - Unit-test specs for the `lib/config`, `lib/matcher`, and `lib/persist`
66
- modules.
86
+ - **Modern `(msg, send, done)` input handler** proper async-message
87
+ tracking and graceful shutdown (the old auto-completion fired
88
+ before awaited work resolved).
89
+ - **Editor UX overhaul** Wait paths and Reset paths now use
90
+ `editableList` (add/remove/reorder rows; inline validation for
91
+ empty rows + invalid regex; Enter adds the next). Inline output
92
+ preview, smart short-timeout warning, jsonata-validity check on
93
+ Group by, tooltips on every label, an **Open example flows**
94
+ shortcut, and an **Advanced** collapsible. Field labels renamed
95
+ for clarity.
96
+ - **Persist store dropdown** new `/join-wait/stores` admin route
97
+ exposes the configured context stores; the editor renders a typo-proof
98
+ `<select>`.
99
+ - **Smarter `node.status()`** single-group nodes show progress
100
+ (`2/3 received`); validation / regex-compile / correlator-evaluation
101
+ failures show a red ring with a short reason.
102
+ - **Five shipped example flows** Quickstart, Correlation, Reset,
103
+ Regex, Exact order discoverable via **Menu Import → Examples
104
+ join-wait**.
105
+ - **`msg.reset`** silently drains the queue for the current
106
+ correlation group.
67
107
 
68
108
  ### Changed
69
109
 
70
- - **Renames for clarity.** Internal-only config keys and runtime
71
- behavior are unchanged for existing flows. `node.paths` → `node.queues`;
72
- `node.topic` / `node.topicType` `node.correlator` /
73
- `node.correlatorType`; `node.pathTopic` / `node.pathTopicType`
74
- `node.pathField` / `node.pathFieldType`; `node.disableComplete` →
75
- `node.ignoreMsgComplete`; `node.persistOnRestart` `node.persistQueue`;
76
- `node.firstMsg` → `node.useFirstAsBase`. The `clearQueue*` family →
77
- `dropQueue` / `flushQueueAsExpired` / `flushOnMsgComplete` /
78
- `flushTimedOutEntries` / `flushTrailingEntries`. Magic
79
- `'_join-wait-node'` `DEFAULT_GROUP` constant. `pickArrayOverride` →
80
- `arrayOrFallback`.
81
- - **`drainQueue` now takes an options object** (`{ sendExpired,
82
- expireByTime, keep }`) instead of three positional booleans + a count.
83
- - **`findAllPaths{AnyOrder,ExactOrder}` return shape** changed from
84
- `null | number` to `{ matched: true } | { matched: false, keep: number }`
85
- for self-documenting call sites. Lib functions are not part of the
86
- public API.
87
- - **`asBool()` helper** accepts both `'true'`/`'false'` strings (from the
88
- HTML `<select>`) and real booleans (programmatic flow construction).
89
- Removes a class of subtle defaults-bug for non-editor-built flows.
90
- - **`countPathsAnyOrder`** now uses a `Set` to track used indices
91
- (O(1) check vs the previous O(n) `Array.indexOf`).
92
- - **Comment around `flushOnMsgComplete`** documents the deliberately
93
- partial behavior: `msg.complete` short-circuits a partial queue but
94
- never overrides a successful match.
95
- - **`node:` prefix on core imports** in test files (`node:fs`,
96
- `node:os`, `node:path`).
97
- - **Expired output now uses the configured `Path field`** for the merged
98
- data (matching the success output). Previously hardcoded to `msg.paths`
99
- even when a different `pathTopic` was configured. Existing flows that
100
- use the default `paths` setting see no change.
101
- - **Persistence rewritten on top of Node-RED's context store** (replacing
102
- the `node-persist` singleton, which raced when multiple `join-wait` nodes
103
- were deployed in the same flow). Default in-memory store keeps queues
104
- across deploys; a configured persistent store keeps them across full
105
- restarts.
106
- - **`Preserve queue` defaults to on** for new nodes. Partial joins now
107
- survive a redeploy out of the box. Existing 0.5.x flows keep their
108
- original `persistOnRestart: false` until re-saved.
109
- - **`Persist store` is no longer required** for restart persistence.
110
- If `localfilesystem` (or similar) is set as the **default** context
111
- store in `settings.js`, every join-wait node uses it automatically.
112
- - **Auto-pick of a persistent named store.** When `Preserve queue` is
113
- on, `Persist store` is empty, and the configured default store is
114
- memory, the node auto-selects the first non-memory named store from
115
- `settings.js` (with a `node.log` line so it's discoverable). If the
116
- user has both `default: memory` and (say) `file: localfilesystem`
117
- configured, they get restart persistence for free without setting
118
- `Persist store` on every node. Setting `Persist store` explicitly
119
- always wins.
120
- - **British → American spellings.** `normalisePaths` → `normalizePaths`,
121
- `serialisable` → `serializable`, `behaviour` → `behavior` in code +
122
- docs for consistency with the wider JS / npm ecosystem.
123
- - **`msg.pathsToWait`, `msg.pathsToExpire`, `msg.useRegex` are now one-shot
124
- overrides** — they apply only to the current message instead of
125
- permanently mutating the node's stored config.
126
- - **Code split** into `lib/config.js`, `lib/matcher.js`, and `lib/persist.js`
127
- — `join-wait.js` is now a thin orchestrator. The input handler is broken
128
- into discrete named phases for readability.
129
- - **`mapPayload` no longer mutates the caller's `pathTopic` object** —
130
- incoming messages are left untouched.
131
- - **jsonata bumped to v2** (`evaluate()` is now properly awaited).
132
- - **Node-RED bumped to ≥ 3.0**, **Node.js ≥ 18**.
133
- - **`engines.node`, `files`** added to `package.json`; deprecated
134
- `licenses` array removed.
135
- - Source labels in the editor renamed for clarity:
136
- - "Paths topic" → **Path field**
137
- - "Paths (Wait)" / "Paths (Expire)" → **Wait paths** / **Reset paths**
138
- - "Sequence order" → **Match order**
139
- - "Base message" → **Output base**
140
- - "Merged data" → **Merge values**
141
- - README rewritten — quickstart first, technical details after.
110
+ - **Persistence rewritten on top of Node-RED's context store**,
111
+ replacing the `node-persist` singleton (which raced when multiple
112
+ `join-wait` nodes shared the flow). Default in-memory store keeps
113
+ queues across deploys; a configured persistent store keeps them
114
+ across full restarts. **Preserve queue** defaults to on; the node
115
+ auto-picks the first persistent named store when the default is
116
+ memory.
117
+ - **`msg.pathsToWait`, `msg.pathsToExpire`, `msg.useRegex`** are now
118
+ one-shot overrides they apply only to the current message
119
+ instead of permanently mutating the node's stored config.
120
+ - **Expired output uses the configured Path field** for the merged
121
+ data (matched the success output). Previously hardcoded to
122
+ `msg.paths`.
123
+ - **Code split** into `lib/config.js`, `lib/matcher.js`,
124
+ `lib/persist.js`; `findAllPaths{AnyOrder,ExactOrder}` return a
125
+ `{matched, keep}` shape; `drainQueue` takes options; internal
126
+ renames for clarity (config keys unchanged for back-compat).
127
+ - jsonata bumped to v2 (proper async `evaluate()`); Node-RED 3.0,
128
+ Node.js 20.
142
129
 
143
130
  ### Fixed
144
131
 
145
- - **`node.error` / `node.warn` second argument now correctly passes the
146
- originating `msg`** (was wrongly passed as `[msg, null]`, a 2-output
147
- send array, since 0.5.x). Catch nodes downstream now receive the real
148
- message instead of an array.
149
- - **Close handler always calls `done`**, even when the context-store
150
- write rejects, so a transient persistence failure can't hold up
151
- Node-RED shutdown.
152
- - Empty `<b></b>` tag in the help text.
153
- - Stale `ignoreUnmatched` references in older example flows.
132
+ - `node.error` / `node.warn` correctly pass the originating `msg`
133
+ (previously passed as `[msg, null]` since 0.5.x — Catch nodes
134
+ downstream now receive the real message).
135
+ - Close handler always calls `done` even when the context-store
136
+ write rejects, so persistence failures don't hold up shutdown.
137
+ - `mapPayload` no longer mutates the caller's `pathTopic` object.
154
138
  - Possible race when multiple `join-wait` nodes shared the global
155
- `node-persist` singleton (now uses an isolated context-store entry per
139
+ `node-persist` singleton (now an isolated context-store entry per
156
140
  node).
157
141
 
158
142
  ### Tooling
159
143
 
160
- - GitHub Actions CI replaces Travis (matrix on Node 20/22/24, publishing
161
- on Node 24). CI + release now live in a single workflow; the publish
162
- job is gated by `needs: [quality, test]` so a tag push only ships once
163
- lint, format, spellcheck, and the full test matrix have all passed.
164
- - Workflow steps pinned to current `actions/checkout@v6`,
165
- `actions/setup-node@v6`, `actions/upload-artifact@v6`.
166
- - `prettier --check` enforced in CI to catch unformatted files.
167
- - `engines.node` bumped to `>=20` (Node 18 EOL'd 2025-04-30).
168
- - Automated npm publish workflow with provenance on tag push.
169
- - Dependabot configured for monthly npm + GitHub Actions updates.
170
- - ESLint upgraded to v9 with flat config (`eslint.config.js`).
171
- - Prettier upgraded to v3.
172
- - Coverage moved from `nyc` to `c8` (native V8 coverage).
173
- - Spellcheck moved from the unmaintained `markdown-spellcheck` to `cspell`.
174
- - `node-red-node-test-helper` bumped from 0.2.x to 0.3.6.
175
- - Test suite expanded from 32 to 97 cases — including dedicated specs
176
- for each `lib/` module, the editor HTML, and end-to-end runs of every
177
- shipped example flow.
178
-
179
- ## [0.5.3] and earlier
180
-
181
- See the [git log](https://github.com/dxdc/node-red-contrib-join-wait/commits/master).
144
+ - GitHub Actions CI replaces Travis (matrix on Node 20/22/24);
145
+ publish job gated on lint + format + spellcheck + test matrix.
146
+ ESLint v9 (flat config), Prettier v3, c8 (replaces nyc), cspell
147
+ (replaces `markdown-spellcheck`); Dependabot for monthly npm + GHA
148
+ updates. Test suite expanded from 32 to 102 cases.
149
+
150
+ ## [0.5.3] - 2021-10-31
151
+
152
+ - Update dependencies.
153
+
154
+ ## [0.5.2] - 2021-02-02
155
+
156
+ - Retain state across restart
157
+ ([#7](https://github.com/dxdc/node-red-contrib-join-wait/issues/7)).
158
+
159
+ ## [0.5.1] - 2021-01-05
160
+
161
+ - More automated testing and code coverage.
162
+
163
+ ## [0.5.0] - 2020-12-17
164
+
165
+ - Expire earlier for "any order" mode
166
+ ([#4](https://github.com/dxdc/node-red-contrib-join-wait/issues/4)).
167
+
168
+ ## [0.4.5] - 2020-12-15
169
+
170
+ - Add automated tests; address several minor issues uncovered by
171
+ testing; refactoring and readability changes.
172
+
173
+ ## [0.4.0] - 2020-12-14
174
+
175
+ - Reworked logic for exact-order mode
176
+ ([#1](https://github.com/dxdc/node-red-contrib-join-wait/issues/1)).
177
+ - Support for duplicate path names
178
+ ([#2](https://github.com/dxdc/node-red-contrib-join-wait/issues/2)).
179
+ - Show all unmatched paths with individual warnings.
180
+ - Minor changes to config UI and defaults.
181
+
182
+ ## [0.3.5] - 2020-05-26
183
+
184
+ - Enforce unique path names.
185
+ - Regex-based path matching.
186
+
187
+ ## [0.3.4] - 2020-01-17
188
+
189
+ - Don't reuse the `msg` variable.
190
+
191
+ ## [0.3.3] - 2020-01-17
192
+
193
+ - Fix bug with paths / `clearTimeout`.
194
+ - Minor code optimizations; add Example 3 to documentation.
195
+
196
+ ## [0.3.2] - 2020-01-16
197
+
198
+ - `msg.complete` handling.
199
+ - `timeoutUnits` in settings.
200
+ - `ignoreUnmatched` paths option in settings.
201
+ - Updated documentation.
202
+
203
+ ## [0.3.0] - 2020-01-14
204
+
205
+ - Initial release.
package/README.md CHANGED
@@ -61,6 +61,11 @@ you want to wait for. That's it.
61
61
  > Looking for a working flow? After install, open
62
62
  > **Menu → Import → Examples → join-wait** for ready-made flows.
63
63
 
64
+ > **Tip:** in the **Wait paths** / **Reset paths** lists you can press
65
+ > <kbd>Enter</kbd> in a row to add the next one, and paste newline-separated
66
+ > text into any row to fill the current row and append the rest as new
67
+ > rows — handy for seeding a list from a spreadsheet column or a debug log.
68
+
64
69
  ## How it works
65
70
 
66
71
  ```
package/join-wait.html CHANGED
@@ -15,8 +15,10 @@
15
15
  or an object with multiple keys.
16
16
  </div>
17
17
 
18
- <div class="form-row">
18
+ <div class="form-row" style="margin-bottom:4px">
19
19
  <label title="Path names to wait for; repeat an entry to require it n times."><i class="fa fa-code-fork"></i> Wait paths</label>
20
+ </div>
21
+ <div class="form-row node-input-paths-container-row">
20
22
  <ol id="node-input-paths-container"></ol>
21
23
  </div>
22
24
  <div class="form-tips" style="margin-bottom:6px">
@@ -24,12 +26,12 @@
24
26
  merged success output. Press <kbd>Enter</kbd> in a row to add the
25
27
  next one. Repeat an entry to require n-of-the-same.
26
28
  </div>
27
- <div class="form-tips" id="join-wait-output-preview" style="margin-bottom:12px; font-family:monospace; color:#888"></div>
29
+ <div class="form-tips" id="join-wait-output-preview" style="margin-bottom:12px; font-family:var(--red-ui-monospace-font, monospace)"></div>
28
30
 
29
31
  <div class="form-row">
30
32
  <label for="node-input-timeout" title="How long all wait paths have to arrive before the queue is expired."><i class="fa fa-clock-o"></i> Timeout</label>
31
- <input type="text" id="node-input-timeout" style="text-align:end; width:100px !important" placeholder="15">
32
- <select id="node-input-timeoutUnits" style="width:200px !important">
33
+ <input type="text" id="node-input-timeout" style="text-align:end; width:90px" placeholder="15">
34
+ <select id="node-input-timeoutUnits" style="width:160px; margin-left:5px">
33
35
  <option value="1">Milliseconds</option>
34
36
  <option value="1000">Seconds</option>
35
37
  <option value="60000">Minutes</option>
@@ -37,11 +39,13 @@
37
39
  <option value="86400000">Days</option>
38
40
  </select>
39
41
  </div>
40
- <div class="form-tips" id="join-wait-timeout-hint" style="margin-bottom:12px; color:#b07b00; display:none"></div>
42
+ <div class="form-tips" id="join-wait-timeout-hint" style="margin-bottom:12px; color:#b07b00; display:none">
43
+ <i class="fa fa-exclamation-triangle"></i> <span></span>
44
+ </div>
41
45
 
42
46
  <div class="form-row">
43
47
  <label for="node-input-exactOrder" title="Any order (default) or strict sequence."><i class="fa fa-sort"></i> Match order</label>
44
- <select id="node-input-exactOrder" style="width:70%; margin-right:5px;">
48
+ <select id="node-input-exactOrder" style="width:70%">
45
49
  <option value="false">Any order</option>
46
50
  <option value="true">Exact order</option>
47
51
  </select>
@@ -64,11 +68,13 @@
64
68
  </button>
65
69
  </div>
66
70
 
67
- <details style="margin-top:14px">
68
- <summary style="cursor:pointer; font-weight:bold; padding:6px 0">Advanced</summary>
71
+ <details class="join-wait-advanced" style="margin-top:14px">
72
+ <summary style="cursor:pointer; font-weight:bold; padding:6px 0; color:var(--red-ui-header-text-color, inherit)">Advanced</summary>
69
73
 
70
- <div class="form-row" style="margin-top:10px">
74
+ <div class="form-row" style="margin-top:10px; margin-bottom:4px">
71
75
  <label title="Paths that immediately drain the queue to the expired output."><i class="fa fa-times-circle-o"></i> Reset paths</label>
76
+ </div>
77
+ <div class="form-row node-input-pathsToExpire-container-row">
72
78
  <ol id="node-input-pathsToExpire-container"></ol>
73
79
  </div>
74
80
  <div class="form-tips" style="margin-bottom:12px">
@@ -78,7 +84,7 @@
78
84
 
79
85
  <div class="form-row">
80
86
  <label for="node-input-firstMsg" title="Use the first or last message in the matched window as the base of the merged output."><i class="fa fa-comments-o"></i> Output base</label>
81
- <select id="node-input-firstMsg" style="width:70%; margin-right:5px;">
87
+ <select id="node-input-firstMsg" style="width:70%">
82
88
  <option value="true">First received message</option>
83
89
  <option value="false">Last received message</option>
84
90
  </select>
@@ -86,7 +92,7 @@
86
92
 
87
93
  <div class="form-row">
88
94
  <label for="node-input-mapPayload" title="Keep the original Path-field values, or overwrite each with that message's payload."><i class="fa fa-arrow-right"></i> Merge values</label>
89
- <select id="node-input-mapPayload" style="width:70%; margin-right:5px;">
95
+ <select id="node-input-mapPayload" style="width:70%">
90
96
  <option value="false">Original Path field values</option>
91
97
  <option value="true">Each msg.payload</option>
92
98
  </select>
@@ -116,13 +122,13 @@
116
122
  <label for="node-input-persistOnRestart" style="width:auto">Preserve queue across deploys / restarts</label>
117
123
  </div>
118
124
 
119
- <div class="form-row">
125
+ <div class="form-row" id="join-wait-persist-store-row">
120
126
  <label for="node-input-persistStore" title="Override the context store used to persist the queue."><i class="fa fa-database"></i> Persist store</label>
121
- <select id="node-input-persistStore" style="width:70%; margin-right:5px;">
127
+ <select id="node-input-persistStore" style="width:70%">
122
128
  <option value="">(default context store)</option>
123
129
  </select>
124
130
  </div>
125
- <div class="form-tips" style="margin-bottom:12px">
131
+ <div class="form-tips" id="join-wait-persist-store-tip" style="margin-bottom:12px">
126
132
  Override the context store this node uses. Leave on default — set
127
133
  the default to a persistent store (e.g. <code>localfilesystem</code>)
128
134
  in <code>settings.js</code> and every join-wait node automatically
@@ -133,37 +139,65 @@
133
139
 
134
140
  <script type="text/javascript">
135
141
  (function () {
136
- function arrayValidator(allowEmpty, requireUnique) {
137
- return function (v) {
138
- // Accept legacy JSON-string format too, for back-compat with old flows.
139
- var arr = v;
140
- if (typeof v === 'string') {
141
- if (v === '') return allowEmpty;
142
- try {
143
- arr = JSON.parse(v);
144
- } catch (err) {
145
- return false;
146
- }
147
- }
148
- if (!Array.isArray(arr)) return false;
149
- if (arr.length === 0) return allowEmpty;
150
- if (
151
- !arr.every(function (s) {
152
- return typeof s === 'string' && s.length > 0;
153
- })
154
- )
142
+ // Every DOM hook the editor uses, in one place.
143
+ const SEL = {
144
+ pathField: '#node-input-pathTopic',
145
+ pathFieldType: '#node-input-pathTopicType',
146
+ correlator: '#node-input-correlationTopic',
147
+ correlatorType: '#node-input-correlationTopicType',
148
+ timeout: '#node-input-timeout',
149
+ timeoutUnits: '#node-input-timeoutUnits',
150
+ useRegex: '#node-input-useRegex',
151
+ persistOnRestart: '#node-input-persistOnRestart',
152
+ persistStore: '#node-input-persistStore',
153
+ outputPreview: '#join-wait-output-preview',
154
+ timeoutHint: '#join-wait-timeout-hint',
155
+ examplesButton: '#join-wait-open-examples',
156
+ persistStoreRow: '#join-wait-persist-store-row',
157
+ persistStoreTip: '#join-wait-persist-store-tip',
158
+ dialogForm: '#dialog-form',
159
+ advancedDetails: '#dialog-form details',
160
+ };
161
+ // editableList wrapper IDs (no leading '#'); consumers either
162
+ // pass them to readEditableList or interpolate `#` themselves.
163
+ const LIST_ID = {
164
+ paths: 'node-input-paths-container',
165
+ expire: 'node-input-pathsToExpire-container',
166
+ };
167
+ const listInputs = (id) => $('#' + id + ' input[type=text]');
168
+ // Path-field name + typedInput type, read together to avoid drift.
169
+ const readPathField = () => ({
170
+ name: $(SEL.pathField).val() || 'topic',
171
+ type: $(SEL.pathFieldType).val() || 'msg',
172
+ });
173
+
174
+ const arrayValidator = (allowEmpty, requireUnique) => (v) => {
175
+ // Accept legacy JSON-string format too, for back-compat with old flows.
176
+ let arr = v;
177
+ if (typeof v === 'string') {
178
+ if (v === '') return allowEmpty;
179
+ try {
180
+ arr = JSON.parse(v);
181
+ } catch (err) {
155
182
  return false;
156
- if (requireUnique) {
157
- var seen = Object.create(null);
158
- for (var i = 0; i < arr.length; i++) {
159
- if (seen[arr[i]]) return false;
160
- seen[arr[i]] = true;
161
- }
162
183
  }
163
- return true;
164
- };
165
- }
184
+ }
185
+ if (!Array.isArray(arr)) return false;
186
+ if (arr.length === 0) return allowEmpty;
187
+ if (!arr.every((s) => typeof s === 'string' && s.length > 0)) return false;
188
+ if (requireUnique) {
189
+ const seen = Object.create(null);
190
+ for (let i = 0; i < arr.length; i++) {
191
+ if (seen[arr[i]]) return false;
192
+ seen[arr[i]] = true;
193
+ }
194
+ }
195
+ return true;
196
+ };
166
197
 
198
+ // Note: Node-RED calls validate functions with `this` bound to the
199
+ // node config, so this stays a function expression (arrow would
200
+ // not bind `this`).
167
201
  function correlationValidator(v) {
168
202
  // Empty / undefined / non-jsonata: nothing to validate here.
169
203
  if (this.correlationTopicType !== 'jsonata' || !v) return true;
@@ -177,162 +211,227 @@
177
211
  }
178
212
  }
179
213
 
180
- function isValidRegex(s) {
214
+ const isValidRegex = (s) => {
181
215
  try {
182
216
  new RegExp(s);
183
217
  return true;
184
218
  } catch (err) {
185
219
  return false;
186
220
  }
187
- }
221
+ };
188
222
 
189
- function setRowValid(input, ok, message) {
223
+ const setRowValid = (input, ok, message) => {
190
224
  input.css('border-color', ok ? '' : '#d9534f');
191
225
  input.attr('title', ok ? '' : message || '');
192
- }
193
-
194
- function validateRow(input) {
195
- var $useRegex = $('#node-input-useRegex');
196
- var v = String(input.val() || '');
197
- if (v === '') {
226
+ };
227
+
228
+ // Single-row validation: empty / invalid regex. List-level
229
+ // checks (uniqueness, cross-list overlap) live in validateAllRows
230
+ // because they need both lists' current values in scope.
231
+ const validateRow = (input, ownValues, otherValues, opts) => {
232
+ const requireUnique = opts && opts.requireUnique;
233
+ const otherListLabel = (opts && opts.otherListLabel) || '';
234
+ const value = String(input.val() || '');
235
+ if (value === '') {
198
236
  setRowValid(input, false, 'path name cannot be empty');
199
237
  return false;
200
238
  }
201
- if ($useRegex.is(':checked') && !isValidRegex(v)) {
239
+ if ($(SEL.useRegex).is(':checked') && !isValidRegex(value)) {
202
240
  setRowValid(input, false, 'invalid regular expression');
203
241
  return false;
204
242
  }
243
+ if (requireUnique && ownValues && ownValues.filter((x) => x === value).length > 1) {
244
+ setRowValid(input, false, 'duplicate entry — must be unique');
245
+ return false;
246
+ }
247
+ if (otherValues && otherValues.indexOf(value) !== -1) {
248
+ setRowValid(
249
+ input,
250
+ false,
251
+ '"' + value + '" is also in ' + otherListLabel + " — paths shouldn't overlap",
252
+ );
253
+ return false;
254
+ }
205
255
  setRowValid(input, true);
206
256
  return true;
207
- }
257
+ };
258
+
259
+ // Re-validate every row across both lists so duplicate and
260
+ // cross-list overlap warnings stay in sync.
261
+ const validateAllRows = () => {
262
+ const $paths = listInputs(LIST_ID.paths);
263
+ const $expire = listInputs(LIST_ID.expire);
264
+ // jQuery .map / .each callbacks rely on `this`, so they stay
265
+ // as function expressions.
266
+ const pathValues = $paths
267
+ .map(function () {
268
+ return String($(this).val() || '');
269
+ })
270
+ .get();
271
+ const expireValues = $expire
272
+ .map(function () {
273
+ return String($(this).val() || '');
274
+ })
275
+ .get();
276
+ $paths.each(function () {
277
+ validateRow($(this), pathValues, expireValues, {
278
+ requireUnique: false,
279
+ otherListLabel: 'Reset paths',
280
+ });
281
+ });
282
+ $expire.each(function () {
283
+ validateRow($(this), expireValues, pathValues, {
284
+ requireUnique: true,
285
+ otherListLabel: 'Wait paths',
286
+ });
287
+ });
288
+ };
208
289
 
209
- function initEditableList(containerId, items, opts) {
210
- var $container = $('#' + containerId);
211
- $container.css({ 'min-height': '100px' });
212
- var placeholder = (opts && opts.placeholder) || 'e.g. sensor_1';
290
+ const initEditableList = (containerId, items, opts) => {
291
+ const $container = $('#' + containerId);
292
+ const placeholder = (opts && opts.placeholder) || 'e.g. sensor_1';
293
+ // Numeric height list scrolls internally past the limit.
294
+ // Recomputed in oneditresize to fill the tray.
213
295
  $container.editableList({
214
- addItem: function (row, index, data) {
215
- var value = (data && data.value) || '';
296
+ addItem: (row, index, data) => {
297
+ const value = (data && data.value) || '';
216
298
  row.css({ overflow: 'hidden', whiteSpace: 'nowrap' });
217
- var input = $('<input/>', {
299
+ const input = $('<input/>', {
218
300
  type: 'text',
219
301
  placeholder: placeholder,
220
302
  style: 'width:100%',
221
303
  }).val(value);
222
- input.on('input change blur', function () {
223
- validateRow(input);
304
+ input.on('input change blur', () => {
305
+ validateAllRows();
224
306
  if (opts && opts.onChange) opts.onChange();
225
307
  });
226
308
  // Pressing Enter in a row adds the next one.
227
- input.on('keydown', function (e) {
309
+ input.on('keydown', (e) => {
228
310
  if (e.key === 'Enter' || e.keyCode === 13) {
229
311
  e.preventDefault();
230
312
  $container.editableList('addItem', { value: '' });
231
313
  // Focus the newly-added row.
232
- setTimeout(function () {
314
+ setTimeout(() => {
233
315
  $container.editableList('items').last().find('input').trigger('focus');
234
316
  }, 0);
235
317
  }
236
318
  });
319
+ // Bulk-paste newline-separated text into multiple
320
+ // rows. Single-line pastes fall through to default.
321
+ input.on('paste', (e) => {
322
+ const clip = (e.originalEvent || e).clipboardData;
323
+ if (!clip) return;
324
+ const text = clip.getData('text');
325
+ if (!text || !/[\r\n]/.test(text)) return;
326
+ const lines = text
327
+ .split(/\r?\n/)
328
+ .map((s) => s.trim())
329
+ .filter((s) => s.length > 0);
330
+ if (lines.length === 0) return;
331
+ e.preventDefault();
332
+ input.val(lines[0]);
333
+ for (let i = 1; i < lines.length; i++) {
334
+ $container.editableList('addItem', { value: lines[i] });
335
+ }
336
+ validateAllRows();
337
+ if (opts && opts.onChange) opts.onChange();
338
+ });
237
339
  row.append(input);
238
- validateRow(input);
340
+ validateAllRows();
239
341
  if (opts && opts.onChange) opts.onChange();
240
342
  },
241
- removeItem: function () {
343
+ removeItem: () => {
344
+ validateAllRows();
242
345
  if (opts && opts.onChange) opts.onChange();
243
346
  },
244
347
  removable: true,
245
348
  sortable: true,
246
- height: 'auto',
349
+ height: (opts && opts.height) || 200,
247
350
  });
248
- (items || []).forEach(function (v) {
351
+ (items || []).forEach((v) => {
249
352
  $container.editableList('addItem', { value: v });
250
353
  });
251
- }
354
+ };
252
355
 
253
356
  // Re-validate every editableList row when the regex toggle flips.
254
- function rebindRegexValidation() {
255
- $('#node-input-useRegex').on('change', function () {
256
- $(
257
- '#node-input-paths-container input[type=text], ' +
258
- '#node-input-pathsToExpire-container input[type=text]',
259
- ).each(function () {
260
- validateRow($(this));
261
- });
262
- });
263
- }
357
+ const rebindRegexValidation = () => {
358
+ $(SEL.useRegex).on('change', validateAllRows);
359
+ };
264
360
 
265
- function readEditableList(containerId) {
266
- var values = [];
361
+ const readEditableList = (containerId) => {
362
+ const values = [];
267
363
  $('#' + containerId)
268
364
  .editableList('items')
269
365
  .each(function () {
270
- var v = $(this).find('input').val();
366
+ // jQuery `each` binds `this` to the row element, so
367
+ // this callback stays a function expression.
368
+ const v = $(this).find('input').val();
271
369
  if (v != null && v !== '') values.push(v);
272
370
  });
273
371
  return values;
274
- }
372
+ };
275
373
 
276
374
  // Coerce legacy string-array values into a plain array for editing.
277
- function toArray(v) {
375
+ const toArray = (v) => {
278
376
  if (Array.isArray(v)) return v;
279
377
  if (typeof v === 'string' && v !== '') {
280
378
  try {
281
- var parsed = JSON.parse(v);
379
+ const parsed = JSON.parse(v);
282
380
  return Array.isArray(parsed) ? parsed : [];
283
381
  } catch (err) {
284
382
  return [];
285
383
  }
286
384
  }
287
385
  return [];
288
- }
289
-
290
- // Render a tiny preview of what msg.<pathField> looks like on the
291
- // success output, derived from the current Wait paths list.
292
- function updateOutputPreview() {
293
- var paths = readEditableList('node-input-paths-container');
294
- var unique = [];
295
- paths.forEach(function (p) {
296
- if (unique.indexOf(p) === -1) unique.push(p);
386
+ };
387
+
388
+ // Preview of the merged success output. Repeated entries
389
+ // surface as `(×n)` so the n-of-the-same semantic is visible.
390
+ const updateOutputPreview = () => {
391
+ const paths = readEditableList(LIST_ID.paths);
392
+ const counts = Object.create(null);
393
+ const order = [];
394
+ paths.forEach((p) => {
395
+ if (counts[p] === undefined) order.push(p);
396
+ counts[p] = (counts[p] || 0) + 1;
297
397
  });
298
- var pathField = $('#node-input-pathTopic').val() || 'topic';
299
- var $tip = $('#join-wait-output-preview');
300
- if (unique.length === 0) {
398
+ const field = readPathField();
399
+ const $tip = $(SEL.outputPreview);
400
+ if (order.length === 0) {
301
401
  $tip.text('');
302
402
  return;
303
403
  }
304
- var entries = unique.slice(0, 4).map(function (p) {
305
- return p + ': …';
306
- });
307
- if (unique.length > 4) entries.push('…');
308
- $tip.text('→ msg.' + pathField + ' = { ' + entries.join(', ') + ' }');
309
- }
404
+ const entries = order.slice(0, 4).map((p) => (counts[p] > 1 ? p + ' (×' + counts[p] + '): …' : p + ': …'));
405
+ if (order.length > 4) entries.push('…');
406
+ $tip.text('→ ' + field.type + '.' + field.name + ' = { ' + entries.join(', ') + ' }');
407
+ };
310
408
 
311
409
  // Warn when the resolved timeout is impractically short (the README
312
410
  // recommends padding ~5–10 ms for evaluation overhead).
313
- function updateTimeoutHint() {
314
- var t = Number($('#node-input-timeout').val()) || 0;
315
- var u = Number($('#node-input-timeoutUnits').val()) || 1;
316
- var ms = t * u;
317
- var $hint = $('#join-wait-timeout-hint');
411
+ const updateTimeoutHint = () => {
412
+ const t = Number($(SEL.timeout).val()) || 0;
413
+ const u = Number($(SEL.timeoutUnits).val()) || 1;
414
+ const ms = t * u;
415
+ const $hint = $(SEL.timeoutHint);
318
416
  if (ms > 0 && ms < 50) {
319
- $hint.text('Very short — pad ~10 ms to leave room for evaluation overhead.').show();
417
+ $hint.find('span').text('Very short — pad ~10 ms to leave room for evaluation overhead.');
418
+ $hint.show();
320
419
  } else {
321
420
  $hint.hide();
322
421
  }
323
- }
324
-
325
- // Populate the Persist store <select> from the runtime via our
326
- // /join-wait/stores admin route. Falls back gracefully if the route
327
- // is unreachable or returns nothing useful.
328
- function populatePersistStores(currentValue) {
329
- var $sel = $('#node-input-persistStore');
330
- $.getJSON('join-wait/stores')
331
- .done(function (stores) {
332
- if (!Array.isArray(stores) || stores.length === 0) return;
333
- stores.forEach(function (s) {
422
+ };
423
+
424
+ // Populate the Persist store <select> from /join-wait/stores.
425
+ // apiRootUrl prefixes the call so it resolves under custom
426
+ // httpAdminRoot / reverse proxies.
427
+ const populatePersistStores = (currentValue) => {
428
+ const $sel = $(SEL.persistStore);
429
+ const base = (RED.settings && RED.settings.apiRootUrl) || '';
430
+ $.getJSON(base + 'join-wait/stores')
431
+ .done((stores) => {
432
+ (Array.isArray(stores) ? stores : []).forEach((s) => {
334
433
  if (s.name === 'default') return; // already represented as the blank option
335
- var label = s.module ? s.name + ' (' + s.module + ')' : s.name;
434
+ const label = s.module ? s.name + ' (' + s.module + ')' : s.name;
336
435
  $('<option/>').val(s.name).text(label).appendTo($sel);
337
436
  });
338
437
  // If the saved value isn't in the list, append it so we
@@ -345,28 +444,79 @@
345
444
  }
346
445
  $sel.val(currentValue || '');
347
446
  })
348
- .fail(function () {
349
- // Admin route not reachable leave the select with just
350
- // its default option.
447
+ .fail(() => {
448
+ // Admin route not reachable. Preserve the saved
449
+ // value as an explicit option so the dropdown doesn't
450
+ // silently overwrite persistStore with '' on save.
451
+ if (currentValue) {
452
+ $('<option/>')
453
+ .val(currentValue)
454
+ .text(currentValue + ' (stores list unavailable)')
455
+ .appendTo($sel);
456
+ $sel.val(currentValue);
457
+ }
351
458
  });
352
- }
353
-
354
- function openExamplesDialog() {
355
- // The edit tray is on top of the editor, so the import tray
356
- // can't surface beneath it. Close this tray first (which cancels
357
- // the in-flight edit), then invoke the core import dialog so the
358
- // user can pick Examples → join-wait.
359
- var invoke = function () {
360
- if (RED.actions && typeof RED.actions.invoke === 'function') {
361
- RED.actions.invoke('core:show-import-dialog');
362
- }
459
+ };
460
+
461
+ // Last `size` handed to oneditresize. Cached so the <details>
462
+ // toggle handler can re-run the resize math without waiting for
463
+ // the next tray drag.
464
+ let lastTraySize = null;
465
+
466
+ // Stretch the Wait paths editableList to fill the tray —
467
+ // pattern from core switch/change/httprequest.
468
+ const resizePathsList = () => {
469
+ const $form = $(SEL.dialogForm);
470
+ if (!$form.length || !lastTraySize || !lastTraySize.height) return;
471
+ let height = lastTraySize.height;
472
+ // jQuery `.each` binds `this` to the row element, so this
473
+ // callback stays a function expression.
474
+ $form.children(':not(.node-input-paths-container-row)').each(function () {
475
+ const $r = $(this);
476
+ if ($r.is(':visible')) height -= $r.outerHeight(true) || 0;
477
+ });
478
+ height -= 12; // breathing room for margins/padding the loop misses
479
+ if (height < 140) height = 140;
480
+ $('#' + LIST_ID.paths).editableList('height', height);
481
+ };
482
+
483
+ // clipboard.js disables import actions while an edit tray is
484
+ // open, and RED.tray.close(cb) fires cb before the editor:close
485
+ // event — so we listen for editor:close ourselves and defer one
486
+ // tick past the synchronous listener chain that clears the flag.
487
+ // The pending guard suppresses a fast double-click registering
488
+ // two listeners.
489
+ let pendingExamplesOpen = false;
490
+ const openExamplesDialog = () => {
491
+ if (pendingExamplesOpen) return;
492
+ pendingExamplesOpen = true;
493
+ const invoke = () => {
494
+ setTimeout(() => {
495
+ pendingExamplesOpen = false;
496
+ if (!RED.actions || typeof RED.actions.invoke !== 'function') return;
497
+ // core:show-examples-import-dialog was added in Node-RED 3.1.
498
+ const actions = RED.actions.list && RED.actions.list();
499
+ const hasExamples =
500
+ Array.isArray(actions) &&
501
+ actions.some((a) => (a && a.id) === 'core:show-examples-import-dialog');
502
+ RED.actions.invoke(hasExamples ? 'core:show-examples-import-dialog' : 'core:show-import-dialog');
503
+ }, 0);
363
504
  };
505
+ if (RED.events && typeof RED.events.once === 'function') {
506
+ RED.events.once('editor:close', invoke);
507
+ } else if (RED.events && typeof RED.events.on === 'function') {
508
+ const handler = () => {
509
+ if (RED.events.off) RED.events.off('editor:close', handler);
510
+ invoke();
511
+ };
512
+ RED.events.on('editor:close', handler);
513
+ }
364
514
  if (RED.tray && typeof RED.tray.close === 'function') {
365
- RED.tray.close(invoke);
515
+ RED.tray.close();
366
516
  } else {
367
517
  invoke();
368
518
  }
369
- }
519
+ };
370
520
 
371
521
  RED.nodes.registerType('join-wait', {
372
522
  category: 'function',
@@ -393,10 +543,9 @@
393
543
  firstMsg: { value: 'true', required: true },
394
544
  mapPayload: { value: 'false', required: true },
395
545
  disableComplete: { value: false },
396
- // Defaults to true so the queue survives a redeploy out of
397
- // the box (uses the default in-memory context store).
398
- // For full Node-RED restart persistence the user still needs
399
- // to point Persist store at a configured persistent store.
546
+ // On by default survives redeploys via the in-memory
547
+ // context store. Restart-persistence still needs Persist
548
+ // store pointing at a persistent context.
400
549
  persistOnRestart: { value: true },
401
550
  persistStore: { value: '' },
402
551
  },
@@ -411,14 +560,14 @@
411
560
  return this.name ? 'node_label_italic' : '';
412
561
  },
413
562
  oneditprepare: function () {
414
- $('#node-input-pathTopic').typedInput({
563
+ $(SEL.pathField).typedInput({
415
564
  default: this.pathTopicType || 'msg',
416
- typeField: $('#node-input-pathTopicType'),
565
+ typeField: $(SEL.pathFieldType),
417
566
  types: ['msg', 'flow', 'global'],
418
567
  });
419
- $('#node-input-correlationTopic').typedInput({
568
+ $(SEL.correlator).typedInput({
420
569
  default: this.correlationTopicType || 'undefined',
421
- typeField: $('#node-input-correlationTopicType'),
570
+ typeField: $(SEL.correlatorType),
422
571
  types: [
423
572
  { value: 'undefined', label: RED._('common.type.undefined'), hasValue: false },
424
573
  'msg',
@@ -427,34 +576,50 @@
427
576
  'jsonata',
428
577
  ],
429
578
  });
430
- $('#node-input-timeout').spinner({ min: 1 });
579
+ // Decimals OK (runtime multiplies by the unit factor).
580
+ $(SEL.timeout).spinner({ min: 0.001, step: 1, numberFormat: 'n' });
431
581
 
432
- initEditableList('node-input-paths-container', toArray(this.paths), {
582
+ initEditableList(LIST_ID.paths, toArray(this.paths), {
433
583
  placeholder: 'e.g. sensor_1',
434
584
  onChange: updateOutputPreview,
585
+ height: 200,
435
586
  });
436
- initEditableList('node-input-pathsToExpire-container', toArray(this.pathsToExpire), {
587
+ initEditableList(LIST_ID.expire, toArray(this.pathsToExpire), {
437
588
  placeholder: 'e.g. abort',
589
+ height: 140,
438
590
  });
439
591
  rebindRegexValidation();
440
592
 
441
- $('#node-input-pathTopic').on('change input', updateOutputPreview);
593
+ $(SEL.pathField).on('change input', updateOutputPreview);
442
594
  updateOutputPreview();
443
595
 
444
- $('#node-input-timeout, #node-input-timeoutUnits').on('change input', updateTimeoutHint);
596
+ $(SEL.timeout + ', ' + SEL.timeoutUnits).on('change input', updateTimeoutHint);
445
597
  updateTimeoutHint();
446
598
 
447
599
  populatePersistStores(this.persistStore);
448
600
 
449
- $('#join-wait-open-examples').on('click', openExamplesDialog);
601
+ // Persist store row is meaningless without Preserve queue.
602
+ const $persistRow = $(SEL.persistStoreRow + ', ' + SEL.persistStoreTip);
603
+ const syncPersistStoreVisibility = () => {
604
+ $persistRow.toggle($(SEL.persistOnRestart).is(':checked'));
605
+ resizePathsList();
606
+ };
607
+ $(SEL.persistOnRestart).on('change', syncPersistStoreVisibility);
608
+ syncPersistStoreVisibility();
609
+
610
+ $(SEL.examplesButton).on('click', openExamplesDialog);
611
+
612
+ // Reflow the list when Advanced toggles (oneditresize
613
+ // only fires on tray drag).
614
+ $(SEL.advancedDetails).on('toggle', resizePathsList);
450
615
  },
451
616
  oneditsave: function () {
452
- this.paths = readEditableList('node-input-paths-container');
453
- this.pathsToExpire = readEditableList('node-input-pathsToExpire-container');
617
+ this.paths = readEditableList(LIST_ID.paths);
618
+ this.pathsToExpire = readEditableList(LIST_ID.expire);
454
619
  },
455
- oneditresize: function () {
456
- $('#node-input-paths-container').css('min-height', '100px');
457
- $('#node-input-pathsToExpire-container').css('min-height', '60px');
620
+ oneditresize: function (size) {
621
+ lastTraySize = size;
622
+ resizePathsList();
458
623
  },
459
624
  });
460
625
  })();
@@ -550,8 +715,12 @@
550
715
  is kept in the merged output.</li>
551
716
  <li>Pad <b>Timeout</b> with a small overhead (~5–10&nbsp;ms) for
552
717
  evaluation time when working with very short windows.</li>
553
- <li>When <b>Preserve queue</b> is on, the queue is saved to a JSON
554
- file under <code>{userDir}/join-wait/{nodeId}.json</code> on close
555
- and reloaded on next start.</li>
718
+ <li>When <b>Preserve queue</b> is on, the queue is written to the
719
+ node's context on close (and reloaded on the next start). With
720
+ the default in-memory store this only survives a redeploy; for
721
+ full restart persistence, point <b>Persist store</b> at a
722
+ store backed by <code>localfilesystem</code> (or any persistent
723
+ module) configured under <code>contextStorage</code> in
724
+ <code>settings.js</code>.</li>
556
725
  </ul>
557
726
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-join-wait",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "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.",
5
5
  "author": "Daniel Caspi <dan@element26.net>",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "main": "join-wait.js",
26
26
  "engines": {
27
- "node": ">=20"
27
+ "node": ">=18.5"
28
28
  },
29
29
  "files": [
30
30
  "join-wait.js",
@@ -63,8 +63,7 @@
63
63
  "version": ">=3.0.0",
64
64
  "nodes": {
65
65
  "join-wait": "join-wait.js"
66
- },
67
- "examples": "examples"
66
+ }
68
67
  },
69
68
  "dependencies": {
70
69
  "jsonata": "^2.0.5"