@tier0/opcua-client 0.6.3 → 0.6.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/README.md +15 -3
- package/lib/diagnostics.js +55 -1
- package/nodes/browse.js +165 -160
- package/nodes/connection.js +2 -8
- package/nodes/read.js +130 -125
- package/nodes/subscribe.js +806 -800
- package/nodes/write.js +72 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -364,13 +364,25 @@ Messages contain payload.level, event, message, time (ISO 8601), nodeId,
|
|
|
364
364
|
connectionId, operation, retrying and optional statusCode/counts. Subscription
|
|
365
365
|
recovery includes rebuilt and dataGapPossible. The envelope contains
|
|
366
366
|
opcua.operation = "diagnostic". No input values, endpoint URLs, credentials,
|
|
367
|
-
certificates or raw SDK error text are copied to this output.
|
|
368
|
-
|
|
367
|
+
certificates or raw SDK error text are copied to this output.
|
|
368
|
+
|
|
369
|
+
Background connection and subscription startup/recovery failures also call
|
|
370
|
+
`node.error(error, msg)` on each affected operation node. Connect a Catch node
|
|
371
|
+
in the same flow to Debug or your error-handling logic; scope Catch to the UA
|
|
372
|
+
operation nodes as needed. The shared Connection configuration no longer emits
|
|
373
|
+
duplicate state/fault warnings. A continuing background failure is reported to
|
|
374
|
+
Catch once per operation node until recovery. Connection recovery re-arms
|
|
375
|
+
connection errors; subscription errors require successful reconciliation.
|
|
376
|
+
Failed input requests still call `done(error)` for each request, preserving the
|
|
377
|
+
original message. A concurrent connection failure reported by that input does
|
|
378
|
+
not also generate a background Catch error. Point-level non-Good results remain
|
|
379
|
+
available on output 2; configuration advice and cleanup warnings remain warnings.
|
|
369
380
|
|
|
370
381
|
Identical event/status-code pairs are emitted at most once per 30 seconds per
|
|
371
382
|
node. The next emitted matching event includes suppressed repeats; there is
|
|
372
383
|
no delayed timer flush. Connection restoration resets this suppression window.
|
|
373
|
-
This limit applies only to diagnostic output
|
|
384
|
+
This 30-second limit applies only to diagnostic output; background Catch errors
|
|
385
|
+
use the recovery-based rule above.
|
|
374
386
|
|
|
375
387
|
For Subscribe, **Status messages → Both outputs (legacy compatible)** is the
|
|
376
388
|
default: existing subscription-status messages remain on output 1 and the new
|
package/lib/diagnostics.js
CHANGED
|
@@ -26,6 +26,9 @@ function createDiagnostics(node, config, operation) {
|
|
|
26
26
|
const recent = new Map();
|
|
27
27
|
let closed = false;
|
|
28
28
|
let interrupted = false;
|
|
29
|
+
const activeFailures = new Set();
|
|
30
|
+
const pendingFailures = new Map();
|
|
31
|
+
let inputs = 0;
|
|
29
32
|
const client = node.connection?.client;
|
|
30
33
|
function emit(level, event, message, details = {}, send) {
|
|
31
34
|
if (closed) return;
|
|
@@ -76,6 +79,50 @@ function createDiagnostics(node, config, operation) {
|
|
|
76
79
|
function failure(error, send, event = `${operation}_failed`, retrying = false) {
|
|
77
80
|
emit('error', event, `OPC UA ${operation} failed`, { statusCode: statusCode(error), retrying }, send);
|
|
78
81
|
}
|
|
82
|
+
// Background failures have no input message/done callback. Report from the
|
|
83
|
+
// operation node so Catch can scope the error to its flow. Retries belong to
|
|
84
|
+
// one failure episode, reset only by the corresponding recovery.
|
|
85
|
+
function backgroundFailure(error, event, group = 'subscription') {
|
|
86
|
+
if (closed) return;
|
|
87
|
+
if (interrupted) group = 'connection';
|
|
88
|
+
if (activeFailures.has(group)) return;
|
|
89
|
+
if (inputs) {
|
|
90
|
+
if (!pendingFailures.has(group)) pendingFailures.set(group, { error, event });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
activeFailures.add(group);
|
|
94
|
+
const code = statusCode(error);
|
|
95
|
+
const caught = new Error(`OPC UA ${event}: ${code}`);
|
|
96
|
+
caught.code = code;
|
|
97
|
+
node.error(caught, {
|
|
98
|
+
opcua: {
|
|
99
|
+
operation: 'diagnostic', event, statusCode: code,
|
|
100
|
+
nodeId: node.id || config.id, connectionId: config.connection,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function recovered(group) {
|
|
105
|
+
activeFailures.delete(group);
|
|
106
|
+
pendingFailures.delete(group);
|
|
107
|
+
}
|
|
108
|
+
function beginInput() {
|
|
109
|
+
inputs++;
|
|
110
|
+
}
|
|
111
|
+
function endInput(error) {
|
|
112
|
+
if (error) {
|
|
113
|
+
// done(error) already reports this input's failure to Catch.
|
|
114
|
+
for (const group of pendingFailures.keys()) activeFailures.add(group);
|
|
115
|
+
pendingFailures.clear();
|
|
116
|
+
}
|
|
117
|
+
inputs--;
|
|
118
|
+
if (!inputs) {
|
|
119
|
+
const pending = [...pendingFailures];
|
|
120
|
+
pendingFailures.clear();
|
|
121
|
+
for (const [group, failure] of pending) {
|
|
122
|
+
backgroundFailure(failure.error, failure.event, group);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
79
126
|
function results(items, send, descriptors) {
|
|
80
127
|
const failed = items.filter((item) => item.statusCode && item.statusCode !== 'Good');
|
|
81
128
|
const failures = failed.map((item) => ({
|
|
@@ -91,11 +138,13 @@ function createDiagnostics(node, config, operation) {
|
|
|
91
138
|
}
|
|
92
139
|
const onFault = (error) => {
|
|
93
140
|
interrupted = true;
|
|
141
|
+
backgroundFailure(error, 'connection_fault', 'connection');
|
|
94
142
|
emit('warn', 'connection_fault', 'OPC UA connection fault', { statusCode: statusCode(error), retrying: true });
|
|
95
143
|
};
|
|
96
144
|
const onState = (state) => {
|
|
97
145
|
if (!state) return;
|
|
98
146
|
if (state.state === 'connected') {
|
|
147
|
+
recovered('connection');
|
|
99
148
|
if (interrupted) {
|
|
100
149
|
recent.clear();
|
|
101
150
|
emit('info', 'connection_restored', 'OPC UA connection restored; subscription recovery may still be pending');
|
|
@@ -103,6 +152,9 @@ function createDiagnostics(node, config, operation) {
|
|
|
103
152
|
interrupted = false;
|
|
104
153
|
} else if (['reconnecting', 'backoff', 'failed'].includes(state.state)) {
|
|
105
154
|
interrupted = true;
|
|
155
|
+
if (state.state === 'failed') {
|
|
156
|
+
backgroundFailure(state.statusCode || 'OPCUA_CONNECTION_FAILED', 'connection_failed', 'connection');
|
|
157
|
+
}
|
|
106
158
|
emit(state.state === 'failed' ? 'error' : 'warn', `connection_${state.state}`,
|
|
107
159
|
'OPC UA connection unavailable; recovery pending', {
|
|
108
160
|
retrying: true, ...(state.statusCode && state.statusCode !== 'Good' ? { statusCode: statusCode(state.statusCode) } : {}),
|
|
@@ -114,10 +166,12 @@ function createDiagnostics(node, config, operation) {
|
|
|
114
166
|
node.on('close', () => {
|
|
115
167
|
closed = true;
|
|
116
168
|
recent.clear();
|
|
169
|
+
activeFailures.clear();
|
|
170
|
+
pendingFailures.clear();
|
|
117
171
|
client?.removeListener?.('fault', onFault);
|
|
118
172
|
client?.removeListener?.('state', onState);
|
|
119
173
|
});
|
|
120
|
-
return { emit, failure, results };
|
|
174
|
+
return { emit, failure, backgroundFailure, recovered, beginInput, endInput, results };
|
|
121
175
|
}
|
|
122
176
|
|
|
123
177
|
module.exports = { createDiagnostics, statusCode };
|
package/nodes/browse.js
CHANGED
|
@@ -1,160 +1,165 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { createDiagnostics } = require('../lib/diagnostics');
|
|
4
|
-
|
|
5
|
-
const { chunks } = require('../lib/points');
|
|
6
|
-
const { buildBrowseTree, selectedVariables } = require('../lib/browse-tree');
|
|
7
|
-
|
|
8
|
-
const activeEditorBrowses = new Set();
|
|
9
|
-
|
|
10
|
-
function positiveInteger(value, fallback, maximum) {
|
|
11
|
-
const number = Number(value);
|
|
12
|
-
if (!Number.isFinite(number) || number <= 0) return fallback;
|
|
13
|
-
return Math.min(Math.floor(number), maximum);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
module.exports = function registerBrowseNode(RED) {
|
|
17
|
-
RED.httpAdmin.post(
|
|
18
|
-
'/opcua-client/browse',
|
|
19
|
-
RED.auth.needsPermission('opcua-client.browse'),
|
|
20
|
-
async (req, res) => {
|
|
21
|
-
const body = req.body || {};
|
|
22
|
-
const connectionId = String(body.connectionId || '');
|
|
23
|
-
const connection = RED.nodes.getNode(connectionId);
|
|
24
|
-
if (!connection?.client) {
|
|
25
|
-
res.status(404).send({ error: 'Connection is not deployed. Save and deploy it before loading the node tree.' });
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (activeEditorBrowses.has(connectionId)) {
|
|
29
|
-
res.status(409).send({ error: 'A node-tree Browse is already running for this connection.' });
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const rootNodeId = String(body.rootNodeId || 'ns=0;i=85').trim();
|
|
34
|
-
if (!rootNodeId || rootNodeId.length > 1024) {
|
|
35
|
-
res.status(400).send({ error: 'Invalid root NodeId.' });
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
activeEditorBrowses.add(connectionId);
|
|
40
|
-
try {
|
|
41
|
-
connection.markUsed?.('editor-browse');
|
|
42
|
-
const items = await connection.client.browse({
|
|
43
|
-
rootNodeId,
|
|
44
|
-
recursive: true,
|
|
45
|
-
variablesOnly: false,
|
|
46
|
-
maxDepth: positiveInteger(body.maxDepth, 10, 50),
|
|
47
|
-
maxNodes: positiveInteger(body.maxNodes, 10000, 100000),
|
|
48
|
-
browseBatchSize: positiveInteger(body.browseBatchSize, 100, 5000),
|
|
49
|
-
referencesPerNode: positiveInteger(body.referencesPerNode, 1000, 10000),
|
|
50
|
-
attributeBatchSize: positiveInteger(body.attributeBatchSize, 200, 5000),
|
|
51
|
-
});
|
|
52
|
-
const tree = buildBrowseTree(items, rootNodeId);
|
|
53
|
-
res.send({
|
|
54
|
-
tree, total: items.length, rootNodeId,
|
|
55
|
-
...(items.diagnostics || {}),
|
|
56
|
-
});
|
|
57
|
-
} catch (error) {
|
|
58
|
-
const status = error?.code === 'OPCUA_RECONNECTING' ? 503 : 500;
|
|
59
|
-
res.status(status).send({ error: error.message || String(error) });
|
|
60
|
-
} finally {
|
|
61
|
-
activeEditorBrowses.delete(connectionId);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
function OpcUaBrowseNode(config) {
|
|
67
|
-
RED.nodes.createNode(this, config);
|
|
68
|
-
const node = this;
|
|
69
|
-
node.connection = RED.nodes.getNode(config.connection);
|
|
70
|
-
const diagnostics = createDiagnostics(node, config, 'browse');
|
|
71
|
-
const consumer = node.connection?.registerConsumer?.('browse', {
|
|
72
|
-
nodeId: config.id, name: config.name,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
node.on('input', async (msg, send, done) => {
|
|
76
|
-
if (!node.connection) {
|
|
77
|
-
const error = new Error('OPCUA_CONNECTION_NOT_CONFIGURED');
|
|
78
|
-
diagnostics.failure(error, send);
|
|
79
|
-
return done(error);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createDiagnostics } = require('../lib/diagnostics');
|
|
4
|
+
|
|
5
|
+
const { chunks } = require('../lib/points');
|
|
6
|
+
const { buildBrowseTree, selectedVariables } = require('../lib/browse-tree');
|
|
7
|
+
|
|
8
|
+
const activeEditorBrowses = new Set();
|
|
9
|
+
|
|
10
|
+
function positiveInteger(value, fallback, maximum) {
|
|
11
|
+
const number = Number(value);
|
|
12
|
+
if (!Number.isFinite(number) || number <= 0) return fallback;
|
|
13
|
+
return Math.min(Math.floor(number), maximum);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = function registerBrowseNode(RED) {
|
|
17
|
+
RED.httpAdmin.post(
|
|
18
|
+
'/opcua-client/browse',
|
|
19
|
+
RED.auth.needsPermission('opcua-client.browse'),
|
|
20
|
+
async (req, res) => {
|
|
21
|
+
const body = req.body || {};
|
|
22
|
+
const connectionId = String(body.connectionId || '');
|
|
23
|
+
const connection = RED.nodes.getNode(connectionId);
|
|
24
|
+
if (!connection?.client) {
|
|
25
|
+
res.status(404).send({ error: 'Connection is not deployed. Save and deploy it before loading the node tree.' });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (activeEditorBrowses.has(connectionId)) {
|
|
29
|
+
res.status(409).send({ error: 'A node-tree Browse is already running for this connection.' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rootNodeId = String(body.rootNodeId || 'ns=0;i=85').trim();
|
|
34
|
+
if (!rootNodeId || rootNodeId.length > 1024) {
|
|
35
|
+
res.status(400).send({ error: 'Invalid root NodeId.' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
activeEditorBrowses.add(connectionId);
|
|
40
|
+
try {
|
|
41
|
+
connection.markUsed?.('editor-browse');
|
|
42
|
+
const items = await connection.client.browse({
|
|
43
|
+
rootNodeId,
|
|
44
|
+
recursive: true,
|
|
45
|
+
variablesOnly: false,
|
|
46
|
+
maxDepth: positiveInteger(body.maxDepth, 10, 50),
|
|
47
|
+
maxNodes: positiveInteger(body.maxNodes, 10000, 100000),
|
|
48
|
+
browseBatchSize: positiveInteger(body.browseBatchSize, 100, 5000),
|
|
49
|
+
referencesPerNode: positiveInteger(body.referencesPerNode, 1000, 10000),
|
|
50
|
+
attributeBatchSize: positiveInteger(body.attributeBatchSize, 200, 5000),
|
|
51
|
+
});
|
|
52
|
+
const tree = buildBrowseTree(items, rootNodeId);
|
|
53
|
+
res.send({
|
|
54
|
+
tree, total: items.length, rootNodeId,
|
|
55
|
+
...(items.diagnostics || {}),
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const status = error?.code === 'OPCUA_RECONNECTING' ? 503 : 500;
|
|
59
|
+
res.status(status).send({ error: error.message || String(error) });
|
|
60
|
+
} finally {
|
|
61
|
+
activeEditorBrowses.delete(connectionId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
function OpcUaBrowseNode(config) {
|
|
67
|
+
RED.nodes.createNode(this, config);
|
|
68
|
+
const node = this;
|
|
69
|
+
node.connection = RED.nodes.getNode(config.connection);
|
|
70
|
+
const diagnostics = createDiagnostics(node, config, 'browse');
|
|
71
|
+
const consumer = node.connection?.registerConsumer?.('browse', {
|
|
72
|
+
nodeId: config.id, name: config.name,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
node.on('input', async (msg, send, done) => {
|
|
76
|
+
if (!node.connection) {
|
|
77
|
+
const error = new Error('OPCUA_CONNECTION_NOT_CONFIGURED');
|
|
78
|
+
diagnostics.failure(error, send);
|
|
79
|
+
return done(error);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
diagnostics.beginInput();
|
|
83
|
+
let inputError;
|
|
84
|
+
try {
|
|
85
|
+
consumer?.markUsed('browse');
|
|
86
|
+
const rootNodeId = msg.nodeId || msg.topic || config.rootNodeId || 'ns=0;i=85';
|
|
87
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'browsing' });
|
|
88
|
+
const configuredSelection = selectedVariables(config.selectedNodes);
|
|
89
|
+
const useSelection = configuredSelection.length > 0 && msg.opcua?.liveBrowse !== true;
|
|
90
|
+
const points = useSelection ? configuredSelection : await node.connection.client.browse({
|
|
91
|
+
rootNodeId,
|
|
92
|
+
recursive: config.recursive !== false,
|
|
93
|
+
maxDepth: Number(config.maxDepth) || 10,
|
|
94
|
+
maxNodes: Number(config.maxNodes) || 10000,
|
|
95
|
+
variablesOnly: config.variablesOnly !== false,
|
|
96
|
+
namespaceIndexes: msg.opcua?.namespaceIndexes || [],
|
|
97
|
+
browseBatchSize: Number(config.browseBatchSize) || 100,
|
|
98
|
+
referencesPerNode: Number(config.referencesPerNode) || 1000,
|
|
99
|
+
attributeBatchSize: Number(config.attributeBatchSize) || 200,
|
|
100
|
+
});
|
|
101
|
+
const browseDiagnostics = useSelection ? null : points.diagnostics || null;
|
|
102
|
+
|
|
103
|
+
const outputMode = config.outputMode || 'all';
|
|
104
|
+
if (outputMode === 'paged') {
|
|
105
|
+
const pages = chunks(points, Number(config.pageSize) || 500);
|
|
106
|
+
pages.forEach((page, index) => send({
|
|
107
|
+
...msg,
|
|
108
|
+
payload: page,
|
|
109
|
+
opcua: {
|
|
110
|
+
...(msg.opcua || {}),
|
|
111
|
+
command: index === 0 ? 'replace' : 'add',
|
|
112
|
+
source: useSelection ? 'browse-selection' : 'browse',
|
|
113
|
+
rootNodeId,
|
|
114
|
+
page: index + 1,
|
|
115
|
+
pages: pages.length,
|
|
116
|
+
done: index === pages.length - 1,
|
|
117
|
+
total: points.length,
|
|
118
|
+
...(browseDiagnostics ? { browse: browseDiagnostics } : {}),
|
|
119
|
+
},
|
|
120
|
+
}));
|
|
121
|
+
} else {
|
|
122
|
+
msg.payload = points;
|
|
123
|
+
msg.opcua = {
|
|
124
|
+
...(msg.opcua || {}),
|
|
125
|
+
command: 'replace',
|
|
126
|
+
source: useSelection ? 'browse-selection' : 'browse',
|
|
127
|
+
rootNodeId,
|
|
128
|
+
done: true,
|
|
129
|
+
total: points.length,
|
|
130
|
+
...(browseDiagnostics ? { browse: browseDiagnostics } : {}),
|
|
131
|
+
};
|
|
132
|
+
send(msg);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const partial = browseDiagnostics?.truncated || browseDiagnostics?.failures?.length;
|
|
136
|
+
if (partial) diagnostics.emit('warn', 'browse_partial', 'OPC UA Browse returned partial results', {
|
|
137
|
+
failed: browseDiagnostics?.failures?.length || 0, total: points.length,
|
|
138
|
+
failures: browseDiagnostics?.failures || [],
|
|
139
|
+
}, send);
|
|
140
|
+
node.status({
|
|
141
|
+
fill: partial ? 'yellow' : 'green', shape: partial ? 'ring' : 'dot',
|
|
142
|
+
text: `${points.length} points` +
|
|
143
|
+
(browseDiagnostics?.truncated
|
|
144
|
+
? ` / truncated: ${browseDiagnostics.truncationReason}` : ''),
|
|
145
|
+
});
|
|
146
|
+
done();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
inputError = error;
|
|
149
|
+
node.status({ fill: 'red', shape: 'ring', text: error.message });
|
|
150
|
+
diagnostics.failure(error, send);
|
|
151
|
+
done(error);
|
|
152
|
+
} finally {
|
|
153
|
+
diagnostics.endInput(inputError);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
node.on('close', (removed, done) => {
|
|
158
|
+
if (typeof removed === 'function') done = removed;
|
|
159
|
+
consumer?.release();
|
|
160
|
+
done?.();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
RED.nodes.registerType('tier0-opcua-browse', OpcUaBrowseNode);
|
|
165
|
+
};
|
package/nodes/connection.js
CHANGED
|
@@ -235,19 +235,13 @@ module.exports = function registerConnectionNode(RED) {
|
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (state.state === 'failed') node.warn(`OPC UA connection failed: ${state.statusCode}`);
|
|
241
|
-
};
|
|
242
|
-
node.client.on('fault', onFault);
|
|
243
|
-
node.client.on('state', onState);
|
|
238
|
+
// Operation nodes report connection faults to their own flow's Catch nodes.
|
|
239
|
+
// Logging here too duplicates each shared-connection failure globally.
|
|
244
240
|
node.on('close', async (removed, done) => {
|
|
245
241
|
if (typeof removed === 'function') {
|
|
246
242
|
done = removed;
|
|
247
243
|
removed = false;
|
|
248
244
|
}
|
|
249
|
-
node.client.off('fault', onFault);
|
|
250
|
-
node.client.off('state', onState);
|
|
251
245
|
registration.unregister();
|
|
252
246
|
const cleanupResults = await Promise.allSettled(
|
|
253
247
|
[...dependentCleanups].map(runDependentCleanup)
|