node-opcua-client 2.173.1 → 2.175.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/README.md +67 -33
- package/dist/client_session.d.ts +75 -3
- package/dist/opcua_client.d.ts +139 -5
- package/dist/opcua_client.js +40 -0
- package/dist/opcua_client.js.map +1 -1
- package/dist/private/client_session_impl.d.ts +1 -0
- package/dist/private/client_session_impl.js +1 -0
- package/dist/private/client_session_impl.js.map +1 -1
- package/dist/private/opcua_client_impl.js +6 -2
- package/dist/private/opcua_client_impl.js.map +1 -1
- package/dist/private/reconnection/client_publish_engine_reconnection.js +41 -7
- package/dist/private/reconnection/client_publish_engine_reconnection.js.map +1 -1
- package/dist/private/reconnection/reconnection.js +25 -6
- package/dist/private/reconnection/reconnection.js.map +1 -1
- package/package.json +38 -38
- package/source/client_session.ts +75 -3
- package/source/opcua_client.ts +139 -6
- package/source/private/client_session_impl.ts +15 -15
- package/source/private/opcua_client_impl.ts +19 -18
- package/source/private/reconnection/client_publish_engine_reconnection.ts +43 -8
- package/source/private/reconnection/reconnection.ts +26 -6
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { LocalizedText } from "node-opcua-data-model";
|
|
22
22
|
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
23
23
|
import { extractFullyQualifiedDomainName } from "node-opcua-hostname";
|
|
24
|
+
import type { NodeId } from "node-opcua-nodeid";
|
|
24
25
|
import { readNamespaceArray } from "node-opcua-pseudo-session";
|
|
25
26
|
import {
|
|
26
27
|
type ClientSecureChannelLayer,
|
|
@@ -48,7 +49,6 @@ import {
|
|
|
48
49
|
import { type Callback, type CallbackT, type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
49
50
|
import type { SignatureDataOptions, UserIdentityToken } from "node-opcua-types";
|
|
50
51
|
import { isNullOrUndefined, matchUri, randomBytes } from "node-opcua-utils";
|
|
51
|
-
import type { NodeId } from "node-opcua-nodeid";
|
|
52
52
|
import type { OPCUAClientBaseEvents } from "../client_base";
|
|
53
53
|
import type { ClientSession } from "../client_session";
|
|
54
54
|
import type { ClientSubscriptionOptions } from "../client_subscription";
|
|
@@ -563,11 +563,7 @@ export class OPCUAClientImpl extends ClientBaseImpl<OPCUAClientBaseEvents> {
|
|
|
563
563
|
clearTimeout(this._retryCreateSessionTimer);
|
|
564
564
|
this._retryCreateSessionTimer = undefined;
|
|
565
565
|
}
|
|
566
|
-
super.closeSession(
|
|
567
|
-
session as unknown as ClientSessionImpl,
|
|
568
|
-
deleteSubscriptions,
|
|
569
|
-
callback as (err?: Error) => void
|
|
570
|
-
);
|
|
566
|
+
super.closeSession(session as unknown as ClientSessionImpl, deleteSubscriptions, callback as (err?: Error) => void);
|
|
571
567
|
}
|
|
572
568
|
|
|
573
569
|
public toJSON(): Record<string, string | number | boolean | undefined> {
|
|
@@ -733,11 +729,11 @@ export class OPCUAClientImpl extends ClientBaseImpl<OPCUAClientBaseEvents> {
|
|
|
733
729
|
return callback?.(
|
|
734
730
|
new Error(
|
|
735
731
|
" End point must exist " +
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
732
|
+
this._secureChannel?.endpointUrl +
|
|
733
|
+
" securityMode = " +
|
|
734
|
+
MessageSecurityMode[this.securityMode] +
|
|
735
|
+
" securityPolicy = " +
|
|
736
|
+
this.securityPolicy
|
|
741
737
|
)
|
|
742
738
|
);
|
|
743
739
|
}
|
|
@@ -968,7 +964,9 @@ export class OPCUAClientImpl extends ClientBaseImpl<OPCUAClientBaseEvents> {
|
|
|
968
964
|
// clientCertificate. The SignatureAlgorithm shall be the AsymmetricSignatureAlgorithm
|
|
969
965
|
// specified in the SecurityPolicy for the Endpoint. The SignatureData type is defined in 7.30.
|
|
970
966
|
|
|
971
|
-
clientSignature:
|
|
967
|
+
clientSignature:
|
|
968
|
+
this.computeClientSignature(this._secureChannel as ClientSecureChannelLayer, serverCertificate, serverNonce) ||
|
|
969
|
+
undefined,
|
|
972
970
|
|
|
973
971
|
// These are the SoftwareCertificates which have been issued to the Client application.
|
|
974
972
|
// The productUri contained in the SoftwareCertificates shall match the productUri in the
|
|
@@ -1013,7 +1011,7 @@ export class OPCUAClientImpl extends ClientBaseImpl<OPCUAClientBaseEvents> {
|
|
|
1013
1011
|
session.lastRequestSentTime = new Date();
|
|
1014
1012
|
|
|
1015
1013
|
this.performMessageTransaction(request, (err1: Error | null, response?: Response) => {
|
|
1016
|
-
if (!err1 && response && response.responseHeader.serviceResult
|
|
1014
|
+
if (!err1 && response && response.responseHeader.serviceResult.isGoodish()) {
|
|
1017
1015
|
/* c8 ignore next */
|
|
1018
1016
|
if (!(response instanceof ActivateSessionResponse)) {
|
|
1019
1017
|
return callback(new Error("Internal Error"));
|
|
@@ -1023,6 +1021,9 @@ export class OPCUAClientImpl extends ClientBaseImpl<OPCUAClientBaseEvents> {
|
|
|
1023
1021
|
return callback(new Error("Invalid server Nonce"));
|
|
1024
1022
|
}
|
|
1025
1023
|
session._client = this;
|
|
1024
|
+
// capture the activation outcome (e.g. Good_PasswordChangeRequired,
|
|
1025
|
+
// OPC 10000-18 §5.2.8) so a Client can react without server access.
|
|
1026
|
+
session.lastActivateSessionStatusCode = response.responseHeader.serviceResult;
|
|
1026
1027
|
session.serverNonce = response.serverNonce;
|
|
1027
1028
|
session.lastResponseReceivedTime = new Date();
|
|
1028
1029
|
if (this.keepSessionAlive) {
|
|
@@ -1152,11 +1153,11 @@ export class OPCUAClientImpl extends ClientBaseImpl<OPCUAClientBaseEvents> {
|
|
|
1152
1153
|
return callback(
|
|
1153
1154
|
new Error(
|
|
1154
1155
|
" End point must exist " +
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1156
|
+
this._secureChannel?.endpointUrl +
|
|
1157
|
+
" securityMode = " +
|
|
1158
|
+
MessageSecurityMode[this.securityMode] +
|
|
1159
|
+
" securityPolicy = " +
|
|
1160
|
+
this.securityPolicy
|
|
1160
1161
|
)
|
|
1161
1162
|
);
|
|
1162
1163
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import assert from "node-opcua-assert";
|
|
3
3
|
import { checkDebugFlag, make_debugLog } from "node-opcua-debug";
|
|
4
|
-
import { StatusCodes } from "node-opcua-status-code";
|
|
4
|
+
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
5
5
|
import { RepublishRequest, type RepublishResponse } from "node-opcua-types";
|
|
6
6
|
import type { ClientSidePublishEngine } from "../client_publish_engine";
|
|
7
7
|
import type { ClientSessionImpl } from "../client_session_impl";
|
|
@@ -12,6 +12,33 @@ import { _shouldNotContinue2 } from "./reconnection";
|
|
|
12
12
|
const debugLog = make_debugLog("RECONNECTION");
|
|
13
13
|
const doDebug = checkDebugFlag("RECONNECTION");
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Recover the StatusCode carried by a Republish reply, whether it came back as a regular
|
|
17
|
+
* RepublishResponse (operation-level serviceResult) or as a ServiceFault.
|
|
18
|
+
*
|
|
19
|
+
* Precedence:
|
|
20
|
+
* 1. the explicit `response` argument, when the server answered with a regular RepublishResponse
|
|
21
|
+
* (the operation-level serviceResult, e.g. CoDeSys);
|
|
22
|
+
* 2. otherwise `err.response` — the secure channel layer turns a ServiceFault into an Error and
|
|
23
|
+
* attaches the original response there;
|
|
24
|
+
* 3. otherwise fall back to parsing the error message.
|
|
25
|
+
*/
|
|
26
|
+
function extractStatusCode(err: Error | null | undefined, response?: RepublishResponse): StatusCode {
|
|
27
|
+
if (response) {
|
|
28
|
+
return response.responseHeader.serviceResult;
|
|
29
|
+
}
|
|
30
|
+
if (err) {
|
|
31
|
+
const faultResponse = (err as { response?: { responseHeader?: { serviceResult?: StatusCode } } }).response;
|
|
32
|
+
if (faultResponse?.responseHeader?.serviceResult) {
|
|
33
|
+
return faultResponse.responseHeader.serviceResult;
|
|
34
|
+
}
|
|
35
|
+
if (/BadMessageNotAvailable/.test(err.message)) {
|
|
36
|
+
return StatusCodes.BadMessageNotAvailable;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return StatusCodes.Bad;
|
|
40
|
+
}
|
|
41
|
+
|
|
15
42
|
function _sendRepublish(
|
|
16
43
|
session: ClientSessionImpl,
|
|
17
44
|
subscription: ClientSubscriptionImpl
|
|
@@ -41,16 +68,22 @@ function _sendRepublish(
|
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
session.republish(request, (err: Error | null, response?: RepublishResponse) => {
|
|
71
|
+
// BadMessageNotAvailable signals the end of the republish loop: there are no more
|
|
72
|
+
// queued NotificationMessages to replay and the client must now resume normal Publish
|
|
73
|
+
// handling (OPC UA Part 4 §6.5/§6.7). A server may report it either as a ServiceFault
|
|
74
|
+
// (err set, no response) or as the serviceResult of a regular RepublishResponse
|
|
75
|
+
// (operation-level result, e.g. CoDeSys). Both cases must terminate the loop.
|
|
76
|
+
const statusCode = extractStatusCode(err, response);
|
|
77
|
+
if (statusCode.equals(StatusCodes.BadMessageNotAvailable)) {
|
|
78
|
+
return resolve({ isDone: true });
|
|
79
|
+
}
|
|
44
80
|
if (!response) {
|
|
45
81
|
reject(err);
|
|
46
82
|
return;
|
|
47
83
|
}
|
|
48
|
-
|
|
49
|
-
if (!err && (statusCode.equals(StatusCodes.Good) || statusCode.equals(StatusCodes.BadMessageNotAvailable))) {
|
|
84
|
+
if (!err && statusCode.equals(StatusCodes.Good)) {
|
|
50
85
|
// reprocess notification message and keep going
|
|
51
|
-
|
|
52
|
-
subscription.onNotificationMessage(response.notificationMessage);
|
|
53
|
-
}
|
|
86
|
+
subscription.onNotificationMessage(response.notificationMessage);
|
|
54
87
|
return resolve({ isDone: false });
|
|
55
88
|
}
|
|
56
89
|
if (!err) {
|
|
@@ -120,8 +153,10 @@ async function __askSubscriptionRepublish(
|
|
|
120
153
|
await recreateSubscriptionAndMonitoredItem(subscription);
|
|
121
154
|
return;
|
|
122
155
|
}
|
|
123
|
-
if (error.message.match(
|
|
124
|
-
//
|
|
156
|
+
if (error.message.match(/BadMessageNotAvailable/)) {
|
|
157
|
+
// End of the republish loop: no more queued messages to replay.
|
|
158
|
+
// Nothing else to do here, the caller will resume normal Publish handling.
|
|
159
|
+
return;
|
|
125
160
|
}
|
|
126
161
|
}
|
|
127
162
|
// check after successful republish too
|
|
@@ -8,7 +8,6 @@ import { assert } from "node-opcua-assert";
|
|
|
8
8
|
import { invalidateExtraDataTypeManager } from "node-opcua-client-dynamic-extension-object";
|
|
9
9
|
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
10
10
|
import { TransferSubscriptionsRequest, type TransferSubscriptionsResponse } from "node-opcua-service-subscription";
|
|
11
|
-
import { StatusCodes } from "node-opcua-status-code";
|
|
12
11
|
import { CloseSessionRequest } from "node-opcua-types";
|
|
13
12
|
import type { ClientSessionImpl, Reconnectable } from "../client_session_impl";
|
|
14
13
|
import type { ClientSubscriptionImpl } from "../client_subscription_impl";
|
|
@@ -162,6 +161,19 @@ function _ask_for_subscription_republish(session: ClientSessionImpl, callback: (
|
|
|
162
161
|
return repair_client_session_by_recreating_a_new_session(session._client!, session, callback);
|
|
163
162
|
}
|
|
164
163
|
|
|
164
|
+
// Republish has completed: resume normal Publish handling. During the connection break the
|
|
165
|
+
// in-flight PublishRequests have failed and the publish-request queue has drained, so we must
|
|
166
|
+
// replenish it now, otherwise the client would stop sending PublishRequests and never receive
|
|
167
|
+
// live notifications again. (https://github.com/node-opcua/node-opcua/issues/1524)
|
|
168
|
+
// Note: on the new-session repair path the engine is still suspended here, so this is a no-op
|
|
169
|
+
// there and the queue is replenished later by suspend(false).
|
|
170
|
+
// Guard on subscriptionCount: with no subscription, send_publish_request() would still emit
|
|
171
|
+
// spurious PublishRequests (it does not check the subscription count itself), adding useless
|
|
172
|
+
// round-trips on every reconnection of a subscription-less session.
|
|
173
|
+
if (engine.subscriptionCount > 0) {
|
|
174
|
+
engine.replenish_publish_request_queue();
|
|
175
|
+
}
|
|
176
|
+
|
|
165
177
|
callback(err);
|
|
166
178
|
});
|
|
167
179
|
}
|
|
@@ -174,7 +186,8 @@ function create_session_and_repeat_if_failed(client: IClientBase, session: Clien
|
|
|
174
186
|
// create new session, based on old session,
|
|
175
187
|
// so we can reuse subscriptions data
|
|
176
188
|
client.__createSession_step2(session, (err: Error | null, session1?: ClientSessionImpl) => {
|
|
177
|
-
|
|
189
|
+
const abortErr = _shouldNotContinue(session);
|
|
190
|
+
if (abortErr) return reject(abortErr);
|
|
178
191
|
|
|
179
192
|
if (!err && session1) {
|
|
180
193
|
assert(session === session1, "session should have been recycled");
|
|
@@ -286,7 +299,8 @@ function attempt_subscription_transfer(
|
|
|
286
299
|
subscriptionsToTransfer,
|
|
287
300
|
(err: Error | null, transferSubscriptionsResponse?: TransferSubscriptionsResponse) => {
|
|
288
301
|
// may be the connection with server has been disconnected
|
|
289
|
-
|
|
302
|
+
const abortErr = _shouldNotContinue(session);
|
|
303
|
+
if (abortErr) return reject(abortErr);
|
|
290
304
|
|
|
291
305
|
if (err || !transferSubscriptionsResponse) {
|
|
292
306
|
warningLog(chalk.bgCyan("May be the server is not supporting this feature"));
|
|
@@ -315,13 +329,19 @@ function attempt_subscription_transfer(
|
|
|
315
329
|
// those one need to be recreated and repaired ....
|
|
316
330
|
for (let i = 0; i < results.length; i++) {
|
|
317
331
|
const statusCode = results[i].statusCode;
|
|
318
|
-
if (statusCode.
|
|
319
|
-
//
|
|
332
|
+
if (statusCode.isNotGood()) {
|
|
333
|
+
// the subscription could not be transferred (BadSubscriptionIdInvalid when the
|
|
334
|
+
// subscription no longer exists, BadUserAccessDenied when the server refuses the
|
|
335
|
+
// transfer per OPC UA Part 4 §5.14.7, ...). Whatever the reason, recreate it here
|
|
336
|
+
// directly on the new session instead of relying on the subsequent Republish step
|
|
337
|
+
// to notice and repair it (which would otherwise cost an extra failed round-trip).
|
|
320
338
|
doDebug &&
|
|
321
339
|
debugLog(
|
|
322
340
|
chalk.red(" WARNING SUBSCRIPTION "),
|
|
323
341
|
subscriptionsIds[i],
|
|
324
|
-
chalk.red(" SHOULD BE RECREATED")
|
|
342
|
+
chalk.red(" SHOULD BE RECREATED ("),
|
|
343
|
+
statusCode.toString(),
|
|
344
|
+
chalk.red(")")
|
|
325
345
|
);
|
|
326
346
|
|
|
327
347
|
subscriptionsToRecreate.push(subscriptionsIds[i]);
|