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.
@@ -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
- this._secureChannel?.endpointUrl +
737
- " securityMode = " +
738
- MessageSecurityMode[this.securityMode] +
739
- " securityPolicy = " +
740
- this.securityPolicy
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: this.computeClientSignature(this._secureChannel as ClientSecureChannelLayer, serverCertificate, serverNonce) || undefined,
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 === StatusCodes.Good) {
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
- this._secureChannel?.endpointUrl +
1156
- " securityMode = " +
1157
- MessageSecurityMode[this.securityMode] +
1158
- " securityPolicy = " +
1159
- this.securityPolicy
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
- const statusCode = err ? StatusCodes.Bad : response.responseHeader.serviceResult;
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
- if (statusCode.equals(StatusCodes.Good)) {
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(/|MessageNotAvailable/)) {
124
- // start engine and start monitoring
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
- _throwIfShouldNotContinue(session);
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
- _throwIfShouldNotContinue(session);
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.equals(StatusCodes.BadSubscriptionIdInvalid)) {
319
- // repair subscription
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]);