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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.js ADDED
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ // Accept either a real array (new editor format) or a JSON-encoded string
4
+ // (legacy editor format). Returns false for anything else so callers can
5
+ // detect "not configured".
6
+ function normalizePaths(value) {
7
+ if (Array.isArray(value)) return value;
8
+ if (typeof value !== 'string' || value === '') return false;
9
+ try {
10
+ const json = JSON.parse(value);
11
+ return Array.isArray(json) ? json : false;
12
+ } catch (_err) {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ function hasDuplicatePath(arr) {
18
+ return arr.some((p, i) => arr.indexOf(p) !== i);
19
+ }
20
+
21
+ function compileRegex(arr) {
22
+ if (!Array.isArray(arr)) return arr;
23
+ return arr.map((pattern) => (pattern instanceof RegExp ? pattern : new RegExp(pattern)));
24
+ }
25
+
26
+ module.exports = { normalizePaths, hasDuplicatePath, compileRegex };
package/lib/matcher.js ADDED
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ // Pure path-matching helpers. No node-red dependencies — easy to unit test.
4
+
5
+ function flatten(arr) {
6
+ return [].concat.apply([], arr);
7
+ }
8
+
9
+ function condenseWithCount(arr) {
10
+ const map = arr.reduce((m, key) => m.set(key, (m.get(key) || 0) + 1), new Map());
11
+ return Array.from(map, ([name, value]) => ({ name, value }));
12
+ }
13
+
14
+ function regexIndexOf(arr, needle) {
15
+ for (let i = 0; i < arr.length; i++) {
16
+ if (arr[i].test(needle)) return i;
17
+ }
18
+ return -1;
19
+ }
20
+
21
+ function matchesAny(val, patterns, useRegex) {
22
+ return useRegex ? patterns.some((p) => p.test(val)) : patterns.includes(val);
23
+ }
24
+
25
+ function anyMatches(values, patterns, useRegex) {
26
+ return values.some((v) => matchesAny(v, patterns, useRegex));
27
+ }
28
+
29
+ function countPathsAnyOrder(keys, waitMap, useRegex) {
30
+ const used = new Set();
31
+ return waitMap.map((p) => {
32
+ let count = 0;
33
+ for (let i = 0; i < keys.length; i++) {
34
+ if (used.has(i)) continue;
35
+ const matched = useRegex ? p.name.test(keys[i]) : p.name === keys[i];
36
+ if (!matched) continue;
37
+ used.add(i);
38
+ count++;
39
+ }
40
+ return count < p.value ? count : true;
41
+ });
42
+ }
43
+
44
+ // Result shape returned by findAllPaths*:
45
+ // { matched: true } — wait paths satisfied; caller drains.
46
+ // { matched: false, keep: <number> }— not yet; caller may expire trailing
47
+ // entries past `keep`.
48
+ const MATCHED = Object.freeze({ matched: true });
49
+
50
+ function findAllPathsAnyOrder(arr, waitPaths, useRegex) {
51
+ const waitMap = condenseWithCount(waitPaths);
52
+ const keys = flatten(arr);
53
+ const result = countPathsAnyOrder(keys, waitMap, useRegex);
54
+
55
+ if (result.every((p) => p === true)) return MATCHED;
56
+
57
+ const originalString = result.toString();
58
+ for (let i = 0; i < arr.length; i++) {
59
+ const newKeys = flatten(arr.slice(i + 1));
60
+ const expireByOne = countPathsAnyOrder(newKeys, waitMap, useRegex);
61
+ if (originalString !== expireByOne.toString()) {
62
+ return { matched: false, keep: arr.length - i };
63
+ }
64
+ }
65
+
66
+ /* c8 ignore next */
67
+ return { matched: false, keep: 0 };
68
+ }
69
+
70
+ function findAllPathsExactOrder(arr, waitPaths, useRegex) {
71
+ let start = 0;
72
+ let marker = false;
73
+
74
+ for (let i = 0; i < arr.length; i++) {
75
+ for (let j = 0; j < arr[i].length; j++) {
76
+ const p = arr[i][j];
77
+
78
+ const offBy = marker === false ? 0 : marker + 1;
79
+ const unusedWaitPaths = waitPaths.slice(offBy);
80
+ let index = useRegex ? regexIndexOf(unusedWaitPaths, p) : unusedWaitPaths.indexOf(p);
81
+
82
+ if (index === -1) {
83
+ if (offBy > 0) {
84
+ index = useRegex ? regexIndexOf(waitPaths, p) : waitPaths.indexOf(p);
85
+ if (index > 0) marker = false;
86
+ }
87
+ } else {
88
+ index += offBy;
89
+ }
90
+
91
+ if (index === 0) {
92
+ start = i;
93
+ } else if (index === -1 || marker === false) {
94
+ continue;
95
+ } else if (index < marker || index > marker + 1) {
96
+ marker = false;
97
+ continue;
98
+ }
99
+
100
+ if (index === waitPaths.length - 1) return MATCHED;
101
+
102
+ marker = index;
103
+ }
104
+ }
105
+
106
+ return { matched: false, keep: marker === false ? 0 : arr.length - start };
107
+ }
108
+
109
+ module.exports = {
110
+ matchesAny,
111
+ anyMatches,
112
+ findAllPathsAnyOrder,
113
+ findAllPathsExactOrder,
114
+ };
package/lib/persist.js ADDED
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ // Persistence adapter over Node-RED's context store API. Production code
4
+ // passes `node.context()` (or any object exposing `get/set` with the
5
+ // callback signature). Tests pass an in-memory stub.
6
+ //
7
+ // Using the context store is the idiomatic Node-RED way:
8
+ // - Default memory store keeps queues across deploys (in-process).
9
+ // - A persistent store configured in settings.js (e.g. localfilesystem)
10
+ // keeps queues across full restarts.
11
+ // The store is selected per-instance via the `persistStore` config field.
12
+
13
+ const KEY = 'paths';
14
+
15
+ function load(ctx, store) {
16
+ return new Promise((resolve) => {
17
+ try {
18
+ ctx.get(KEY, store, (err, value) => {
19
+ /* c8 ignore next 4 */
20
+ if (err) {
21
+ resolve({});
22
+ return;
23
+ }
24
+ resolve(value && typeof value === 'object' ? value : {});
25
+ });
26
+ /* c8 ignore next 3 */
27
+ } catch (_err) {
28
+ resolve({});
29
+ }
30
+ });
31
+ }
32
+
33
+ function save(ctx, store, paths) {
34
+ // Strip non-serializable timeOut handles before writing.
35
+ const serializable = {};
36
+ for (const t of Object.keys(paths)) {
37
+ serializable[t] = { queue: paths[t].queue };
38
+ }
39
+ return new Promise((resolve) => {
40
+ try {
41
+ ctx.set(KEY, serializable, store, () => resolve());
42
+ /* c8 ignore next 3 */
43
+ } catch (_err) {
44
+ resolve();
45
+ }
46
+ });
47
+ }
48
+
49
+ function clear(ctx, store) {
50
+ return new Promise((resolve) => {
51
+ try {
52
+ ctx.set(KEY, undefined, store, () => resolve());
53
+ /* c8 ignore next 3 */
54
+ } catch (_err) {
55
+ resolve();
56
+ }
57
+ });
58
+ }
59
+
60
+ module.exports = { load, save, clear, KEY };
package/lib/store.js ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ // Resolves which Node-RED context store the join-wait node should use
4
+ // for its queue. Order of preference:
5
+ //
6
+ // 1. The explicit per-node `Persist store` (config.persistStore).
7
+ // 2. If "Preserve queue" is on AND the default store is memory AND
8
+ // there's a non-memory named store, pick that — saves the user
9
+ // from having to set Persist store on every node just to get
10
+ // restart persistence.
11
+ // 3. Otherwise undefined (use Node-RED's default store).
12
+ //
13
+ // The contextStorage entry shape supported by Node-RED:
14
+ // { default: { module: 'memory' }, file: { module: 'localfilesystem' } }
15
+ // { default: 'memoryOnly', memoryOnly: { module: 'memory' }, ... }
16
+ // { default: 'memory' } (string shorthand)
17
+
18
+ // Returns the module string for a contextStorage entry. The entry may itself
19
+ // be a module-config object (`{ module: 'memory' }`) or a plain module name
20
+ // string (`'memory'`).
21
+ function entryModule(entry) {
22
+ if (!entry) return undefined;
23
+ if (typeof entry === 'string') return entry;
24
+ if (typeof entry === 'object') return entry.module;
25
+ /* c8 ignore next */
26
+ return undefined;
27
+ }
28
+
29
+ // `default` may itself be a string alias to another named entry (e.g.
30
+ // `default: 'memoryOnly'` referring to a named `memoryOnly` store).
31
+ // Follow the alias to find the actual module.
32
+ function resolveDefaultModule(contextStorage) {
33
+ const def = contextStorage.default;
34
+ if (!def) return undefined;
35
+ if (typeof def === 'string' && contextStorage[def]) {
36
+ return entryModule(contextStorage[def]);
37
+ }
38
+ return entryModule(def);
39
+ }
40
+
41
+ function resolveContextStore(contextStorage, configStore, persistQueue) {
42
+ if (configStore) return configStore;
43
+ if (!persistQueue) return undefined;
44
+ if (!contextStorage || typeof contextStorage !== 'object') return undefined;
45
+
46
+ const defaultModule = resolveDefaultModule(contextStorage);
47
+ // If the default is anything other than memory (or unset → memory) it'll
48
+ // already survive restarts — nothing to upgrade.
49
+ if (defaultModule && defaultModule !== 'memory') return undefined;
50
+
51
+ // Look for a persistent named store. Skip the `default` key itself, and
52
+ // any entries whose resolved module is memory.
53
+ const defaultAlias = typeof contextStorage.default === 'string' ? contextStorage.default : undefined;
54
+ for (const name of Object.keys(contextStorage)) {
55
+ if (name === 'default') continue;
56
+ if (name === defaultAlias) continue;
57
+ const m = entryModule(contextStorage[name]);
58
+ if (m && m !== 'memory') return name;
59
+ }
60
+
61
+ return undefined;
62
+ }
63
+
64
+ module.exports = { resolveContextStore };
package/package.json CHANGED
@@ -1,61 +1,94 @@
1
1
  {
2
- "author": "Daniel Caspi <dan@element26.net>",
3
- "bugs": {
4
- "url": "https://github.com/dxdc/node-red-contrib-join-wait/issues"
5
- },
6
- "dependencies": {
7
- "jsonata": "^1.8.5",
8
- "node-persist": "^2.1.0"
9
- },
10
- "description": "Waits for incoming messages from different input paths to arrive within a fixed time window.",
11
- "devDependencies": {
12
- "@types/node-red": "^1.1.1",
13
- "eslint": "^8.1.0",
14
- "eslint-plugin-mocha": "^9.0.0",
15
- "markdown-spellcheck": "^1.3.1",
16
- "mocha": "^9.1.3",
17
- "node-red": "^2.1.3",
18
- "node-red-node-test-helper": "^0.2.7",
19
- "nyc": "^15.1.0",
20
- "prettier": "^2.4.1",
21
- "rimraf": "^3.0.2"
22
- },
23
- "homepage": "https://github.com/dxdc/node-red-contrib-join-wait#readme",
24
- "keywords": [
25
- "node-red",
26
- "join",
27
- "parallel",
28
- "timeout",
29
- "wait",
30
- "merge"
31
- ],
32
- "license": "MIT",
33
- "licenses": [
34
- {
35
- "type": "MIT",
36
- "url": "https://github.com/dxdc/node-red-contrib-join-wait/blob/master/LICENSE"
2
+ "name": "node-red-contrib-join-wait",
3
+ "version": "0.6.0",
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
+ "author": "Daniel Caspi <dan@element26.net>",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/dxdc/node-red-contrib-join-wait#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/dxdc/node-red-contrib-join-wait.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/dxdc/node-red-contrib-join-wait/issues"
14
+ },
15
+ "funding": [
16
+ {
17
+ "type": "individual",
18
+ "url": "https://paypal.me/ddcaspi"
19
+ },
20
+ {
21
+ "type": "github",
22
+ "url": "https://github.com/sponsors/dxdc"
23
+ }
24
+ ],
25
+ "main": "join-wait.js",
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "files": [
30
+ "join-wait.js",
31
+ "join-wait.html",
32
+ "lib/",
33
+ "icons/",
34
+ "examples/",
35
+ "LICENSE",
36
+ "README.md",
37
+ "CHANGELOG.md"
38
+ ],
39
+ "keywords": [
40
+ "node-red",
41
+ "node-red-contrib",
42
+ "join",
43
+ "merge",
44
+ "wait",
45
+ "timeout",
46
+ "parallel",
47
+ "synchronize",
48
+ "coordinate",
49
+ "correlate",
50
+ "correlation",
51
+ "barrier",
52
+ "gate",
53
+ "aggregate",
54
+ "sequence",
55
+ "debounce",
56
+ "regex",
57
+ "pattern",
58
+ "events",
59
+ "iot",
60
+ "home-automation"
61
+ ],
62
+ "node-red": {
63
+ "version": ">=3.0.0",
64
+ "nodes": {
65
+ "join-wait": "join-wait.js"
66
+ },
67
+ "examples": "examples"
68
+ },
69
+ "dependencies": {
70
+ "jsonata": "^2.0.5"
71
+ },
72
+ "devDependencies": {
73
+ "@eslint/js": "^9.10.0",
74
+ "@types/node-red": "^1.3.5",
75
+ "c8": "^10.1.2",
76
+ "cspell": "^8.13.3",
77
+ "eslint": "^9.10.0",
78
+ "eslint-plugin-mocha": "^10.5.0",
79
+ "globals": "^15.9.0",
80
+ "mocha": "^10.7.3",
81
+ "node-red": "^4.0.3",
82
+ "node-red-node-test-helper": "^0.3.6",
83
+ "prettier": "^3.3.3",
84
+ "should": "^13.2.3"
85
+ },
86
+ "scripts": {
87
+ "test": "mocha \"test/**/*_spec.js\"",
88
+ "coverage": "c8 --reporter=text-summary --reporter=lcov --check-coverage --lines 95 --statements 95 --functions 95 --branches 90 --exclude 'test/**' --exclude 'coverage/**' npm test",
89
+ "lint": "eslint .",
90
+ "format": "prettier --write \"{.,test,lib}/*.{js,json}\" *.html *.md",
91
+ "format:check": "prettier --check \"{.,test,lib}/*.{js,json}\" *.html *.md",
92
+ "spellcheck": "cspell \"**/*.md\""
37
93
  }
38
- ],
39
- "main": "join-wait.js",
40
- "maintainers": [
41
- "Daniel Caspi <dan@element26.net>"
42
- ],
43
- "name": "node-red-contrib-join-wait",
44
- "node-red": {
45
- "nodes": {
46
- "join-wait": "join-wait.js"
47
- }
48
- },
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/dxdc/node-red-contrib-join-wait.git"
52
- },
53
- "scripts": {
54
- "coverage": "nyc npm t",
55
- "format": "prettier --write {.,test}/*.js *.html *.md",
56
- "lint": "eslint {.,test}/*.js",
57
- "spellcheck": "mdspell -r -n *.md",
58
- "test": "mocha \"test/**/*_spec.js\""
59
- },
60
- "version": "0.5.3"
61
94
  }
package/.eslintrc.js DELETED
@@ -1,24 +0,0 @@
1
- module.exports = {
2
- env: {
3
- es6: true,
4
- node: true,
5
- mocha: true,
6
- },
7
- extends: ['eslint:recommended', 'plugin:mocha/recommended'],
8
- globals: {
9
- Atomics: 'readonly',
10
- SharedArrayBuffer: 'readonly',
11
- },
12
- parserOptions: {
13
- ecmaVersion: 2018,
14
- sourceType: 'module',
15
- },
16
- plugins: ['mocha'],
17
- rules: {
18
- indent: ['error', 4, { "SwitchCase": 1 }],
19
- 'linebreak-style': ['error', 'unix'],
20
- quotes: ['error', 'single'],
21
- semi: ['error', 'always'],
22
- 'no-console': 'off',
23
- },
24
- };
@@ -1,12 +0,0 @@
1
- # These are supported funding model platforms
2
-
3
- github: dxdc
4
- patreon: # Replace with a single Patreon username
5
- open_collective: # Replace with a single Open Collective username
6
- ko_fi: # Replace with a single Ko-fi username
7
- tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
- community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
- liberapay: # Replace with a single Liberapay username
10
- issuehunt: # Replace with a single IssueHunt username
11
- otechie: # Replace with a single Otechie username
12
- custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
package/.nycrc.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "reporter": [
3
- "lcov",
4
- "text-summary"
5
- ],
6
- "lines": 100,
7
- "statements": 100,
8
- "functions": 100,
9
- "branches": 100,
10
- "check-coverage": true
11
- }
package/.prettierrc DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "printWidth": 120,
3
- "tabWidth": 4,
4
- "trailingComma": "all",
5
- "singleQuote": true
6
- }
package/.spelling DELETED
@@ -1,20 +0,0 @@
1
- # usernames
2
- dxdc
3
- mauriciom75
4
-
5
- # text
6
- 1A
7
- 1B
8
- 2A
9
- 2B
10
- 3A
11
- behavior
12
- my_path1_test
13
- path_1
14
- path_2
15
- path1
16
- path2
17
- path3
18
- regex
19
- timestamp
20
- Venmo
package/.travis.yml DELETED
@@ -1,20 +0,0 @@
1
- language: node_js
2
- node_js:
3
- - 10
4
- - 12
5
- - 14
6
- - node
7
-
8
- install: yarn
9
- cache: yarn
10
-
11
- script:
12
- - yarn lint
13
- - yarn test
14
-
15
- deploy:
16
- provider: npm
17
- email: "$NPM_EMAIL"
18
- api_key: "$NPM_API_KEY"
19
- on:
20
- tags: true
package/docs/_config.yml DELETED
@@ -1 +0,0 @@
1
- theme: jekyll-theme-minimal
package/docs/example1.png DELETED
Binary file
package/docs/example2.png DELETED
Binary file
package/docs/example3.png DELETED
Binary file
package/docs/example4.png DELETED
Binary file
package/docs/example5.png DELETED
Binary file
package/docs/example6.png DELETED
Binary file
package/docs/venmo.png DELETED
Binary file
package/test/flows.js DELETED
@@ -1,33 +0,0 @@
1
- const Flows = {
2
- getDefault: function (options) {
3
- let defaultFlow = [
4
- {
5
- id: 'n1',
6
- type: 'join-wait',
7
- paths: '["path_1", "path_2", "path_3"]',
8
- pathsToExpire: '',
9
- useRegex: false,
10
- warnUnmatched: true,
11
- pathTopic: 'paths',
12
- pathTopicType: 'msg',
13
- correlationTopic: '',
14
- correlationTopicType: 'undefined',
15
- timeout: '1',
16
- timeoutUnits: '1000',
17
- exactOrder: 'false',
18
- firstMsg: 'true',
19
- mapPayload: 'true',
20
- disableComplete: false,
21
- persistOnRestart: false,
22
- wires: [['n2'], ['n3']],
23
- },
24
- { id: 'n2', type: 'helper' },
25
- { id: 'n3', type: 'helper' },
26
- ];
27
-
28
- defaultFlow[0] = Object.assign(defaultFlow[0], options);
29
- return defaultFlow;
30
- },
31
- };
32
-
33
- module.exports = Flows;