node-opcua-client 2.173.0 → 2.174.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.
@@ -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
@@ -162,6 +162,19 @@ function _ask_for_subscription_republish(session: ClientSessionImpl, callback: (
162
162
  return repair_client_session_by_recreating_a_new_session(session._client!, session, callback);
163
163
  }
164
164
 
165
+ // Republish has completed: resume normal Publish handling. During the connection break the
166
+ // in-flight PublishRequests have failed and the publish-request queue has drained, so we must
167
+ // replenish it now, otherwise the client would stop sending PublishRequests and never receive
168
+ // live notifications again. (https://github.com/node-opcua/node-opcua/issues/1524)
169
+ // Note: on the new-session repair path the engine is still suspended here, so this is a no-op
170
+ // there and the queue is replenished later by suspend(false).
171
+ // Guard on subscriptionCount: with no subscription, send_publish_request() would still emit
172
+ // spurious PublishRequests (it does not check the subscription count itself), adding useless
173
+ // round-trips on every reconnection of a subscription-less session.
174
+ if (engine.subscriptionCount > 0) {
175
+ engine.replenish_publish_request_queue();
176
+ }
177
+
165
178
  callback(err);
166
179
  });
167
180
  }