node-red-contrib-uos-nats 1.2.0 → 1.2.4
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/nodes/datahub-input.js +135 -15
- package/nodes/uos-config.js +25 -0
- package/package.json +1 -1
package/nodes/datahub-input.js
CHANGED
|
@@ -150,26 +150,108 @@ module.exports = function (RED) {
|
|
|
150
150
|
nc = await connection.acquire();
|
|
151
151
|
this.status({ fill: 'green', shape: 'dot', text: 'connected' });
|
|
152
152
|
|
|
153
|
-
// Retry Definition Fetch via NATS if Map is empty
|
|
154
|
-
|
|
153
|
+
// Retry Definition Fetch via NATS if Map is empty OR if we have Heuristic IDs (missingId)
|
|
154
|
+
// Heuristic IDs (ID=Index) are dangerous because they might not match the real NATS IDs (e.g. 291 vs 5)
|
|
155
|
+
const hasMissingIds = Array.from(defMap.values()).some(d => d.missingId);
|
|
156
|
+
|
|
157
|
+
if ((defMap.size === 0 && this.manualDefs.length === 0) || hasMissingIds) {
|
|
155
158
|
try {
|
|
159
|
+
this.warn(hasMissingIds
|
|
160
|
+
? `Loaded variables have unresolved IDs (Heuristic). Attempting NATS Discovery to resolve real IDs for ${this.providerId}...`
|
|
161
|
+
: `Attempting NATS Discovery (Direct) for ${this.providerId}...`
|
|
162
|
+
);
|
|
163
|
+
|
|
156
164
|
// Strategy 1: Direct Provider Query (Standard for many providers)
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
const requestOptions = { timeout: 2000 };
|
|
166
|
+
// Reuse serialRequest if available to avoid blocking connection?
|
|
167
|
+
// Discovery is one-off, nc.request is fine, but safer to use serial if we updated input.js fully.
|
|
168
|
+
// Start uses 'nc' directly currently. That's fine for now as it's sequential in 'start'.
|
|
169
|
+
|
|
170
|
+
const defMsg = await nc.request(subjects.readProviderDefinitionQuery(this.providerId), payloads.buildReadProviderDefinitionQuery(), requestOptions);
|
|
159
171
|
const defs = payloads.decodeProviderDefinition(defMsg.data);
|
|
160
|
-
|
|
161
|
-
defs
|
|
172
|
+
|
|
173
|
+
if (defs && defs.variables.length > 0) {
|
|
174
|
+
this.warn(`NATS Discovery Successful: Received ${defs.variables.length} definitions with real IDs.`);
|
|
175
|
+
// Overwrite/Update defMap
|
|
176
|
+
// Logic: Match by KEY. DataHub providers should have unique keys.
|
|
177
|
+
// If we have existing "fake" ID 5 for "temp", and NATS says "temp" is ID 291.
|
|
178
|
+
// We need to update defMap to use 291.
|
|
179
|
+
|
|
180
|
+
// Clear Heuristic entries if we trust NATS fully?
|
|
181
|
+
// Or just merge?
|
|
182
|
+
// Safer: Create a lookup from NATS.
|
|
183
|
+
const realMap = new Map();
|
|
184
|
+
defs.variables.forEach(d => realMap.set(d.key, d));
|
|
185
|
+
|
|
186
|
+
// Update existing defMap
|
|
187
|
+
// If we had a heuristic entry, replace it.
|
|
188
|
+
// We rebuild defMap based on NATS mostly, but keep manual fallback?
|
|
189
|
+
|
|
190
|
+
// Let's iterate NATS defs and Populating defMap.
|
|
191
|
+
// Note: NATS Defs don't have 'missingId'.
|
|
192
|
+
defs.variables.forEach(d => {
|
|
193
|
+
// If we overwrite, we lose manual metadata (if any)?
|
|
194
|
+
// REST might have had better metadata? Usually NATS is source of truth for IDs.
|
|
195
|
+
defMap.set(d.id, d);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Use Key Matching to remove old Heuristic entries?
|
|
199
|
+
// Heuristic entries are stored by Key (via fetchProviderVariables logic? No, by ID).
|
|
200
|
+
// We need to clean up the Fake IDs (0..N) if they don't map to real IDs.
|
|
201
|
+
// Actually, if we just add real IDs, we have duplicates?
|
|
202
|
+
// Map is Key=ID.
|
|
203
|
+
// Fake ID 5: { key: 'voltage' }
|
|
204
|
+
// Real ID 291: { key: 'voltage' }
|
|
205
|
+
// If user selected 'voltage', filtering uses KEYS (processStates line 104).
|
|
206
|
+
// Resolution (line 195) iterates values and matches Key.
|
|
207
|
+
// It will find BOTH 5 and 291.
|
|
208
|
+
// targetIds will get [5, 291].
|
|
209
|
+
// DataHub gets request [5, 291].
|
|
210
|
+
// 5 is invalid -> ignored.
|
|
211
|
+
// 291 is valid -> returns value.
|
|
212
|
+
// Result: It works! (Partially, effectively).
|
|
213
|
+
|
|
214
|
+
// But cleaner to remove heuristic ones.
|
|
215
|
+
for (const [id, def] of defMap.entries()) {
|
|
216
|
+
if (def.missingId) {
|
|
217
|
+
const real = realMap.get(def.key);
|
|
218
|
+
if (real && real.id !== id) {
|
|
219
|
+
defMap.delete(id); // Remove fake ID
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.warn(`IDs resolved via NATS. Mapped ${defs.variables.length} real IDs.`);
|
|
224
|
+
}
|
|
225
|
+
|
|
162
226
|
} catch (firstErr) {
|
|
163
|
-
// Strategy 2: Registry Query
|
|
227
|
+
// Strategy 2: Registry Query
|
|
164
228
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
229
|
+
if (!hasMissingIds) { // Only log if we were truly empty
|
|
230
|
+
this.warn(`NATS Direct failed (${firstErr.message}), trying Registry Discovery...`);
|
|
231
|
+
}
|
|
167
232
|
const regMsg = await nc.request(subjects.registryProviderQuery(this.providerId), payloads.buildReadProviderDefinitionQuery(), { timeout: 2000 });
|
|
168
233
|
const defs = payloads.decodeProviderDefinition(regMsg.data);
|
|
169
|
-
|
|
170
|
-
|
|
234
|
+
if (defs && defs.variables.length > 0) {
|
|
235
|
+
this.warn(`NATS Registry Discovery: Loaded ${defs.variables.length} variables.`);
|
|
236
|
+
// Same merge logic
|
|
237
|
+
const realMap = new Map();
|
|
238
|
+
defs.variables.forEach(d => realMap.set(d.key, d));
|
|
239
|
+
defs.variables.forEach(d => defMap.set(d.id, d));
|
|
240
|
+
for (const [id, def] of defMap.entries()) {
|
|
241
|
+
if (def.missingId) {
|
|
242
|
+
const real = realMap.get(def.key);
|
|
243
|
+
if (real && real.id !== id) {
|
|
244
|
+
defMap.delete(id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
171
249
|
} catch (secondErr) {
|
|
172
|
-
|
|
250
|
+
if (!hasMissingIds) {
|
|
251
|
+
this.warn(`All Discovery methods failed (REST, NATS Direct, NATS Registry). Please use Manual Definitions (Name:ID). Error: ${secondErr.message}`);
|
|
252
|
+
} else {
|
|
253
|
+
this.warn(`NATS ID Resolution failed. Continuing with Heuristic (Index-based) IDs. This generally fails for advanced providers.`);
|
|
254
|
+
}
|
|
173
255
|
}
|
|
174
256
|
}
|
|
175
257
|
}
|
|
@@ -214,7 +296,14 @@ module.exports = function (RED) {
|
|
|
214
296
|
|
|
215
297
|
// If Wildcard (empty targetIds and isWildcard=true) -> Request ALL.
|
|
216
298
|
// If Specific (targetIds has items) -> Request specific.
|
|
217
|
-
|
|
299
|
+
// Use serialRequest via Config Node to prevent concurrency issues on the connection
|
|
300
|
+
let snapshotMsg;
|
|
301
|
+
if (typeof connection.serialRequest === 'function') {
|
|
302
|
+
snapshotMsg = await connection.serialRequest(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 5000 });
|
|
303
|
+
} else {
|
|
304
|
+
// Fallback for older config nodes (should not happen if package updated correctly)
|
|
305
|
+
snapshotMsg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 5000 });
|
|
306
|
+
}
|
|
218
307
|
|
|
219
308
|
const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
|
|
220
309
|
const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
|
|
@@ -223,12 +312,41 @@ module.exports = function (RED) {
|
|
|
223
312
|
// Re-process states (lookup names, formatting)
|
|
224
313
|
const filteredSnapshot = processStates(states);
|
|
225
314
|
|
|
226
|
-
if (filteredSnapshot.length) {
|
|
315
|
+
if (filteredSnapshot.length > 0) {
|
|
227
316
|
this.send({ payload: { type: 'snapshot', variables: filteredSnapshot } });
|
|
228
317
|
} else {
|
|
229
318
|
if (states.length > 0) {
|
|
230
319
|
this.warn(`Snapshot received data but everything was filtered out. Check Variable selection. Debug: First raw ID: ${states[0].id}, DefMap has it? ${defMap.has(states[0].id)}`);
|
|
231
320
|
} else {
|
|
321
|
+
// EMPTY RESPONSE
|
|
322
|
+
// Check if we requested multiple IDs. Some providers fail on bulk read.
|
|
323
|
+
if (targetIds.length > 1) {
|
|
324
|
+
this.warn(`Snapshot Bulk Read failed (Empty List). Retrying ${targetIds.length} variables individually...`);
|
|
325
|
+
const accumulatedStates = [];
|
|
326
|
+
|
|
327
|
+
for (const id of targetIds) {
|
|
328
|
+
try {
|
|
329
|
+
let msg;
|
|
330
|
+
if (typeof connection.serialRequest === 'function') {
|
|
331
|
+
msg = await connection.serialRequest(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery([id]), { timeout: 2000 });
|
|
332
|
+
} else {
|
|
333
|
+
msg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery([id]), { timeout: 2000 });
|
|
334
|
+
}
|
|
335
|
+
const singleResponse = payloads.decodeVariableList(ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(new flatbuffers.ByteBuffer(msg.data)).variables());
|
|
336
|
+
if (singleResponse.length > 0) {
|
|
337
|
+
accumulatedStates.push(...singleResponse);
|
|
338
|
+
}
|
|
339
|
+
} catch (e) { /* ignore single failures */ }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const accumulatedFiltered = processStates(accumulatedStates);
|
|
343
|
+
if (accumulatedFiltered.length > 0) {
|
|
344
|
+
this.send({ payload: { type: 'snapshot', variables: accumulatedFiltered } });
|
|
345
|
+
this.warn(`Snapshot Recovery successful! Retrieved ${accumulatedFiltered.length} items via single requests.`);
|
|
346
|
+
return; // Success
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
232
350
|
this.warn(`Snapshot received empty list from Data Hub. (Requested ${targetIds.length > 0 ? targetIds.length + ' specific IDs' : 'ALL variables'}).`);
|
|
233
351
|
}
|
|
234
352
|
}
|
|
@@ -239,7 +357,9 @@ module.exports = function (RED) {
|
|
|
239
357
|
|
|
240
358
|
|
|
241
359
|
|
|
242
|
-
// Initial snapshot
|
|
360
|
+
// Initial snapshot with random jitter into prevent concurrency overload
|
|
361
|
+
// (If multiple nodes start simultaneously)
|
|
362
|
+
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 500) + 100));
|
|
243
363
|
await performSnapshot();
|
|
244
364
|
|
|
245
365
|
// Setup polling if configured
|
package/nodes/uos-config.js
CHANGED
|
@@ -175,6 +175,31 @@ module.exports = function (RED) {
|
|
|
175
175
|
return this.nc;
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
this.requestQueue = Promise.resolve();
|
|
179
|
+
|
|
180
|
+
this.serialRequest = async (subject, payload, options = {}) => {
|
|
181
|
+
// Enqueue request to run sequentially
|
|
182
|
+
const result = this.requestQueue.then(async () => {
|
|
183
|
+
const nc = await this.ensureConnection();
|
|
184
|
+
return nc.request(subject, payload, options);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Ensure queue does not block on failures (catch individually inside the chain logic)
|
|
188
|
+
// Actually, chaining the result directly makes the next one wait for result resolution.
|
|
189
|
+
// We want to update queue pointer to catch errors so next request still runs.
|
|
190
|
+
this.requestQueue = result.catch(() => { });
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @deprecated Use serialRequest for concurrency safety
|
|
197
|
+
*/
|
|
198
|
+
this.acquire = async () => {
|
|
199
|
+
this.users += 1;
|
|
200
|
+
return this.ensureConnection();
|
|
201
|
+
};
|
|
202
|
+
|
|
178
203
|
this.getGrantedScopes = async () => {
|
|
179
204
|
await this.getToken();
|
|
180
205
|
return this.tokenInfo?.grantedScope || '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Features: Variable Key resolution, custom icons, example flows, and provider definition caching.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "IoTUeli",
|