node-red-contrib-uos-nats 1.3.39 → 1.3.52

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 CHANGED
@@ -68,12 +68,28 @@ Reads values from existing providers (like `u_os_adm`).
68
68
  - **Provider ID:** Name of the source provider.
69
69
  - **Variables:** Use **Load Variables** to browse and select variables.
70
70
  - **Trigger:** "Event" (instant update) or "Poll" (interval).
71
+ - **Dynamic Read:** Send `msg.payload` as an Array of keys (e.g. `["machine.status", "temp"]`) to trigger a specific snapshot, ignoring the node configuration.
71
72
 
72
73
  ### DataHub - Write
73
74
  Changes values in other providers.
74
- - **Single Mode:** Select a variable from the list. Send `msg.payload` = value.
75
- - **Batch Mode:** Select NO variable (clear selection). Send `msg.payload` as a JSON object: `{"var_key": value, "var2": value}`.
76
- - **Strict Mode:** Automatically handles Fingerprints for strict providers (e.g. `u_os_sbm`).
75
+ - **Single Mode:** Select a variable from the list. Send `msg.payload` = value.
76
+ - **Batch Mode:** Select NO variable (clear selection). Send `msg.payload` as a JSON object: `{"var_key": value, "var2": value}` (uses Configured Provider).
77
+ - **Dynamic Mode:** Send a full target object to write anywhere:
78
+ ```json
79
+ {
80
+ "provider": "target_provider_id",
81
+ "key": "variable_key",
82
+ "value": 123
83
+ }
84
+ ```json
85
+ {
86
+ "provider": "target_provider_id",
87
+ "key": "variable_key",
88
+ "value": 123
89
+ }
90
+ ```
91
+ Or send an **Array** of these objects to write to multiple providers in one go.
92
+ - **Strict Mode:** Automatically handles Fingerprints for strict providers (e.g. `u_os_sbm`).
77
93
 
78
94
  ### DataHub - Provider
79
95
  Publishes your own data to the Data Hub.
@@ -92,10 +108,11 @@ Publishes your own data to the Data Hub.
92
108
  ## Troubleshooting
93
109
 
94
110
  - **Provider not visible?** Ensure **Provider ID** matches your **Client ID**. Easiest way: Leave Provider ID empty in the node.
111
+ - **Node Status is Green (Ring)?**
112
+ - `waiting for provider`: The node is connected to NATS (OK), but the target Provider (e.g. `u_os_sbm`) is currently offline. It will resume automatically.
95
113
  - **Node Status is Yellow?**
96
- - `cooldown (10s)`: The node is waiting to protect the device. This is normal after an error.
97
- - `provider offline`: The connection to NATS is OK, but the target (e.g. `u_os_sbm`) is not responding (503).
98
- - `auth failed`: Check your OAuth Client Secret and Scopes.
114
+ - `cooldown (10s)`: The node is pausing after an error to protect the network.
115
+ - `auth failed`: OAuth credentials generated an error. Check Client Secret.
99
116
  - **Node Status is Red?**
100
117
  - `illegal ID`: You used a reserved name like `u_os_sbm`. Rename your Client/Provider.
101
118
  - `write error`: A command failed. Check Scopes (`hub.variables.readwrite`) or Fingerprint.
@@ -348,4 +348,8 @@
348
348
  <li><b>Provider ID:</b> ID of the data source (e.g., <code>u_os_adm</code>). Use the search button to find available providers.</li>
349
349
  <li><b>Variables:</b> Click "Load Variables" to select specific data points. Leave empty to read everything.</li>
350
350
  </ol>
351
+ <p>
352
+ <b>Dynamic Read:</b> Send <code>msg.payload</code> as an Array of variable keys (e.g. <code>["var1", "var2"]</code>)
353
+ to read specific variables on demand. This overrides the node's configuration for that single request.
354
+ </p>
351
355
  </script>
@@ -168,101 +168,126 @@ module.exports = function (RED) {
168
168
  return; // Stop here, do not start Interval or Subscription
169
169
  }
170
170
 
171
- performSnapshot = async () => {
172
- // Debugging connection state
171
+ performSnapshot = async (overrideItems) => {
173
172
  if (!nc || nc.isClosed()) {
174
173
  this.warn('Snapshot skipped: Connection is closed or not ready.');
175
174
  return;
176
175
  }
177
176
 
178
- try {
179
- // Resolve requested variable names to IDs
180
- let targetIds = [];
181
- let isWildcard = (this.variables.length === 0);
177
+ // Group items by Provider
178
+ // Default Provider: this.providerId
179
+ const requests = new Map(); // ProviderId -> Set<VariableId>
180
+
181
+ if (overrideItems) {
182
+ // Dynamic Mode: Resolve each item
183
+ for (const item of overrideItems) {
184
+ let pId = (typeof item === 'object' && item.provider) ? item.provider : this.providerId;
185
+ let key = (typeof item === 'object') ? item.key : item; // String or Object.key
182
186
 
187
+ if (!key) continue;
188
+
189
+ try {
190
+ // Use Config Node Resolution (Cache)
191
+ // This handles fetching definitions for ANY provider on demand
192
+ const resolved = await connection.resolveVariableId(pId, key);
193
+
194
+ if (!requests.has(pId)) requests.set(pId, new Set());
195
+ requests.get(pId).add(resolved.id);
196
+ } catch (e) {
197
+ this.warn(`Dynamic Read: Skipping '${key}' on '${pId}': ${e.message}`);
198
+ }
199
+ }
200
+ } else {
201
+ // Static Mode: Use local defMap for configured provider
202
+ const targetIds = new Set();
203
+ const isWildcard = (this.variables.length === 0);
183
204
  if (!isWildcard) {
184
- // Reverse lookup: Find Def by Key
185
205
  const requestedKeys = new Set(this.variables);
186
-
187
206
  for (const def of defMap.values()) {
188
- if (requestedKeys.has(def.key)) {
189
- targetIds.push(Number(def.id));
190
- }
207
+ if (requestedKeys.has(def.key)) targetIds.add(def.id);
191
208
  }
192
-
193
- // CRITICAL FIX: If user requested specific variables but we found NONE,
194
- // we MUST NOT send an empty list, because that would interpret as "Read All".
195
- if (targetIds.length === 0) {
196
- // Already handled by Gate above mostly, but good for safety
197
- this.warn(`Snapshot Aborted: None of the ${this.variables.length} requested variables could be resolved to IDs.`);
209
+ if (targetIds.size === 0) {
210
+ this.warn(`Snapshot Aborted: None of the configured variables could be resolved.`);
198
211
  return;
199
212
  }
200
213
  }
214
+ requests.set(this.providerId, targetIds); // Empty Set = Wildcard
215
+ }
201
216
 
202
- // If Wildcard (empty targetIds and isWildcard=true) -> Request ALL.
203
- // If Specific (targetIds has items) -> Request specific.
204
- // Use serialRequest via Config Node to prevent concurrency issues on the connection
205
- // Simple Snapshot Logic using Config Node Semaphore
206
- // The Config Node now handles concurrency (max 3 parallel), preventing 503s naturally.
207
- // We use a simple retry wrapper just in case.
208
- let lastError;
209
- for (let attempt = 1; attempt <= 3; attempt++) {
210
- try {
211
- let snapshotMsg;
212
- // If config node has serialRequest (Semaphore), use it.
213
- if (typeof connection.serialRequest === 'function') {
214
- snapshotMsg = await connection.serialRequest(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 10000 });
215
- } else {
216
- // Fallback to direct request (unsafe)
217
- snapshotMsg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 10000 });
218
- }
217
+ if (requests.size === 0) {
218
+ this.warn("Snapshot Aborted: No valid variables resolved.");
219
+ return;
220
+ }
219
221
 
220
- const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
221
- const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
222
- const states = payloads.decodeVariableList(snapshotObj.variables());
223
-
224
- const filteredSnapshot = processStates(states);
225
- if (filteredSnapshot.length > 0) {
226
- this.send({ payload: { type: 'snapshot', variables: filteredSnapshot } });
227
- this.status({ fill: 'green', shape: 'dot', text: 'active' });
228
- } else {
229
- // Handle empty/partial...
230
- this.status({ fill: 'green', shape: 'ring', text: 'active (empty)' });
231
- }
232
- return true; // Success
222
+ const allResults = [];
223
+ const errors = [];
233
224
 
234
- } catch (err) {
235
- lastError = err;
236
- // If semaphore is full or timeout, wait a bit
237
- await new Promise(r => setTimeout(r, 1000 * attempt));
238
- }
239
- }
240
- if (lastError) {
241
- const msg = lastError.message || '';
242
- if (msg.includes('503') || msg.includes('no responders')) {
243
- this.debug(`Snapshot skipped (Provider offline/503): ${msg}`);
244
- this.status({ fill: 'yellow', shape: 'dot', text: 'provider offline' });
245
- } else if (msg.includes('Cooldown')) {
246
- this.debug(`Snapshot skipped (Cooldown): ${msg}`);
247
- this.status({ fill: 'yellow', shape: 'ring', text: 'cooldown (10s)' });
248
- } else if (msg.includes('Authorization') || msg.includes('Permission')) {
249
- this.debug(`Snapshot skipped (Auth): ${msg}`);
250
- this.status({ fill: 'yellow', shape: 'ring', text: 'auth failed' });
225
+ // Execute Requests (Serial or Parallel?)
226
+ // Using Serial to be safe with connection bandwidth
227
+ for (const [pId, varIds] of requests) {
228
+ try {
229
+ let targetIdsArr = Array.from(varIds);
230
+
231
+ // Request
232
+ let snapshotMsg;
233
+ if (typeof connection.serialRequest === 'function') {
234
+ snapshotMsg = await connection.serialRequest(subjects.readVariablesQuery(pId), payloads.buildReadVariablesQuery(targetIdsArr), { timeout: 10000 });
251
235
  } else {
252
- this.warn(`Snapshot failed: ${msg}`);
253
- this.status({ fill: 'red', shape: 'ring', text: 'snapshot error' });
236
+ snapshotMsg = await nc.request(subjects.readVariablesQuery(pId), payloads.buildReadVariablesQuery(targetIdsArr), { timeout: 10000 });
254
237
  }
255
- }
256
- } catch (e) {
257
- const msg = e.message || '';
258
- if (msg.includes('503') || msg.includes('no responders')) {
259
- this.debug(`Snapshot skipped (Provider offline/503): ${msg}`);
260
- } else if (msg.includes('Cooldown')) {
261
- // Ignore cooldown errors in outer catch
262
- } else {
263
- this.warn(`Snapshot failed: ${msg}`);
238
+
239
+ const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
240
+ const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
241
+ const states = payloads.decodeVariableList(snapshotObj.variables());
242
+
243
+ // Add Provider Info to Result?
244
+ // ProcessStates adds keys, but we might want to know which provider it came from if mixed?
245
+ // The current `processStates` maps IDs back to Keys using `defMap`.
246
+ // ISSUE: `defMap` only has local provider defs.
247
+ // If we read from foreign provider, `processStates` will return IDs or "Unknown".
248
+ // We need `resolveVariableId` to also START caching the reverse lookup or we assume result has IDs?
249
+ // Wait, `decodeVariableList` returns {id, value}.
250
+ // `processStates` tries to find Key.
251
+
252
+ // QUICK FIX for Dynamic Multi-Provider:
253
+ // If pId !== this.providerId, we don't have local defMap.
254
+ // But `resolveVariableId` (in config) caches definitions.
255
+ // We can ask config to resolve ID back to Key? Or just return ID?
256
+ // Better: We know the Keys we asked for.
257
+
258
+ // Let's modify processStates to accept a custom Map?
259
+ // Or simple: Just return the raw objects with IDs if we can't map them?
260
+ // Actually, for Dynamic Read, users might accept {id, value} or we try to enrich.
261
+
262
+ const enriched = states.map(s => {
263
+ // Try local defMap
264
+ if (pId === this.providerId && defMap.has(s.id)) {
265
+ return { ...s, key: defMap.get(s.id).key, provider: pId };
266
+ }
267
+ // Try Config Node Cache (it has all fetched defs)
268
+ const cachedDef = connection.getProviderVariable(pId, s.id);
269
+ if (cachedDef) {
270
+ return { ...s, key: cachedDef.key, provider: pId };
271
+ }
272
+ return { ...s, key: `id:${s.id}`, provider: pId };
273
+ });
274
+
275
+ allResults.push(...enriched);
276
+
277
+ } catch (err) {
278
+ errors.push(`${pId}: ${err.message}`);
264
279
  }
265
280
  }
281
+
282
+ if (allResults.length > 0) {
283
+ this.send({ payload: { type: 'snapshot', variables: allResults } });
284
+ this.status({ fill: 'green', shape: 'dot', text: 'active' });
285
+ } else if (errors.length > 0) {
286
+ this.warn(`Snapshot errors: ${errors.join('; ')}`);
287
+ this.status({ fill: 'red', shape: 'ring', text: 'error' });
288
+ } else {
289
+ this.status({ fill: 'green', shape: 'ring', text: 'empty' });
290
+ }
266
291
  };
267
292
 
268
293
 
@@ -334,7 +359,14 @@ module.exports = function (RED) {
334
359
  };
335
360
 
336
361
  this.on('input', (msg, send, done) => {
337
- performSnapshot()
362
+ let overrideItems = null;
363
+ if (Array.isArray(msg.payload) && msg.payload.length > 0) {
364
+ // Pass the raw array items (String or Object {provider, key})
365
+ // performSnapshot will handle filtering
366
+ overrideItems = msg.payload;
367
+ }
368
+
369
+ performSnapshot(overrideItems)
338
370
  .then(() => done())
339
371
  .catch((err) => done(err));
340
372
  });
@@ -135,7 +135,9 @@
135
135
  <dd>Connection node with host/port and OAuth credentials.</dd>
136
136
  <dt>Provider ID</dt>
137
137
  <dd>The unique ID for this provider. <b>Note:</b> You cannot use <code>u_os_sbm</code> (Reserved system name).</dd>
138
- <dd><b>Auto Mode:</b> Shows the <em>Client Name</em> from your Config. Uses this name automatically.</dd>
138
+ <dt>Provider ID</dt>
139
+ <dd>The unique ID for this provider. <b>Note:</b> You cannot use <code>u_os_sbm</code> (Reserved system name).</dd>
140
+ <dd><b>Auto Mode:</b> If empty, uses your **Client Name** (e.g. `nodered`) to match your Connection.</dd>
139
141
  <dt>Keep-Alive (s)</dt>
140
142
  <dd>Interval in seconds to refresh the provider definition. Default: <b>300s (5 min)</b>.</dd>
141
143
  <dt>Allow external writes</dt>
@@ -77,11 +77,10 @@ module.exports = function (RED) {
77
77
  return;
78
78
  }
79
79
  // Retrieve configuration node
80
- // Default to Connection's Client Name (Friendly Name) because Client ID is often a UUID
81
- // that doesn't match the desired Provider ID. User typically expects 'nodered' not '0069...'
80
+ // Auto Mode: Prefer Client Name ("nodered") to match Connection Name.
81
+ // This ensures consistency with DataHub UI expectations.
82
82
  let defaultId = connection.clientName;
83
83
  if (!defaultId) {
84
- // Fallback to ID if Name is missing (should not happen as Name is mandatory)
85
84
  defaultId = connection.clientId || 'nodered';
86
85
  }
87
86
 
@@ -134,6 +133,7 @@ module.exports = function (RED) {
134
133
  };
135
134
 
136
135
  // Check Singleton Status
136
+ this.fatalError = false;
137
137
  let isPrimary = false;
138
138
  const existingNodeId = providerRegistry.get(this.providerId);
139
139
  if (existingNodeId && existingNodeId !== this.id) {
@@ -147,7 +147,8 @@ module.exports = function (RED) {
147
147
  // --- SEND DEFINITION HELPER ---
148
148
  const sendDefinitionUpdate = async (modPayloads, modSubjects) => {
149
149
  if (!nc) return;
150
- if (!isPrimary) return; // Only Primary sends definitions
150
+ if (!isPrimary) return;
151
+ if (this.fatalError) return; // permanent lockout
151
152
 
152
153
  try {
153
154
  const { payload, fingerprint: fp } = modPayloads.buildProviderDefinitionEvent(definitions);
@@ -160,7 +161,17 @@ module.exports = function (RED) {
160
161
  await nc.flush();
161
162
  console.log(`[DataHub Output] Definition published. FP: ${fp}`);
162
163
  } catch (err) {
163
- this.warn(`Definition update error: ${err.message}`);
164
+ let msg = err.message || '';
165
+ // Check for fatal Auth/Permission errors
166
+ if (msg.includes('Authorization') || msg.includes('permissions') || msg.includes('10003') || msg.includes('Access Denied')) {
167
+ this.fatalError = true;
168
+ this.error(`FATAL AUTH ERROR: ${msg}. Stopping provider to protect connection.`);
169
+ this.status({ fill: 'red', shape: 'dot', text: 'auth blocked (permanent)' });
170
+ // Clear heartbeats
171
+ if (outputHeartbeat) clearInterval(outputHeartbeat);
172
+ } else {
173
+ this.warn(`Definition update error: ${err.message}`);
174
+ }
164
175
  }
165
176
  };
166
177
 
@@ -186,6 +197,7 @@ module.exports = function (RED) {
186
197
  // console.log('[DataHub Output] Heartbeat skipped: NATS closed.');
187
198
  return;
188
199
  }
200
+ if (this.fatalError) return; // permanent lockout
189
201
  if (!loadedPayloads || !loadedSubjects) return;
190
202
 
191
203
  // If we have no definitions yet, nothing to send
@@ -248,5 +248,24 @@
248
248
  <li><b>Provider ID:</b> Target provider (e.g. <code>u_os_adm</code>). Use the search button to find it.</li>
249
249
  <li><b>Variable:</b> Select a target variable. <b>Only writable variables are shown.</b></li>
250
250
  </ol>
251
- <p>If no variable is selected, the node works in <b>Batch Mode</b> (expects JSON object payload).</p>
252
- </script>
251
+ <p>If no variable is selected, the node works in <b>Batch Mode</b> or <b>Dynamic Mode</b>.</p>
252
+ <ul>
253
+ <li><b>Batch:</b> Send a JSON object to update multiple keys for the configured Provider: <code>{"key": value, "key2": value}</code></li>
254
+ <li><b>Dynamic:</b> Send a specific object to target ANY Provider/Key:
255
+ <code>
256
+ {
257
+ "provider": "u_os_sbm",
258
+ "key": "machine.outputs.DO01",
259
+ "value": true
260
+ }
261
+ </code>
262
+ </li>
263
+ <li><b>Mixed Array:</b> Send an array of dynamic objects to write to multiple providers at once:
264
+ <code>
265
+ [
266
+ { "provider": "u_os_sbm", "key": "doo1", "value": true },
267
+ { "provider": "other_provider", "key": "var2", "value": 123 }
268
+ ]
269
+ </code>
270
+ </li>
271
+ </ul>
@@ -170,7 +170,8 @@ module.exports = function (RED) {
170
170
  node.resolvedId = this.variableId;
171
171
  // Log as debug implies we handle it gracefully => No User Warn
172
172
  node.debug(`ID Resolution failed for '${this.variableKey}' (${err.message}). Using configured ID: ${this.variableId}`);
173
- node.status({ fill: 'green', shape: 'ring', text: 'ready (fallback)' });
173
+ // User Request v1.3.48: Show Green 'ready' even if ID resolution failed (Dynamic Mode)
174
+ node.status({ fill: 'green', shape: 'dot', text: 'ready' });
174
175
  } else {
175
176
  node.warn(`ID Resolution failed for '${this.variableKey}': ${err.message}`);
176
177
  node.status({ fill: 'red', shape: 'dot', text: 'resolution failed' });
@@ -228,30 +229,40 @@ module.exports = function (RED) {
228
229
  // Array: [{id:1, value:val}, {key:'k', value:val}]
229
230
  items = rawPayload;
230
231
  } else {
231
- // Object: { "key": val, "id:1": val }
232
- items = Object.entries(rawPayload).map(([k, v]) => {
233
- // Detect if key is actually an ID (e.g. "5" or "id:5")?
234
- // Easier: treat all keys as Keys, unless user passes explicit Array.
235
- const asInt = parseInt(k, 10);
236
- if (!isNaN(asInt) && String(asInt) === k) {
237
- return { id: asInt, value: v };
238
- }
239
- return { key: k, value: v };
240
- });
232
+ // Check for Single Dynamic Write Object
233
+ // Format: { provider: "p", key: "k", value: "v" }
234
+ if (rawPayload.provider || rawPayload.key || rawPayload.variableKey || rawPayload.variableId) {
235
+ items = [rawPayload];
236
+ } else {
237
+ // Object: { "key": val, "id:1": val }
238
+ items = Object.entries(rawPayload).map(([k, v]) => {
239
+ // Detect if key is actually an ID (e.g. "5" or "id:5")?
240
+ // Easier: treat all keys as Keys, unless user passes explicit Array.
241
+ const asInt = parseInt(k, 10);
242
+ if (!isNaN(asInt) && String(asInt) === k) {
243
+ return { id: asInt, value: v };
244
+ }
245
+ return { key: k, value: v };
246
+ });
247
+ }
241
248
  }
242
249
 
243
250
  // Process Items
244
251
  for (const item of items) {
245
- let targetId = item.id;
252
+ let targetId = item.id || item.variableId;
246
253
  let targetType = item.dataType; // Optional explicit type
247
254
 
248
255
  if (targetId === undefined) {
249
- if (item.key) {
256
+ const effectiveKey = item.key || item.variableKey;
257
+ if (effectiveKey) {
250
258
  // Resolve Key
251
259
  try {
260
+ // Use Dynamic Provider if present, otherwise configured Provider
261
+ const effectiveProvider = item.provider || item.providerId || node.providerId;
262
+
252
263
  // Use Centralized Cache in Config Node
253
264
  if (typeof configNode.resolveVariableId === 'function') {
254
- const resolved = await configNode.resolveVariableId(node.providerId, item.key);
265
+ const resolved = await configNode.resolveVariableId(effectiveProvider, effectiveKey);
255
266
  targetId = resolved.id;
256
267
  if (!targetType) targetType = resolved.dataType;
257
268
  if (resolved.fingerprint) currentFingerprint = resolved.fingerprint;
@@ -259,7 +270,7 @@ module.exports = function (RED) {
259
270
  throw new Error("Config Node too old");
260
271
  }
261
272
  } catch (e) {
262
- node.warn(`Skipping key '${item.key}': ${e.message}`);
273
+ node.warn(`Skipping key '${effectiveKey}': ${e.message}`);
263
274
  continue;
264
275
  }
265
276
  } else {
@@ -176,37 +176,51 @@ module.exports = function (RED) {
176
176
  if (this.nc) {
177
177
  return this.nc;
178
178
  }
179
- // Ensure we have a valid token initially
180
- await this.getToken();
179
+ // Deduplication: Return existing promise if we are already connecting
180
+ if (this.connectionPromise) {
181
+ // this.log('Joining pending connection request...');
182
+ return this.connectionPromise;
183
+ }
181
184
 
182
- // Use jwtAuthenticator to allow dynamic token refresh on reconnect
183
- try {
184
- this.nc = await connect({
185
- servers: `nats://${this.host}:${this.port}`,
186
- // Authenticator must be SYNCHRONOUS. We rely on background refresh to keep this.tokenInfo current.
187
- // Token function must be SYNCHRONOUS if we rely on background refresh.
188
- token: () => {
189
- return this.tokenInfo ? this.tokenInfo.token : '';
190
- },
191
- name: this.clientName,
192
- inboxPrefix: `_INBOX.${this.clientName}`,
193
- maxReconnectAttempts: -1, // Infinite reconnects
194
- reconnectTimeWait: 2000,
195
- });
196
- this.log(`NATS connecting with Name: '${this.clientName}'`);
185
+ this.connectionPromise = (async () => {
186
+ try {
187
+ // Ensure we have a valid token initially
188
+ await this.getToken();
189
+
190
+ this.nc = await connect({
191
+ servers: `nats://${this.host}:${this.port}`,
192
+ // Authenticator must be SYNCHRONOUS. We rely on background refresh to keep this.tokenInfo current.
193
+ // Token function must be SYNCHRONOUS if we rely on background refresh.
194
+ token: () => {
195
+ return this.tokenInfo ? this.tokenInfo.token : '';
196
+ },
197
+ // REVERT: Use Configured Client Name for Connection.
198
+ // Using UUID caused "Authorization Violation" for some users.
199
+ name: this.clientName,
200
+ inboxPrefix: `_INBOX.${this.clientName}`,
201
+ maxReconnectAttempts: -1, // Infinite reconnects
202
+ reconnectTimeWait: 2000,
203
+ });
197
204
 
198
- // Reset Failure timestamp on success
199
- this.authFailureTimestamp = 0;
205
+ this.log(`NATS connecting as Name: '${this.clientName}' (Dedup Active)`);
200
206
 
201
- } catch (e) {
202
- if (e.message && (e.message.includes('Authorization') || e.message.includes('Permissions') || e.message.includes('Authentication'))) {
203
- this.warn(`NATS Authorization failed. Invalidating token cache. Circuit Breaker active for 10s.`);
204
- this.tokenInfo = null; // Force fresh token next time
205
- this.authFailureTimestamp = Date.now(); // Start Cooldown
207
+ // Reset Failure timestamp on success
208
+ this.authFailureTimestamp = 0;
209
+ return this.nc;
210
+
211
+ } catch (e) {
212
+ if (e.message && (e.message.includes('Authorization') || e.message.includes('Permissions') || e.message.includes('Authentication'))) {
213
+ this.warn(`NATS Authorization failed. Invalidating token cache. Circuit Breaker active for 10s.`);
214
+ this.tokenInfo = null; // Force fresh token next time
215
+ this.authFailureTimestamp = Date.now(); // Start Cooldown
216
+ }
217
+ this.error(`NATS connect failed: ${e.message}`);
218
+ throw e;
219
+ } finally {
220
+ this.connectionPromise = null;
206
221
  }
207
- this.error(`NATS connect failed: ${e.message}`);
208
- throw e;
209
- }
222
+ })();
223
+ return this.connectionPromise;
210
224
  this.nc.closed().then(() => {
211
225
  this.nc = null;
212
226
  this.emit('disconnected');
@@ -545,8 +559,26 @@ module.exports = function (RED) {
545
559
  }
546
560
  };
547
561
 
548
- this.on('close', (done) => {
549
- this.release().finally(done);
562
+ this.on('close', async (done) => {
563
+ // Force Close Connection on Full Deploy (ignore reference count)
564
+ if (this.refreshTimer) {
565
+ clearTimeout(this.refreshTimer);
566
+ this.refreshTimer = null;
567
+ }
568
+
569
+ if (this.nc) {
570
+ const nc = this.nc;
571
+ this.nc = null;
572
+ this.users = 0;
573
+ try {
574
+ // Drain ensures all pending messages are sent before closing
575
+ await nc.drain();
576
+ // this.log('NATS Connection flushed and closed.');
577
+ } catch (err) {
578
+ this.warn(`Error closing NATS connection: ${err.message}`);
579
+ }
580
+ }
581
+ done();
550
582
  });
551
583
  }
552
584
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-uos-nats",
3
- "version": "1.3.39",
3
+ "version": "1.3.52",
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",