node-red-contrib-join-wait 0.6.0 → 0.6.2

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,174 @@ 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.2] - 2026-05-09
8
+
9
+ ### Fixed
10
+
11
+ - **Open example flows button** was a no-op. Now invokes
12
+ `core:show-examples-import-dialog` directly, opening the dialog as a
13
+ modal above the edit tray.
14
+ - **Wait paths editableList overlapped the form rows below it** for
15
+ long lists. Resized via the canonical Node-RED pattern (numeric
16
+ height + `oneditresize` filling the tray, no upper clamp); re-flows
17
+ on the Advanced `<details>` toggle.
18
+ - **Persist store admin call** now prefixes `RED.settings.apiRootUrl`,
19
+ so it works under a custom `httpAdminRoot` or behind a reverse
20
+ proxy.
21
+ - **Help text** no longer references the file-based persistence path
22
+ removed in 0.6.0; corrected to describe the context-store backend.
23
+
24
+ ### Added
25
+
26
+ - **Bulk paste in path lists** — paste newline-separated text into any
27
+ Wait paths or Reset paths row to fill the row + append one row per
28
+ remaining line.
29
+ - **Richer inline validation** — output preview reflects
30
+ `pathTopicType` and shows repeat counts as `path_1 (×2): …`;
31
+ duplicate Reset paths and overlap between Wait/Reset paths are
32
+ flagged inline; Timeout accepts fractional values.
33
+ - Tests for the `/join-wait/stores` admin endpoint and drift fences
34
+ in `editor_spec.js` so the help text never re-acquires legacy keys.
35
+
36
+ ### Changed
37
+
38
+ - Persist store row hides when Preserve queue is off; output-preview
39
+ and Advanced summary use Node-RED CSS variables (theme-aware);
40
+ Timeout field widths cleaned up.
41
+
42
+ ## [0.6.1] - 2026-05-08
43
+
44
+ ### Fixed
45
+
46
+ - Shipped example flows wouldn't import — inject nodes were missing
47
+ the standard top-level `topic`, `payload`, `repeat`, `crontab`,
48
+ `once`, and `onceDelay` fields. All five examples regenerated in
49
+ the canonical inject format.
50
+ - First attempt at fixing the **Open example flows** button (closed
51
+ the edit tray before invoking the import dialog) — didn't reliably
52
+ surface; see 0.6.2 for the working fix.
53
+
7
54
  ## [0.6.0] - 2026-05-08
8
55
 
9
56
  ### Added
10
57
 
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.
58
+ - **Modern `(msg, send, done)` input handler** proper async-message
59
+ tracking and graceful shutdown (the old auto-completion fired
60
+ before awaited work resolved).
61
+ - **Editor UX overhaul** Wait paths and Reset paths now use
62
+ `editableList` (add/remove/reorder rows; inline validation for
63
+ empty rows + invalid regex; Enter adds the next). Inline output
64
+ preview, smart short-timeout warning, jsonata-validity check on
65
+ Group by, tooltips on every label, an **Open example flows**
66
+ shortcut, and an **Advanced** collapsible. Field labels renamed
67
+ for clarity.
68
+ - **Persist store dropdown** new `/join-wait/stores` admin route
69
+ exposes the configured context stores; the editor renders a typo-proof
70
+ `<select>`.
71
+ - **Smarter `node.status()`** single-group nodes show progress
72
+ (`2/3 received`); validation / regex-compile / correlator-evaluation
73
+ failures show a red ring with a short reason.
74
+ - **Five shipped example flows** Quickstart, Correlation, Reset,
75
+ Regex, Exact order discoverable via **Menu Import → Examples
76
+ join-wait**.
77
+ - **`msg.reset`** silently drains the queue for the current
78
+ correlation group.
67
79
 
68
80
  ### Changed
69
81
 
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.
82
+ - **Persistence rewritten on top of Node-RED's context store**,
83
+ replacing the `node-persist` singleton (which raced when multiple
84
+ `join-wait` nodes shared the flow). Default in-memory store keeps
85
+ queues across deploys; a configured persistent store keeps them
86
+ across full restarts. **Preserve queue** defaults to on; the node
87
+ auto-picks the first persistent named store when the default is
88
+ memory.
89
+ - **`msg.pathsToWait`, `msg.pathsToExpire`, `msg.useRegex`** are now
90
+ one-shot overrides they apply only to the current message
91
+ instead of permanently mutating the node's stored config.
92
+ - **Expired output uses the configured Path field** for the merged
93
+ data (matched the success output). Previously hardcoded to
94
+ `msg.paths`.
95
+ - **Code split** into `lib/config.js`, `lib/matcher.js`,
96
+ `lib/persist.js`; `findAllPaths{AnyOrder,ExactOrder}` return a
97
+ `{matched, keep}` shape; `drainQueue` takes options; internal
98
+ renames for clarity (config keys unchanged for back-compat).
99
+ - jsonata bumped to v2 (proper async `evaluate()`); Node-RED 3.0,
100
+ Node.js 20.
142
101
 
