node-red-contrib-tdn-eventsub 0.2.2 → 0.2.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/README.md +120 -69
- package/examples/rest-basic.json +387 -72
- package/lib/client.js +177 -76
- package/lib/outputs.js +2 -0
- package/lib/subscription.js +117 -0
- package/nodes/rest-events.html +133 -0
- package/nodes/rest-events.js +172 -0
- package/nodes/rest-item-status.html +15 -7
- package/nodes/rest-item-status.js +53 -96
- package/nodes/rest-lists.html +379 -60
- package/nodes/rest-lists.js +214 -75
- package/nodes/rest-server.html +36 -9
- package/nodes/rest-server.js +108 -53
- package/package.json +3 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { RestClient, RestError } = require('../lib/client');
|
|
3
|
+
const { OUT_RESPONSE, send, setStatus, reportError, storeName } = require('../lib/outputs');
|
|
4
|
+
const { configureCreds, resolveCreds, stripCreds } = require('../lib/credentials');
|
|
5
|
+
const { runSubscription } = require('../lib/subscription');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Event updates subscription.
|
|
9
|
+
*
|
|
10
|
+
* GET <base>/events/updates?<filters> (long poll: returns new events, or empty after ~50 s)
|
|
11
|
+
* loop: follow the returned updates/next link, forever
|
|
12
|
+
*
|
|
13
|
+
* Filters (node "Filter" setting, msg.query at start): e.g. group=23&type=20001&source=508
|
|
14
|
+
* Failure handling (back-off, hard stop on key/cert/licence errors): lib/subscription.js.
|
|
15
|
+
* Any message starts (or restarts) it; msg.payload "stop", msg.action "stop" or msg.stop = true ends it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const PATH = '/events/updates';
|
|
19
|
+
const MAX_DELAY_MS = 20000;
|
|
20
|
+
|
|
21
|
+
function isStop(msg) {
|
|
22
|
+
const word = (v) => typeof v === 'string' && v.trim().toLowerCase() === 'stop';
|
|
23
|
+
return msg.stop === true || word(msg.payload) || word(msg.action);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The long-poll link to follow: updates.href (preferred) or next.href. */
|
|
27
|
+
function nextHref(res) {
|
|
28
|
+
const b = res.body || {};
|
|
29
|
+
return (b.updates && b.updates.href) || (b.next && b.next.href) || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function eventsOf(res) {
|
|
33
|
+
return res.body && Array.isArray(res.body.events) ? res.body.events : [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = function (RED) {
|
|
37
|
+
function RestEventsNode(config) {
|
|
38
|
+
RED.nodes.createNode(this, config);
|
|
39
|
+
const node = this;
|
|
40
|
+
node.name = config.name;
|
|
41
|
+
node.server = RED.nodes.getNode(config.server);
|
|
42
|
+
node.query = (config.query || '').trim();
|
|
43
|
+
node.pollDelay = Math.min(Math.max(parseInt(config.pollDelay, 10) || 0, 0), MAX_DELAY_MS);
|
|
44
|
+
node.retryDelay = Math.max(parseInt(config.retryDelay, 10) || 5000, 100);
|
|
45
|
+
node.maxRetryDelay = Math.max(parseInt(config.maxRetryDelay, 10) || 60000, node.retryDelay);
|
|
46
|
+
node.emitTimeouts = config.emitTimeouts === true;
|
|
47
|
+
node.contextKey = (config.contextKey || '').trim();
|
|
48
|
+
node.contextStore = config.contextStore || '';
|
|
49
|
+
node.keepEvents = Math.max(parseInt(config.keepEvents, 10) || 100, 1);
|
|
50
|
+
node.autoStart = config.autoStart === true;
|
|
51
|
+
configureCreds(node, config);
|
|
52
|
+
|
|
53
|
+
let run = null; // { ctrl }
|
|
54
|
+
|
|
55
|
+
function storeEvents(events) {
|
|
56
|
+
if (!node.contextKey || !events.length) return;
|
|
57
|
+
const g = node.context().global;
|
|
58
|
+
const store = storeName(node.contextStore);
|
|
59
|
+
const list = (g.get(node.contextKey, store) || []).concat(events);
|
|
60
|
+
g.set(node.contextKey, list.slice(-node.keepEvents), store); // newest last, capped
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loop(ctrl, cred, query) {
|
|
64
|
+
let client;
|
|
65
|
+
try {
|
|
66
|
+
client = node.server.getClient(cred, node);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
reportError(node, e, { stopped: true, category: 'fatal' });
|
|
69
|
+
node.status({ fill: 'red', shape: 'ring', text: 'stopped: ' + (e.code || 'error') });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const startHref = RestClient.addQuery(client.url(PATH), query);
|
|
73
|
+
let seq = 0;
|
|
74
|
+
const emit = (res, phase) => {
|
|
75
|
+
const events = eventsOf(res);
|
|
76
|
+
storeEvents(events);
|
|
77
|
+
send(node, OUT_RESPONSE, {
|
|
78
|
+
topic: 'rest/events/updates',
|
|
79
|
+
payload: res.body, // full server response
|
|
80
|
+
events,
|
|
81
|
+
phase, // 'events' | 'timeout'
|
|
82
|
+
seq: ++seq,
|
|
83
|
+
query,
|
|
84
|
+
statusCode: res.statusCode,
|
|
85
|
+
url: res.url
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
await runSubscription({
|
|
89
|
+
node,
|
|
90
|
+
client,
|
|
91
|
+
signal: ctrl.signal,
|
|
92
|
+
label: 'events',
|
|
93
|
+
extra: { query },
|
|
94
|
+
startHref,
|
|
95
|
+
nextHref,
|
|
96
|
+
hasData: (res) => eventsOf(res).length > 0,
|
|
97
|
+
onStart: () => {},
|
|
98
|
+
onData: (res) => emit(res, 'events'),
|
|
99
|
+
onEmpty: (res) => { if (node.emitTimeouts) emit(res, 'timeout'); },
|
|
100
|
+
dataText: (res) => `${eventsOf(res).length} event(s) @ ${new Date().toLocaleTimeString()}`
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stop(reason) {
|
|
105
|
+
if (run) {
|
|
106
|
+
run.ctrl.abort();
|
|
107
|
+
run = null;
|
|
108
|
+
setStatus(node, 'stopped', reason || 'stopped');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function start(cred, msgQuery) {
|
|
113
|
+
const query = Object.assign(RestClient.parseQueryString(node.query),
|
|
114
|
+
msgQuery ? (typeof msgQuery === 'string' ? RestClient.parseQueryString(msgQuery) : msgQuery) : {});
|
|
115
|
+
if (run) stop('replaced by new subscription');
|
|
116
|
+
const ctrl = new AbortController();
|
|
117
|
+
run = { ctrl };
|
|
118
|
+
loop(ctrl, cred, query).catch((e) => reportError(node, e)).finally(() => {
|
|
119
|
+
if (run && run.ctrl === ctrl) run = null;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!node.server) node.status({ fill: 'red', shape: 'dot', text: 'no server config' });
|
|
124
|
+
else node.status({ fill: 'grey', shape: 'ring', text: 'idle' });
|
|
125
|
+
|
|
126
|
+
node.on('input', async function (msg, sendFn, done) {
|
|
127
|
+
done = done || function () {};
|
|
128
|
+
if (isStop(msg)) {
|
|
129
|
+
stripCreds(RED, node, msg);
|
|
130
|
+
if (run) stop('stopped by input');
|
|
131
|
+
else setStatus(node, 'stopped', 'not running');
|
|
132
|
+
return done();
|
|
133
|
+
}
|
|
134
|
+
let cred;
|
|
135
|
+
try {
|
|
136
|
+
cred = await resolveCreds(RED, node, msg);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
reportError(node, e);
|
|
139
|
+
return done();
|
|
140
|
+
} finally {
|
|
141
|
+
stripCreds(RED, node, msg);
|
|
142
|
+
}
|
|
143
|
+
if (!node.server) {
|
|
144
|
+
reportError(node, new RestError('No REST server configured', { code: 'ENOSERVER' }));
|
|
145
|
+
return done();
|
|
146
|
+
}
|
|
147
|
+
start(cred, msg.query);
|
|
148
|
+
done(); // the trigger message is handled; the subscription carries on by itself
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
let autoTimer = null;
|
|
152
|
+
if (node.autoStart && node.server) {
|
|
153
|
+
// credentials from flow/global/env only (there is no msg yet)
|
|
154
|
+
autoTimer = setTimeout(() => {
|
|
155
|
+
autoTimer = null;
|
|
156
|
+
resolveCreds(RED, node, {}).then((cred) => start(cred)).catch((e) => reportError(node, e));
|
|
157
|
+
}, 500);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
node.on('close', function (removed, done) {
|
|
161
|
+
if (typeof removed === 'function') { done = removed; }
|
|
162
|
+
if (autoTimer) clearTimeout(autoTimer);
|
|
163
|
+
if (run) { run.ctrl.abort(); run = null; }
|
|
164
|
+
node.status({});
|
|
165
|
+
done();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
RED.nodes.registerType('tdn-rest-events', RestEventsNode);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
module.exports.nextHref = nextHref;
|
|
@@ -76,22 +76,21 @@
|
|
|
76
76
|
<script type="text/html" data-help-name="tdn-rest-item-status">
|
|
77
77
|
<p>Subscribes to status changes for a set of REST Server items and keeps listening indefinitely.
|
|
78
78
|
Needs REST Server v8.30+ and the RESTStatus licence.</p>
|
|
79
|
-
<p>Sequence: <code>POST
|
|
79
|
+
<p>Sequence: <code>POST <server base>/items/updates</code> (e.g. <code>https://192.168.0.20:8904/api/items/updates</code>) with <code>{"itemIds":[...]}</code> (returns current state),
|
|
80
80
|
then an update-wait <code>GET next.href</code>. When an update arrives it is output and a new update-wait is sent.
|
|
81
81
|
When an update-wait times out (~50 s, empty <code>updates</code>) another one is sent.
|
|
82
|
-
A <code>404</code> means the server dropped the subscription (more than 30 s between calls, or server restart), so it re-subscribes
|
|
83
|
-
Any other error is reported, then retried with exponential back-off.</p>
|
|
82
|
+
A <code>404</code> on a followed link means the server dropped the subscription (more than 30 s between calls, or server restart), so it re-subscribes.</p>
|
|
84
83
|
<h3>Input</h3>
|
|
85
84
|
<dl class="message-properties">
|
|
86
85
|
<dt>payload <span class="property-type">string | number | array | object</span></dt>
|
|
87
86
|
<dd>The source id(s): <code>"508"</code>, <code>"508,526"</code>, <code>["508","526"]</code>,
|
|
88
87
|
<code>{"itemIds":["508","526"]}</code>, or an array of item objects with an <code>id</code> (e.g. straight from <i>get items</i> results).
|
|
89
|
-
<code>msg.itemIds</code>
|
|
88
|
+
<code>msg.itemIds</code>, then <code>msg.id</code>, take precedence if present.</dd>
|
|
90
89
|
<dt class="optional">credentials <span class="property-type">string | object</span></dt><dd>Used only when the server config has no API key stored.
|
|
91
90
|
Read from the <i>Credentials</i> property (default <code>msg.credentials</code>; msg, flow, global or env): a bare key string, or
|
|
92
91
|
<code>{"apiKey":"...", "authType":"authorization"|"header", "authScheme":"...", "headerName":"X-API-Key"}</code>.
|
|
93
92
|
Missing fields fall back to the server config. A msg-borne credentials property is removed from the trigger msg.</dd>
|
|
94
|
-
<dd>Credentials are read once per trigger and kept for that subscription. A new trigger replaces the running subscription. <code>payload</code> = <code>"stop"</code> or <code>msg.stop = true</code> stops it.</dd>
|
|
93
|
+
<dd>Credentials are read once per trigger and kept for that subscription. A new trigger replaces the running subscription. <code>payload</code> = <code>"stop"</code>, <code>msg.action = "stop"</code> or <code>msg.stop = true</code> stops it.</dd>
|
|
95
94
|
</dl>
|
|
96
95
|
<h3>Outputs</h3>
|
|
97
96
|
<ol class="node-ports">
|
|
@@ -103,9 +102,18 @@
|
|
|
103
102
|
<dt>itemIds / seq / statusCode / url</dt><dd></dd>
|
|
104
103
|
</dl>
|
|
105
104
|
</li>
|
|
106
|
-
<li>
|
|
107
|
-
|
|
105
|
+
<li>Status – successful server responses only: <code>subscribed</code> (POST ok), <code>update</code> (poll with updates); plus <code>stopped</code> on a stop input.
|
|
106
|
+
Empty long polls (~every 50 s) show on the node badge only.</li>
|
|
107
|
+
<li>Error – <code>payload.message</code>, <code>statusCode</code>, <code>body</code> (on 9.60+ a 403 lists the ids you may monitor), <code>category</code>, <code>stopped</code>, <code>attempt</code>, <code>retryInMs</code>.</li>
|
|
108
108
|
</ol>
|
|
109
|
+
<h3>Failures</h3>
|
|
110
|
+
<p>Every non-success goes to the error output (never the status output), with <code>category</code>:</p>
|
|
111
|
+
<ul>
|
|
112
|
+
<li><b>fatal</b> – 401 (API key), 403 (licence / privilege), 404 on the subscribe endpoint, other 4xx, certificate or TLS failure:
|
|
113
|
+
polling <b>stops</b> (<code>stopped: true</code>) until a new trigger or redeploy.</li>
|
|
114
|
+
<li><b>expired</b> – 404 on a followed link (subscription dropped by the server): re-subscribes immediately.</li>
|
|
115
|
+
<li><b>transient</b> – 5xx, 408, 429, timeout, network: back-off <i>retry delay</i> × 2<sup>n</sup> up to <i>max</i>, then re-subscribes. Runs until stopped.</li>
|
|
116
|
+
</ul>
|
|
109
117
|
<h3>Global context (optional)</h3>
|
|
110
118
|
<p>If a key is set, <code>global.<key></code> is kept as a map of item id → latest status (with an <code>updated</code> time).</p>
|
|
111
119
|
<h3>Notes</h3>
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const { OUT_RESPONSE, send, setStatus, reportError, storeName, nowIso } = require('../lib/outputs');
|
|
3
3
|
const { configureCreds, resolveCreds, stripCreds } = require('../lib/credentials');
|
|
4
|
+
const { runSubscription } = require('../lib/subscription');
|
|
5
|
+
const { RestError } = require('../lib/client');
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Item status subscription (REST Server v8.30+, RESTStatus licence).
|
|
7
9
|
*
|
|
8
|
-
* POST
|
|
9
|
-
* loop: GET next.href (long poll, ~50 s server timeout)
|
|
10
|
-
* -> updates (emit) or empty (timeout: just wait again)
|
|
11
|
-
* -> always follow the new next.href
|
|
12
|
-
* 404 -> subscription dropped (>30 s gap or server restart): POST again
|
|
13
|
-
* other errors -> back off and re-subscribe, forever, until stopped
|
|
10
|
+
* POST <base>/items/updates {itemIds:[...]} -> current status + next.href
|
|
11
|
+
* loop: GET next.href (long poll, ~50 s server timeout) -> updates or empty, follow next.href
|
|
14
12
|
*
|
|
13
|
+
* Failure handling (back-off, hard stop on key/cert/licence errors): lib/subscription.js.
|
|
15
14
|
* One trigger starts it; it then runs indefinitely. A new trigger with ids replaces
|
|
16
|
-
* the subscription; msg.payload "stop"
|
|
15
|
+
* the subscription; msg.payload "stop", msg.action "stop" or msg.stop = true ends it.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
|
-
const
|
|
20
|
-
const
|
|
18
|
+
const PATH = '/items/updates';
|
|
19
|
+
const MAX_GAP_MS = 20000; // the server drops a subscription if next.href isn't fetched within 30 s
|
|
21
20
|
|
|
22
21
|
function parseIds(msg) {
|
|
23
|
-
let v = msg.itemIds !== undefined ? msg.itemIds : msg.payload;
|
|
22
|
+
let v = msg.itemIds !== undefined ? msg.itemIds : (msg.id !== undefined ? msg.id : msg.payload);
|
|
24
23
|
if (v && typeof v === 'object' && !Array.isArray(v) && v.itemIds !== undefined) v = v.itemIds;
|
|
25
24
|
if (typeof v === 'number') v = [v];
|
|
26
25
|
if (typeof v === 'string') v = v.split(/[\s,;]+/);
|
|
@@ -36,7 +35,8 @@ function parseIds(msg) {
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
function isStop(msg) {
|
|
39
|
-
|
|
38
|
+
const word = (v) => typeof v === 'string' && v.trim().toLowerCase() === 'stop';
|
|
39
|
+
return msg.stop === true || word(msg.payload) || word(msg.action);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
module.exports = function (RED) {
|
|
@@ -46,7 +46,7 @@ module.exports = function (RED) {
|
|
|
46
46
|
node.name = config.name;
|
|
47
47
|
node.server = RED.nodes.getNode(config.server);
|
|
48
48
|
node.pollDelay = Math.min(Math.max(parseInt(config.pollDelay, 10) || 0, 0), MAX_GAP_MS);
|
|
49
|
-
node.retryDelay = Math.max(parseInt(config.retryDelay, 10) || 5000,
|
|
49
|
+
node.retryDelay = Math.max(parseInt(config.retryDelay, 10) || 5000, 100);
|
|
50
50
|
node.maxRetryDelay = Math.max(parseInt(config.maxRetryDelay, 10) || 60000, node.retryDelay);
|
|
51
51
|
node.emitTimeouts = config.emitTimeouts === true;
|
|
52
52
|
node.contextKey = (config.contextKey || '').trim();
|
|
@@ -55,15 +55,6 @@ module.exports = function (RED) {
|
|
|
55
55
|
|
|
56
56
|
let run = null; // { ctrl, ids }
|
|
57
57
|
|
|
58
|
-
function sleep(ms, signal) {
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
if (signal.aborted) return resolve();
|
|
61
|
-
const t = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
62
|
-
const onAbort = () => { clearTimeout(t); resolve(); };
|
|
63
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
function storeUpdates(updates) {
|
|
68
59
|
if (!node.contextKey || !Array.isArray(updates) || !updates.length) return;
|
|
69
60
|
const g = node.context().global;
|
|
@@ -74,83 +65,49 @@ module.exports = function (RED) {
|
|
|
74
65
|
g.set(node.contextKey, map, store);
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
68
|
+
function emitter(ids) {
|
|
69
|
+
let seq = 0;
|
|
70
|
+
return (res, phase) => {
|
|
71
|
+
const body = res.body || {};
|
|
72
|
+
storeUpdates(body.updates);
|
|
73
|
+
send(node, OUT_RESPONSE, {
|
|
74
|
+
topic: 'rest/items/updates',
|
|
75
|
+
payload: body, // full server response
|
|
76
|
+
updates: Array.isArray(body.updates) ? body.updates : [],
|
|
77
|
+
phase, // 'subscribe' | 'update' | 'timeout'
|
|
78
|
+
seq: ++seq,
|
|
79
|
+
itemIds: ids,
|
|
80
|
+
statusCode: res.statusCode,
|
|
81
|
+
url: res.url
|
|
82
|
+
});
|
|
83
|
+
};
|
|
90
84
|
}
|
|
91
85
|
|
|
92
86
|
async function loop(ids, ctrl, cred) {
|
|
93
|
-
const signal = ctrl.signal;
|
|
94
87
|
let client;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
while (!signal.aborted) {
|
|
102
|
-
try {
|
|
103
|
-
if (!next) {
|
|
104
|
-
setStatus(node, 'subscribing', `subscribing ${ids.length} item(s)`, { itemIds: ids });
|
|
105
|
-
const href = await client.feature('items.updates', { signal });
|
|
106
|
-
const res = await client.request('POST', href, { itemIds: ids }, { signal });
|
|
107
|
-
if (signal.aborted) break;
|
|
108
|
-
emit(res, 'subscribe', ids, ++seq);
|
|
109
|
-
next = res.body && res.body.next && res.body.next.href;
|
|
110
|
-
if (!next) throw new Error('Subscription response had no next.href');
|
|
111
|
-
setStatus(node, 'subscribed', `subscribed ${ids.length} item(s)`, { itemIds: ids });
|
|
112
|
-
} else {
|
|
113
|
-
if (node.pollDelay) await sleep(node.pollDelay, signal);
|
|
114
|
-
if (signal.aborted) break;
|
|
115
|
-
// badge only - every long poll would otherwise flood the status output
|
|
116
|
-
node.status({ fill: 'green', shape: 'ring', text: `waiting (${ids.length} item(s))` });
|
|
117
|
-
const res = await client.request('GET', next, null, { signal, timeoutMs: LONG_POLL_MS });
|
|
118
|
-
if (signal.aborted) break;
|
|
119
|
-
const updates = res.body && Array.isArray(res.body.updates) ? res.body.updates : [];
|
|
120
|
-
if (updates.length) {
|
|
121
|
-
emit(res, 'update', ids, ++seq);
|
|
122
|
-
setStatus(node, 'update', `${updates.length} update(s) @ ${new Date().toLocaleTimeString()}`, {
|
|
123
|
-
count: updates.length,
|
|
124
|
-
ids: updates.map((u) => u.id)
|
|
125
|
-
});
|
|
126
|
-
} else {
|
|
127
|
-
// update-wait timed out with nothing new: send another update-wait.
|
|
128
|
-
// Badge only - this happens every ~50 s and would flood the status output.
|
|
129
|
-
if (node.emitTimeouts) emit(res, 'timeout', ids, ++seq);
|
|
130
|
-
node.status({ fill: 'green', shape: 'ring', text: `wait timed out, re-waiting (${ids.length} item(s))` });
|
|
131
|
-
}
|
|
132
|
-
next = res.body && res.body.next && res.body.next.href;
|
|
133
|
-
if (!next) {
|
|
134
|
-
setStatus(node, 'resubscribing', 'no next link, re-subscribing');
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
failures = 0;
|
|
138
|
-
} catch (err) {
|
|
139
|
-
if (signal.aborted || err.aborted) break;
|
|
140
|
-
next = null;
|
|
141
|
-
if (err.statusCode === 404 && failures === 0) {
|
|
142
|
-
// subscription expired (gap > 30 s or server restart) - re-POST straight away once
|
|
143
|
-
setStatus(node, 'resubscribing', 'subscription expired (404), re-subscribing');
|
|
144
|
-
failures++;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
failures++;
|
|
148
|
-
const delay = Math.min(node.retryDelay * Math.pow(2, Math.min(failures - 1, 10)), node.maxRetryDelay);
|
|
149
|
-
reportError(node, err, { itemIds: ids, attempt: failures, retryInMs: delay });
|
|
150
|
-
setStatus(node, 'retrying', `retry ${failures} in ${Math.round(delay / 1000)} s`, { attempt: failures, retryInMs: delay, error: err.message });
|
|
151
|
-
await sleep(delay, signal);
|
|
152
|
-
}
|
|
88
|
+
try {
|
|
89
|
+
client = node.server.getClient(cred, node);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
reportError(node, e, { itemIds: ids, stopped: true, category: 'fatal' });
|
|
92
|
+
node.status({ fill: 'red', shape: 'ring', text: 'stopped: ' + (e.code || 'error') });
|
|
93
|
+
return;
|
|
153
94
|
}
|
|
95
|
+
const emit = emitter(ids);
|
|
96
|
+
const updatesOf = (res) => (res.body && Array.isArray(res.body.updates) ? res.body.updates : []);
|
|
97
|
+
await runSubscription({
|
|
98
|
+
node,
|
|
99
|
+
client,
|
|
100
|
+
signal: ctrl.signal,
|
|
101
|
+
label: `${ids.length} item(s)`,
|
|
102
|
+
extra: { itemIds: ids },
|
|
103
|
+
start: () => client.request('POST', client.url(PATH), { itemIds: ids }, { signal: ctrl.signal }),
|
|
104
|
+
nextHref: (res) => (res.body && res.body.next && res.body.next.href) || null,
|
|
105
|
+
hasData: (res) => updatesOf(res).length > 0,
|
|
106
|
+
onStart: (res) => emit(res, 'subscribe'),
|
|
107
|
+
onData: (res) => emit(res, 'update'),
|
|
108
|
+
onEmpty: (res) => { if (node.emitTimeouts) emit(res, 'timeout'); },
|
|
109
|
+
dataText: (res) => `${updatesOf(res).length} update(s) @ ${new Date().toLocaleTimeString()}`
|
|
110
|
+
});
|
|
154
111
|
}
|
|
155
112
|
|
|
156
113
|
function stop(reason) {
|
|
@@ -161,7 +118,7 @@ module.exports = function (RED) {
|
|
|
161
118
|
}
|
|
162
119
|
}
|
|
163
120
|
|
|
164
|
-
if (!node.server)
|
|
121
|
+
if (!node.server) node.status({ fill: 'red', shape: 'dot', text: 'no server config' });
|
|
165
122
|
else node.status({ fill: 'grey', shape: 'ring', text: 'idle' });
|
|
166
123
|
|
|
167
124
|
node.on('input', async function (msg, sendFn, done) {
|
|
@@ -182,12 +139,12 @@ module.exports = function (RED) {
|
|
|
182
139
|
stripCreds(RED, node, msg);
|
|
183
140
|
}
|
|
184
141
|
if (!node.server) {
|
|
185
|
-
reportError(node, new
|
|
142
|
+
reportError(node, new RestError('No REST server configured', { code: 'ENOSERVER' }));
|
|
186
143
|
return done();
|
|
187
144
|
}
|
|
188
145
|
const ids = parseIds(msg);
|
|
189
146
|
if (!ids.length) {
|
|
190
|
-
reportError(node, new
|
|
147
|
+
reportError(node, new RestError('No item ids in msg.payload / msg.itemIds / msg.id (string "508,526", array, or {itemIds:[...]})', { code: 'EBADINPUT' }));
|
|
191
148
|
return done();
|
|
192
149
|
}
|
|
193
150
|
if (run) stop('replaced by new subscription');
|