143
102
  ### Fixed
144
103
 
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.
104
+ - `node.error` / `node.warn` correctly pass the originating `msg`
105
+ (previously passed as `[msg, null]` since 0.5.x — Catch nodes
106
+ downstream now receive the real message).
107
+ - Close handler always calls `done` even when the context-store
108
+ write rejects, so persistence failures don't hold up shutdown.
109
+ - `mapPayload` no longer mutates the caller's `pathTopic` object.
154
110
  - Possible race when multiple `join-wait` nodes shared the global
155
- `node-persist` singleton (now uses an isolated context-store entry per
111
+ `node-persist` singleton (now an isolated context-store entry per
156
112
  node).
157
113
 
158
114
  ### Tooling
159
115
 
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).
116
+ - GitHub Actions CI replaces Travis (matrix on Node 20/22/24);
117
+ publish job gated on lint + format + spellcheck + test matrix.
118
+ ESLint v9 (flat config), Prettier v3, c8 (replaces nyc), cspell
119
+ (replaces `markdown-spellcheck`); Dependabot for monthly npm + GHA
120
+ updates. Test suite expanded from 32 to 102 cases.
121
+
122
+ ## [0.5.3] - 2021-10-31
123
+
124
+ - Update dependencies.
125
+
126
+ ## [0.5.2] - 2021-02-02
127
+
128
+ - Retain state across restart
129
+ ([#7](https://github.com/dxdc/node-red-contrib-join-wait/issues/7)).
130
+
131
+ ## [0.5.1] - 2021-01-05
132
+
133
+ - More automated testing and code coverage.
134
+
135
+ ## [0.5.0] - 2020-12-17
136
+
137
+ - Expire earlier for "any order" mode
138
+ ([#4](https://github.com/dxdc/node-red-contrib-join-wait/issues/4)).
139
+
140
+ ## [0.4.5] - 2020-12-15
141
+
142
+ - Add automated tests; address several minor issues uncovered by
143
+ testing; refactoring and readability changes.
144
+
145
+ ## [0.4.0] - 2020-12-14
146
+
147
+ - Reworked logic for exact-order mode
148
+ ([#1](https://github.com/dxdc/node-red-contrib-join-wait/issues/1)).
149
+ - Support for duplicate path names
150
+ ([#2](https://github.com/dxdc/node-red-contrib-join-wait/issues/2)).
151
+ - Show all unmatched paths with individual warnings.
152
+ - Minor changes to config UI and defaults.
153
+
154
+ ## [0.3.5] - 2020-05-26
155
+
156
+ - Enforce unique path names.
157
+ - Regex-based path matching.
158
+
159
+ ## [0.3.4] - 2020-01-17
160
+
161
+ - Don't reuse the `msg` variable.
162
+
163
+ ## [0.3.3] - 2020-01-17
164
+
165
+ - Fix bug with paths / `clearTimeout`.
166
+ - Minor code optimizations; add Example 3 to documentation.
167
+
168
+ ## [0.3.2] - 2020-01-16
169
+
170
+ - `msg.complete` handling.
171
+ - `timeoutUnits` in settings.
172
+ - `ignoreUnmatched` paths option in settings.
173
+ - Updated documentation.
174
+
175
+ ## [0.3.0] - 2020-01-14
176
+
177
+ - 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
  ```
@@ -11,7 +11,15 @@
11
11
  "type": "inject",
12
12
  "z": "qs-tab",
13
13
  "name": "path_1",
14
- "props": [{ "p": "topic", "v": "path_1", "vt": "str" }, { "p": "payload", "v": "hello-1", "vt": "str" }],
14
+ "props": [
15
+ { "p": "topic", "vt": "str" },
16
+ { "p": "payload", "v": "hello-1", "vt": "str" }
17
+ ],
18
+ "repeat": "",
19
+ "crontab": "",
20
+ "once": false,
21
+ "onceDelay": "",
22
+ "topic": "path_1",
15
23
  "x": 130,
16
24
  "y": 100,
17
25
  "wires": [["qs-jw"]]
@@ -21,7 +29,15 @@
21
29
  "type": "inject",
22
30
  "z": "qs-tab",
23
31
  "name": "path_2",
24
- "props": [{ "p": "topic", "v": "path_2", "vt": "str" }, { "p": "payload", "v": "hello-2", "vt": "str" }],
32
+ "props": [
33
+ { "p": "topic", "vt": "str" },
34
+ { "p": "payload", "v": "hello-2", "vt": "str" }
35
+ ],
36
+ "repeat": "",
37
+ "crontab": "",
38
+ "once": false,
39
+ "onceDelay": "",
40
+ "topic": "path_2",
25
41
  "x": 130,
26
42
  "y": 160,
27
43
  "wires": [["qs-jw"]]
@@ -12,6 +12,11 @@
12
12
  "z": "corr-tab",
13
13
  "name": "split flow",
14
14
  "props": [{ "p": "payload", "v": "{}", "vt": "json" }],
15
+ "repeat": "",
16
+ "crontab": "",
17
+ "once": false,
18
+ "onceDelay": "",
19
+ "topic": "",
15
20
  "x": 120,
16
21
  "y": 130,
17
22
  "wires": [["corr-set1", "corr-delay", "corr-set3"]]
@@ -22,6 +27,11 @@
22
27
  "z": "corr-tab",
23
28
  "name": "topic = path_1",
24
29
  "rules": [{ "t": "set", "p": "topic", "pt": "msg", "to": "path_1", "tot": "str" }],
30
+ "action": "",
31
+ "property": "",
32
+ "from": "",
33
+ "to": "",
34
+ "reg": false,
25
35
  "x": 320,
26
36
  "y": 80,
27
37
  "wires": [["corr-jw"]]
@@ -34,6 +44,15 @@
34
44
  "pauseType": "delay",
35
45
  "timeout": "1",
36
46
  "timeoutUnits": "seconds",
47
+ "rate": "1",
48
+ "nbRateUnits": "1",
49
+ "rateUnits": "second",
50
+ "randomFirst": "1",
51
+ "randomLast": "5",
52
+ "randomUnits": "seconds",
53
+ "drop": false,
54
+ "allowrate": false,
55
+ "outputs": 1,
37
56
  "x": 320,
38
57
  "y": 130,
39
58
  "wires": [["corr-set2"]]
@@ -44,6 +63,11 @@
44
63
  "z": "corr-tab",
45
64
  "name": "topic = path_2",
46
65
  "rules": [{ "t": "set", "p": "topic", "pt": "msg", "to": "path_2", "tot": "str" }],
66
+ "action": "",
67
+ "property": "",
68
+ "from": "",
69
+ "to": "",
70
+ "reg": false,
47
71
  "x": 480,
48
72
  "y": 130,
49
73
  "wires": [["corr-jw"]]
@@ -54,6 +78,11 @@
54
78
  "z": "corr-tab",
55
79
  "name": "topic = path_3",
56
80
  "rules": [{ "t": "set", "p": "topic", "pt": "msg", "to": "path_3", "tot": "str" }],
81
+ "action": "",
82
+ "property": "",
83
+ "from": "",
84
+ "to": "",
85
+ "reg": false,
57
86
  "x": 320,
58
87
  "y": 180,
59
88
  "wires": [["corr-jw"]]
@@ -11,7 +11,12 @@
11
11
  "type": "inject",
12
12
  "z": "rst-tab",
13
13
  "name": "path_1",
14
- "props": [{ "p": "topic", "v": "path_1", "vt": "str" }],
14
+ "props": [{ "p": "topic", "vt": "str" }],
15
+ "repeat": "",
16
+ "crontab": "",
17
+ "once": false,
18
+ "onceDelay": "",
19
+ "topic": "path_1",
15
20
  "x": 130,
16
21
  "y": 80,
17
22
  "wires": [["rst-jw"]]
@@ -21,7 +26,12 @@
21
26
  "type": "inject",
22
27
  "z": "rst-tab",
23
28
  "name": "path_2",
24
- "props": [{ "p": "topic", "v": "path_2", "vt": "str" }],
29
+ "props": [{ "p": "topic", "vt": "str" }],
30
+ "repeat": "",
31
+ "crontab": "",
32
+ "once": false,
33
+ "onceDelay": "",
34
+ "topic": "path_2",
25
35
  "x": 130,
26
36
  "y": 130,
27
37
  "wires": [["rst-jw"]]
@@ -31,7 +41,12 @@
31
41
  "type": "inject",
32
42
  "z": "rst-tab",
33
43
  "name": "reset",
34
- "props": [{ "p": "topic", "v": "abort", "vt": "str" }],
44
+ "props": [{ "p": "topic", "vt": "str" }],
45
+ "repeat": "",
46
+ "crontab": "",
47
+ "once": false,
48
+ "onceDelay": "",
49
+ "topic": "abort",
35
50
  "x": 130,
36
51
  "y": 200,
37
52
  "wires": [["rst-jw"]]
@@ -12,9 +12,14 @@
12
12
  "z": "rx-tab",
13
13
  "name": "sensor_a",
14
14
  "props": [
15
- { "p": "topic", "v": "sensor_a", "vt": "str" },
15
+ { "p": "topic", "vt": "str" },
16
16
  { "p": "payload", "v": "42", "vt": "num" }
17
17
  ],
18
+ "repeat": "",
19
+ "crontab": "",
20
+ "once": false,
21
+ "onceDelay": "",
22
+ "topic": "sensor_a",
18
23
  "x": 130,
19
24
  "y": 80,
20
25
  "wires": [["rx-jw"]]
@@ -25,9 +30,14 @@
25
30
  "z": "rx-tab",
26
31
  "name": "heartbeat",
27
32
  "props": [
28
- { "p": "topic", "v": "heartbeat", "vt": "str" },
33
+ { "p": "topic", "vt": "str" },
29
34
  { "p": "payload", "v": "ok", "vt": "str" }
30
35
  ],
36
+ "repeat": "",
37
+ "crontab": "",
38
+ "once": false,
39
+ "onceDelay": "",
40
+ "topic": "heartbeat",
31
41
  "x": 130,
32
42
  "y": 140,
33
43
  "wires": [["rx-jw"]]
@@ -11,7 +11,12 @@
11
11
  "type": "inject",
12
12
  "z": "eo-tab",
13
13
  "name": "start",
14
- "props": [{ "p": "topic", "v": "start", "vt": "str" }],
14
+ "props": [{ "p": "topic", "vt": "str" }],
15
+ "repeat": "",
16
+ "crontab": "",
17
+ "once": false,
18
+ "onceDelay": "",
19
+ "topic": "start",
15
20
  "x": 130,
16
21
  "y": 80,
17
22
  "wires": [["eo-jw"]]
@@ -21,7 +26,12 @@
21
26
  "type": "inject",
22
27
  "z": "eo-tab",
23
28
  "name": "work",
24
- "props": [{ "p": "topic", "v": "work", "vt": "str" }],
29
+ "props": [{ "p": "topic", "vt": "str" }],
30
+ "repeat": "",
31
+ "crontab": "",
32
+ "once": false,
33
+ "onceDelay": "",
34
+ "topic": "work",
25
35
  "x": 130,
26
36
  "y": 130,
27
37
  "wires": [["eo-jw"]]
@@ -31,7 +41,12 @@
31
41
  "type": "inject",
32
42
  "z": "eo-tab",
33
43
  "name": "end",
34
- "props": [{ "p": "topic", "v": "end", "vt": "str" }],
44
+ "props": [{ "p": "topic", "vt": "str" }],
45
+ "repeat": "",
46
+ "crontab": "",
47
+ "once": false,
48
+ "onceDelay": "",
49
+ "topic": "end",
35
50
  "x": 130,
36
51
  "y": 180,
37
52
  "wires": [["eo-jw"]]
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,6 +139,48 @@
133
139
 
134
140
  <script type="text/javascript">
135
141
  (function () {
142
+ // Form-element selectors gathered in one place. Keeps the rest
143
+ // of the IIFE free of stringly-typed `#node-input-*` lookups
144
+ // and makes the editor surface auditable at a glance — every
145
+ // hook into the dialog is listed here.
146
+ var SEL = {
147
+ pathField: '#node-input-pathTopic',
148
+ pathFieldType: '#node-input-pathTopicType',
149
+ correlator: '#node-input-correlationTopic',
150
+ correlatorType: '#node-input-correlationTopicType',
151
+ timeout: '#node-input-timeout',
152
+ timeoutUnits: '#node-input-timeoutUnits',
153
+ useRegex: '#node-input-useRegex',
154
+ persistOnRestart: '#node-input-persistOnRestart',
155
+ persistStore: '#node-input-persistStore',
156
+ outputPreview: '#join-wait-output-preview',
157
+ timeoutHint: '#join-wait-timeout-hint',
158
+ examplesButton: '#join-wait-open-examples',
159
+ persistStoreRow: '#join-wait-persist-store-row',
160
+ persistStoreTip: '#join-wait-persist-store-tip',
161
+ dialogForm: '#dialog-form',
162
+ advancedDetails: '#dialog-form details',
163
+ };
164
+ // editableList wrapper IDs (no leading '#'); consumers either
165
+ // pass them to readEditableList or interpolate `#` themselves.
166
+ var LIST_ID = {
167
+ paths: 'node-input-paths-container',
168
+ expire: 'node-input-pathsToExpire-container',
169
+ };
170
+ function listInputs(id) {
171
+ return $('#' + id + ' input[type=text]');
172
+ }
173
+ // Read the typed-input pair behind the Path field — name + type.
174
+ // Used wherever both halves are needed together (currently the
175
+ // output preview); centralising it avoids drift between the two
176
+ // selectors.
177
+ function readPathField() {
178
+ return {
179
+ name: $(SEL.pathField).val() || 'topic',
180
+ type: $(SEL.pathFieldType).val() || 'msg',
181
+ };
182
+ }
183
+
136
184
  function arrayValidator(allowEmpty, requireUnique) {
137
185
  return function (v) {
138
186
  // Accept legacy JSON-string format too, for back-compat with old flows.
@@ -191,25 +239,82 @@
191
239
  input.attr('title', ok ? '' : message || '');
192
240
  }
193
241
 
194
- function validateRow(input) {
195
- var $useRegex = $('#node-input-useRegex');
196
- var v = String(input.val() || '');
197
- if (v === '') {
242
+ // Single-row validation: empty / invalid regex. List-level
243
+ // checks (uniqueness, cross-list overlap) live in validateAllRows
244
+ // because they need both lists' current values in scope.
245
+ function validateRow(input, ownValues, otherValues, opts) {
246
+ var requireUnique = opts && opts.requireUnique;
247
+ var otherListLabel = (opts && opts.otherListLabel) || '';
248
+ var value = String(input.val() || '');
249
+ if (value === '') {
198
250
  setRowValid(input, false, 'path name cannot be empty');
199
251
  return false;
200
252
  }
201
- if ($useRegex.is(':checked') && !isValidRegex(v)) {
253
+ if ($(SEL.useRegex).is(':checked') && !isValidRegex(value)) {
202
254
  setRowValid(input, false, 'invalid regular expression');
203
255
  return false;
204
256
  }
257
+ if (
258
+ requireUnique &&
259
+ ownValues &&
260
+ ownValues.filter(function (x) {
261
+ return x === value;
262
+ }).length > 1
263
+ ) {
264
+ setRowValid(input, false, 'duplicate entry — must be unique');
265
+ return false;
266
+ }
267
+ if (otherValues && otherValues.indexOf(value) !== -1) {
268
+ setRowValid(
269
+ input,
270
+ false,
271
+ '"' + value + '" is also in ' + otherListLabel + " — paths shouldn't overlap",
272
+ );
273
+ return false;
274
+ }
205
275
  setRowValid(input, true);
206
276
  return true;
207
277
  }
208
278
 
279
+ // Re-validate every row in both editableLists. Called whenever
280
+ // any row changes, the regex toggle flips, or rows are added /
281
+ // removed — so duplicate and cross-list overlap warnings are
282
+ // always in sync with the current state.
283
+ function validateAllRows() {
284
+ var $paths = listInputs(LIST_ID.paths);
285
+ var $expire = listInputs(LIST_ID.expire);
286
+ var pathValues = $paths
287
+ .map(function () {
288
+ return String($(this).val() || '');
289
+ })
290
+ .get();
291
+ var expireValues = $expire
292
+ .map(function () {
293
+ return String($(this).val() || '');
294
+ })
295
+ .get();
296
+ $paths.each(function () {
297
+ validateRow($(this), pathValues, expireValues, {
298
+ requireUnique: false,
299
+ otherListLabel: 'Reset paths',
300
+ });
301
+ });
302
+ $expire.each(function () {
303
+ validateRow($(this), expireValues, pathValues, {
304
+ requireUnique: true,
305
+ otherListLabel: 'Wait paths',
306
+ });
307
+ });
308
+ }
309
+
209
310
  function initEditableList(containerId, items, opts) {
210
311
  var $container = $('#' + containerId);
211
- $container.css({ 'min-height': '100px' });
212
312
  var placeholder = (opts && opts.placeholder) || 'e.g. sensor_1';
313
+ // A numeric height makes editableList scroll its items
314
+ // internally past the limit — without it (height:'auto'), a
315
+ // long list grows the wrapper and visually overlaps the form
316
+ // rows that follow. The actual height is recomputed in
317
+ // oneditresize to fill the available tray space.
213
318
  $container.editableList({
214
319
  addItem: function (row, index, data) {
215
320
  var value = (data && data.value) || '';
@@ -220,7 +325,7 @@
220
325
  style: 'width:100%',
221
326
  }).val(value);
222
327
  input.on('input change blur', function () {
223
- validateRow(input);
328
+ validateAllRows();
224
329
  if (opts && opts.onChange) opts.onChange();
225
330
  });
226
331
  // Pressing Enter in a row adds the next one.
@@ -234,16 +339,44 @@
234
339
  }, 0);
235
340
  }
236
341
  });
342
+ // Bulk-paste: paste newline-separated text into any
343
+ // row to fill the current row + add a new row per
344
+ // remaining line. Whitespace-only and empty lines
345
+ // are dropped. Single-line pastes fall through to
346
+ // the browser's default paste handling.
347
+ input.on('paste', function (e) {
348
+ var clip = (e.originalEvent || e).clipboardData;
349
+ if (!clip) return;
350
+ var text = clip.getData('text');
351
+ if (!text || !/[\r\n]/.test(text)) return;
352
+ var lines = text
353
+ .split(/\r?\n/)
354
+ .map(function (s) {
355
+ return s.trim();
356
+ })
357
+ .filter(function (s) {
358
+ return s.length > 0;
359
+ });
360
+ if (lines.length === 0) return;
361
+ e.preventDefault();
362
+ input.val(lines[0]);
363
+ for (var i = 1; i < lines.length; i++) {
364
+ $container.editableList('addItem', { value: lines[i] });
365
+ }
366
+ validateAllRows();
367
+ if (opts && opts.onChange) opts.onChange();
368
+ });
237
369
  row.append(input);
238
- validateRow(input);
370
+ validateAllRows();
239
371
  if (opts && opts.onChange) opts.onChange();
240
372
  },
241
373
  removeItem: function () {
374
+ validateAllRows();
242
375
  if (opts && opts.onChange) opts.onChange();
243
376
  },
244
377
  removable: true,
245
378
  sortable: true,
246
- height: 'auto',
379
+ height: (opts && opts.height) || 200,
247
380
  });
248
381
  (items || []).forEach(function (v) {
249
382
  $container.editableList('addItem', { value: v });
@@ -252,14 +385,7 @@
252
385
 
253
386
  // Re-validate every editableList row when the regex toggle flips.
254
387
  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
- });
388
+ $(SEL.useRegex).on('change', validateAllRows);
263
389
  }
264
390
 
265
391
  function readEditableList(containerId) {
@@ -287,50 +413,58 @@
287
413
  return [];
288
414
  }
289
415
 
290
- // Render a tiny preview of what msg.<pathField> looks like on the
291
- // success output, derived from the current Wait paths list.
416
+ // Render a tiny preview of what the path-field property looks
417
+ // like on the success output, derived from the current Wait paths
418
+ // list. Prefix matches the typed-input type so flow/global don't
419
+ // mis-render as msg. Repeated entries surface as `(×n)` so the
420
+ // n-of-the-same semantic is visible.
292
421
  function updateOutputPreview() {
293
- var paths = readEditableList('node-input-paths-container');
294
- var unique = [];
422
+ var paths = readEditableList(LIST_ID.paths);
423
+ var counts = Object.create(null);
424
+ var order = [];
295
425
  paths.forEach(function (p) {
296
- if (unique.indexOf(p) === -1) unique.push(p);
426
+ if (counts[p] === undefined) order.push(p);
427
+ counts[p] = (counts[p] || 0) + 1;
297
428
  });
298
- var pathField = $('#node-input-pathTopic').val() || 'topic';
299
- var $tip = $('#join-wait-output-preview');
300
- if (unique.length === 0) {
429
+ var field = readPathField();
430
+ var $tip = $(SEL.outputPreview);
431
+ if (order.length === 0) {
301
432
  $tip.text('');
302
433
  return;
303
434
  }
304
- var entries = unique.slice(0, 4).map(function (p) {
305
- return p + ': …';
435
+ var entries = order.slice(0, 4).map(function (p) {
436
+ return counts[p] > 1 ? p + ' (×' + counts[p] + '): …' : p + ': …';
306
437
  });
307
- if (unique.length > 4) entries.push('…');
308
- $tip.text('→ msg.' + pathField + ' = { ' + entries.join(', ') + ' }');
438
+ if (order.length > 4) entries.push('…');
439
+ $tip.text('→ ' + field.type + '.' + field.name + ' = { ' + entries.join(', ') + ' }');
309
440
  }
310
441
 
311
442
  // Warn when the resolved timeout is impractically short (the README
312
443
  // recommends padding ~5–10 ms for evaluation overhead).
313
444
  function updateTimeoutHint() {
314
- var t = Number($('#node-input-timeout').val()) || 0;
315
- var u = Number($('#node-input-timeoutUnits').val()) || 1;
445
+ var t = Number($(SEL.timeout).val()) || 0;
446
+ var u = Number($(SEL.timeoutUnits).val()) || 1;
316
447
  var ms = t * u;
317
- var $hint = $('#join-wait-timeout-hint');
448
+ var $hint = $(SEL.timeoutHint);
318
449
  if (ms > 0 && ms < 50) {
319
- $hint.text('Very short — pad ~10 ms to leave room for evaluation overhead.').show();
450
+ $hint.find('span').text('Very short — pad ~10 ms to leave room for evaluation overhead.');
451
+ $hint.show();
320
452
  } else {
321
453
  $hint.hide();
322
454
  }
323
455
  }
324
456
 
325
457
  // 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.
458
+ // /join-wait/stores admin route. apiRootUrl prefixes the call so
459
+ // it resolves correctly under custom httpAdminRoot / reverse
460
+ // proxies; falls back to the bare relative URL on older
461
+ // Node-RED versions where the setting isn't exposed.
328
462
  function populatePersistStores(currentValue) {
329
- var $sel = $('#node-input-persistStore');
330
- $.getJSON('join-wait/stores')
463
+ var $sel = $(SEL.persistStore);
464
+ var base = (RED.settings && RED.settings.apiRootUrl) || '';
465
+ $.getJSON(base + 'join-wait/stores')
331
466
  .done(function (stores) {
332
- if (!Array.isArray(stores) || stores.length === 0) return;
333
- stores.forEach(function (s) {
467
+ (Array.isArray(stores) ? stores : []).forEach(function (s) {
334
468
  if (s.name === 'default') return; // already represented as the blank option
335
469
  var label = s.module ? s.name + ' (' + s.module + ')' : s.name;
336
470
  $('<option/>').val(s.name).text(label).appendTo($sel);
@@ -346,16 +480,52 @@
346
480
  $sel.val(currentValue || '');
347
481
  })
348
482
  .fail(function () {
349
- // Admin route not reachable leave the select with just
350
- // its default option.
483
+ // Admin route not reachable. Preserve the saved
484
+ // value as an explicit option so the dropdown doesn't
485
+ // silently overwrite persistStore with '' on save.
486
+ if (currentValue) {
487
+ $('<option/>')
488
+ .val(currentValue)
489
+ .text(currentValue + ' (stores list unavailable)')
490
+ .appendTo($sel);
491
+ $sel.val(currentValue);
492
+ }
351
493
  });
352
494
  }
353
495
 
496
+ // Last `size` handed to oneditresize. Cached so the <details>
497
+ // toggle handler can re-run the resize math without waiting for
498
+ // the next tray drag.
499
+ var lastTraySize = null;
500
+
501
+ // Stretch the Wait paths editableList to fill the tray. Mirrors
502
+ // the pattern in Node-RED core (switch/change/httprequest):
503
+ // subtract every other row's outer height from the tray height
504
+ // and give the remainder to the list. The :visible filter mirrors
505
+ // httprequest so hidden rows (e.g. timeout-hint) are skipped
506
+ // explicitly. No upper clamp — when the user drags the tray
507
+ // taller they want to see more rows.
508
+ function resizePathsList() {
509
+ var $form = $(SEL.dialogForm);
510
+ if (!$form.length || !lastTraySize || !lastTraySize.height) return;
511
+ var height = lastTraySize.height;
512
+ $form.children(':not(.node-input-paths-container-row)').each(function () {
513
+ var $r = $(this);
514
+ if ($r.is(':visible')) height -= $r.outerHeight(true) || 0;
515
+ });
516
+ height -= 12; // breathing room for margins/padding the loop misses
517
+ if (height < 140) height = 140;
518
+ $('#' + LIST_ID.paths).editableList('height', height);
519
+ }
520
+
354
521
  function openExamplesDialog() {
355
- // Open the import dialog; the user picks Examples join-wait.
356
- // RED.actions exposes core actions across recent Node-RED versions.
522
+ // The import dialog opens as a modal above the edit tray, so
523
+ // the user can pick a flow and on close return to this edit
524
+ // dialog with in-flight edits intact. The earlier approach of
525
+ // closing the tray first was unreliable (close-callback drops
526
+ // when stack races editor teardown).
357
527
  if (RED.actions && typeof RED.actions.invoke === 'function') {
358
- RED.actions.invoke('core:show-import-dialog');
528
+ RED.actions.invoke('core:show-examples-import-dialog');
359
529
  }
360
530
  }
361
531
 
@@ -402,14 +572,14 @@
402
572
  return this.name ? 'node_label_italic' : '';
403
573
  },
404
574
  oneditprepare: function () {
405
- $('#node-input-pathTopic').typedInput({
575
+ $(SEL.pathField).typedInput({
406
576
  default: this.pathTopicType || 'msg',
407
- typeField: $('#node-input-pathTopicType'),
577
+ typeField: $(SEL.pathFieldType),
408
578
  types: ['msg', 'flow', 'global'],
409
579
  });
410
- $('#node-input-correlationTopic').typedInput({
580
+ $(SEL.correlator).typedInput({
411
581
  default: this.correlationTopicType || 'undefined',
412
- typeField: $('#node-input-correlationTopicType'),
582
+ typeField: $(SEL.correlatorType),
413
583
  types: [
414
584
  { value: 'undefined', label: RED._('common.type.undefined'), hasValue: false },
415
585
  'msg',
@@ -418,34 +588,57 @@
418
588
  'jsonata',
419
589
  ],
420
590
  });
421
- $('#node-input-timeout').spinner({ min: 1 });
591
+ // Spinner accepts decimals (1.5 seconds, 0.25 hours, …)
592
+ // — the runtime multiplies the value by the unit factor,
593
+ // so fractional units are valid. step:1 keeps the up/down
594
+ // arrows snappy for the common integer case.
595
+ $(SEL.timeout).spinner({ min: 0.001, step: 1, numberFormat: 'n' });
422
596
 
423
- initEditableList('node-input-paths-container', toArray(this.paths), {
597
+ initEditableList(LIST_ID.paths, toArray(this.paths), {
424
598
  placeholder: 'e.g. sensor_1',
425
599
  onChange: updateOutputPreview,
600
+ height: 200,
426
601
  });
427
- initEditableList('node-input-pathsToExpire-container', toArray(this.pathsToExpire), {
602
+ initEditableList(LIST_ID.expire, toArray(this.pathsToExpire), {
428
603
  placeholder: 'e.g. abort',
604
+ height: 140,
429
605
  });
430
606
  rebindRegexValidation();
431
607
 
432
- $('#node-input-pathTopic').on('change input', updateOutputPreview);
608
+ $(SEL.pathField).on('change input', updateOutputPreview);
433
609
  updateOutputPreview();
434
610
 
435
- $('#node-input-timeout, #node-input-timeoutUnits').on('change input', updateTimeoutHint);
611
+ $(SEL.timeout + ', ' + SEL.timeoutUnits).on('change input', updateTimeoutHint);
436
612
  updateTimeoutHint();
437
613
 
438
614
  populatePersistStores(this.persistStore);
439
615
 
440
- $('#join-wait-open-examples').on('click', openExamplesDialog);
616
+ // Hide the Persist store row when Preserve queue is off —
617
+ // the override is meaningless without persistence enabled.
618
+ var $persistRow = $(SEL.persistStoreRow + ', ' + SEL.persistStoreTip);
619
+ function syncPersistStoreVisibility() {
620
+ $persistRow.toggle($(SEL.persistOnRestart).is(':checked'));
621
+ resizePathsList();
622
+ }
623
+ $(SEL.persistOnRestart).on('change', syncPersistStoreVisibility);
624
+ syncPersistStoreVisibility();
625
+
626
+ $(SEL.examplesButton).on('click', openExamplesDialog);
627
+
628
+ // Re-run the editableList resize math when Advanced is
629
+ // expanded/collapsed — oneditresize only fires on tray
630
+ // drag, so without this the Wait paths list keeps the
631
+ // height it had before the toggle and doesn't reclaim
632
+ // (or yield) space.
633
+ $(SEL.advancedDetails).on('toggle', resizePathsList);
441
634
  },
442
635
  oneditsave: function () {
443
- this.paths = readEditableList('node-input-paths-container');
444
- this.pathsToExpire = readEditableList('node-input-pathsToExpire-container');
636
+ this.paths = readEditableList(LIST_ID.paths);
637
+ this.pathsToExpire = readEditableList(LIST_ID.expire);
445
638
  },
446
- oneditresize: function () {
447
- $('#node-input-paths-container').css('min-height', '100px');
448
- $('#node-input-pathsToExpire-container').css('min-height', '60px');
639
+ oneditresize: function (size) {
640
+ lastTraySize = size;
641
+ resizePathsList();
449
642
  },
450
643
  });
451
644
  })();
@@ -541,8 +734,12 @@
541
734
  is kept in the merged output.</li>
542
735
  <li>Pad <b>Timeout</b> with a small overhead (~5–10&nbsp;ms) for
543
736
  evaluation time when working with very short windows.</li>
544
- <li>When <b>Preserve queue</b> is on, the queue is saved to a JSON
545
- file under <code>{userDir}/join-wait/{nodeId}.json</code> on close
546
- and reloaded on next start.</li>
737
+ <li>When <b>Preserve queue</b> is on, the queue is written to the
738
+ node's context on close (and reloaded on the next start). With
739
+ the default in-memory store this only survives a redeploy; for
740
+ full restart persistence, point <b>Persist store</b> at a
741
+ store backed by <code>localfilesystem</code> (or any persistent
742
+ module) configured under <code>contextStorage</code> in
743
+ <code>settings.js</code>.</li>
547
744
  </ul>
548
745
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-join-wait",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",