opcjs-client 0.1.40-alpha → 0.1.41-alpha

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { NodeId, StatusCodeToString, initLoggerProvider, getLogger, WebSocketFascade, WebSocketReadableStream, WebSocketWritableStream, SecureChannelContext, TcpMessageInjector, TcpConnectionHandler, TcpMessageDecoupler, SecureChannelMessageEncoder, SecureChannelTypeDecoder, SecureChannelMessageDecoder, SecureChannelTypeEncoder, SecureChannelChunkWriter, SecureChannelChunkReader, SecureChannelFacade, CallMethodRequest, Variant, BrowseDescription, BrowseDirectionEnum, BrowseResultMaskEnum, Configuration, LoggerFactory, Encoder, BinaryWriter, registerEncoders, Decoder, BinaryReader, registerTypeDecoders, registerBinaryDecoders, UserTokenTypeEnum, AnonymousIdentityToken, UserNameIdentityToken, IssuedIdentityToken, TimestampsToReturnEnum, ReadValueId, QualifiedName, ReadRequest, StatusCode, CallRequest, ViewDescription, BrowseRequest, BrowseNextRequest, SubscriptionAcknowledgement, ExpandedNodeId, CreateSubscriptionRequest, PublishRequest, MonitoringParameters, ExtensionObject, MonitoredItemCreateRequest, MonitoringModeEnum, CreateMonitoredItemsRequest, StatusCodeToStringNumber, RequestHeader, ApplicationDescription, LocalizedText, ApplicationTypeEnum, CreateSessionRequest, CreateSessionResponse, SignatureData, ActivateSessionRequest, CloseSessionRequest } from 'opcjs-base';
1
+ import { NodeId, StatusCodeToString, initLoggerProvider, getLogger, ServerStateEnum, WebSocketFascade, WebSocketReadableStream, WebSocketWritableStream, SecureChannelContext, TcpMessageInjector, TcpConnectionHandler, TcpMessageDecoupler, SecureChannelMessageEncoder, SecureChannelTypeDecoder, SecureChannelMessageDecoder, SecureChannelTypeEncoder, SecureChannelChunkWriter, SecureChannelChunkReader, SecureChannelFacade, CallMethodRequest, Variant, BrowseDescription, BrowseDirectionEnum, BrowseResultMaskEnum, Configuration, LoggerFactory, Encoder, BinaryWriter, registerEncoders, Decoder, BinaryReader, registerTypeDecoders, registerBinaryDecoders, UserTokenTypeEnum, AnonymousIdentityToken, UserNameIdentityToken, IssuedIdentityToken, TimestampsToReturnEnum, ReadValueId, QualifiedName, ReadRequest, StatusCode, CallRequest, ViewDescription, BrowseRequest, BrowseNextRequest, SubscriptionAcknowledgement, ExpandedNodeId, CreateSubscriptionRequest, PublishRequest, MonitoringParameters, ExtensionObject, MonitoredItemCreateRequest, MonitoringModeEnum, CreateMonitoredItemsRequest, StatusCodeToStringNumber, RequestHeader, ApplicationDescription, LocalizedText, ApplicationTypeEnum, CreateSessionRequest, CreateSessionResponse, SignatureData, ActivateSessionRequest, CancelRequest, CloseSessionRequest } from 'opcjs-base';
2
2
 
3
3
  // src/client.ts
4
4
  var SessionInvalidError = class extends Error {
@@ -11,6 +11,18 @@ var SessionInvalidError = class extends Error {
11
11
  };
12
12
 
13
13
  // src/services/serviceBase.ts
14
+ var requestHandleCounter = 0;
15
+ function nextRequestHandle() {
16
+ if (requestHandleCounter >= 2147483647) {
17
+ requestHandleCounter = 1;
18
+ } else {
19
+ requestHandleCounter++;
20
+ }
21
+ return requestHandleCounter;
22
+ }
23
+ function lastAssignedHandle() {
24
+ return requestHandleCounter;
25
+ }
14
26
  var ServiceBase = class {
15
27
  constructor(authToken, secureChannel) {
16
28
  this.authToken = authToken;
@@ -33,18 +45,43 @@ var ServiceBase = class {
33
45
  }
34
46
  throw new Error(`${context} failed: ${StatusCodeToString(result)} (${StatusCodeToStringNumber(result)})`);
35
47
  }
36
- createRequestHeader() {
48
+ /**
49
+ * Builds a RequestHeader for an outgoing service request.
50
+ *
51
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request from the
52
+ * server (OPC UA Part 4, §7.15). Use {@link ReturnDiagnosticsMask} constants
53
+ * to compose the value. Default `0` = no diagnostics.
54
+ */
55
+ createRequestHeader(returnDiagnostics = 0, preAllocatedHandle) {
37
56
  const requestHeader = new RequestHeader();
38
57
  requestHeader.authenticationToken = this.authToken;
39
58
  requestHeader.timestamp = /* @__PURE__ */ new Date();
40
- requestHeader.requestHandle = 0;
41
- requestHeader.returnDiagnostics = 0;
59
+ requestHeader.requestHandle = preAllocatedHandle ?? nextRequestHandle();
60
+ requestHeader.returnDiagnostics = returnDiagnostics;
42
61
  requestHeader.auditEntryId = "";
43
62
  requestHeader.timeoutHint = 6e4;
44
63
  requestHeader.additionalHeader = ExtensionObject.newEmpty();
45
64
  return requestHeader;
46
65
  }
47
66
  };
67
+ var CertificateRequiredError = class extends Error {
68
+ statusCode;
69
+ constructor(statusCode) {
70
+ super(
71
+ `Server requires a client certificate: ${StatusCodeToString(statusCode)} (0x${statusCode.toString(16).toUpperCase().padStart(8, "0")})`
72
+ );
73
+ this.name = "CertificateRequiredError";
74
+ this.statusCode = statusCode;
75
+ }
76
+ };
77
+ var CERTIFICATE_REQUIRED_STATUS_CODES = /* @__PURE__ */ new Set([
78
+ 2148663296,
79
+ // BadCertificateInvalid
80
+ 2148728832,
81
+ // BadSecurityChecksFailed
82
+ 2153316352
83
+ // BadNoValidCertificates
84
+ ]);
48
85
 
49
86
  // src/services/sessionService.ts
50
87
  var SessionService = class _SessionService extends ServiceBase {
@@ -55,9 +92,16 @@ var SessionService = class _SessionService extends ServiceBase {
55
92
  logger = getLogger("services.SessionService");
56
93
  /**
57
94
  * Creates a new session on the server (OPC UA Part 4, Section 5.7.2).
95
+ *
96
+ * @param clientCertificate - Optional DER-encoded client certificate to include in
97
+ * the request. Pass `null` (default) for SecurityPolicy None without a cert.
98
+ * When the server rejects a `null`-cert request with a certificate-related status
99
+ * code, the caller should retry with a `Uint8Array` certificate (OPC UA 1.0
100
+ * fallback — see `CertificateRequiredError`).
58
101
  * @returns The session ID, authentication token, and selected server endpoint.
102
+ * @throws {CertificateRequiredError} when the server demands a client certificate.
59
103
  */
60
- async createSession() {
104
+ async createSession(clientCertificate = null) {
61
105
  this.logger.debug("Creating session...");
62
106
  const clientDescription = new ApplicationDescription();
63
107
  clientDescription.applicationUri = this.configuration.applicationUri;
@@ -74,7 +118,7 @@ var SessionService = class _SessionService extends ServiceBase {
74
118
  request.endpointUrl = this.secureChannel.getEndpointUrl();
75
119
  request.sessionName = "";
76
120
  request.clientNonce = null;
77
- request.clientCertificate = null;
121
+ request.clientCertificate = clientCertificate ?? null;
78
122
  request.requestedSessionTimeout = 6e4;
79
123
  request.maxResponseMessageSize = 0;
80
124
  this.logger.debug("Sending CreateSessionRequest...");
@@ -88,6 +132,9 @@ var SessionService = class _SessionService extends ServiceBase {
88
132
  }
89
133
  const serviceResult = castedResponse.responseHeader?.serviceResult;
90
134
  if (serviceResult !== void 0 && serviceResult !== StatusCode.Good) {
135
+ if (CERTIFICATE_REQUIRED_STATUS_CODES.has(serviceResult)) {
136
+ throw new CertificateRequiredError(serviceResult);
137
+ }
91
138
  throw new Error(`CreateSessionRequest failed: ${StatusCodeToString(serviceResult)}`);
92
139
  }
93
140
  const clientConnectionUrl = new URL("opc." + this.secureChannel.getEndpointUrl());
@@ -137,6 +184,33 @@ var SessionService = class _SessionService extends ServiceBase {
137
184
  }
138
185
  this.logger.debug("Session activated.");
139
186
  }
187
+ /**
188
+ * Sends a CancelRequest to the server asking it to abandon the pending
189
+ * service request identified by `requestHandle` (OPC UA Part 4, Section 5.7.5).
190
+ *
191
+ * The server makes a best-effort attempt to cancel matching requests.
192
+ * Cancelled requests complete with a status of `BadRequestCancelledByClient`.
193
+ *
194
+ * @param requestHandle - The `requestHeader.requestHandle` value of the pending
195
+ * request to cancel. Pass `0` to attempt to cancel all outstanding requests
196
+ * (server behaviour for handle 0 is implementation-specific).
197
+ * @returns The number of pending requests that were actually cancelled
198
+ * (`CancelResponse.cancelCount`).
199
+ */
200
+ async cancel(requestHandle) {
201
+ this.logger.debug(`Sending CancelRequest for requestHandle ${requestHandle}...`);
202
+ const request = new CancelRequest();
203
+ request.requestHeader = this.createRequestHeader();
204
+ request.requestHandle = requestHandle;
205
+ const response = await this.secureChannel.issueServiceRequest(request);
206
+ const result = response?.responseHeader?.serviceResult;
207
+ if (result !== void 0 && result !== StatusCode.Good) {
208
+ throw new Error(`CancelRequest failed: ${StatusCodeToString(result)}`);
209
+ }
210
+ const cancelCount = response?.cancelCount ?? 0;
211
+ this.logger.debug(`CancelRequest completed; cancelCount = ${cancelCount}.`);
212
+ return cancelCount;
213
+ }
140
214
  /**
141
215
  * Closes the current session on the server (OPC UA Part 4, Section 5.7.4).
142
216
  * @param deleteSubscriptions - When true the server deletes all Subscriptions
@@ -222,6 +296,22 @@ var Session = class {
222
296
  getEndpoint() {
223
297
  return this.endpoint;
224
298
  }
299
+ /**
300
+ * Switches the active user identity for this session by calling ActivateSession with
301
+ * a new identity token (OPC UA Part 4, Section 5.7.3 — Session Client Impersonate
302
+ * conformance unit).
303
+ *
304
+ * The server re-evaluates authorisation for the session under the new identity.
305
+ * All existing Subscriptions and MonitoredItems are preserved; only the security
306
+ * context changes.
307
+ *
308
+ * @param identity - The new user identity to apply to the session.
309
+ * @throws When the server returns a non-Good ServiceResult (e.g. `BadIdentityTokenRejected`
310
+ * or `BadUserAccessDenied`).
311
+ */
312
+ async impersonate(identity) {
313
+ await this.activateSession(identity);
314
+ }
225
315
  /**
226
316
  * Closes the session on the server (OPC UA Part 4, Section 5.7.4).
227
317
  * @param deleteSubscriptions - When true the server deletes all Subscriptions
@@ -241,10 +331,30 @@ var SessionHandler = class {
241
331
  sessionServices;
242
332
  logger = getLogger("sessions.SessionHandler");
243
333
  async createNewSession(identity) {
244
- const ret = await this.sessionServices.createSession();
245
- this.sessionServices = this.sessionServices.recreate(ret.authToken);
246
- const session = new Session(ret.sessionId, ret.authToken, ret.endpoint, this.sessionServices);
247
- this.validateUserTokenPolicy(identity, ret.endpoint);
334
+ let sessionResult;
335
+ try {
336
+ sessionResult = await this.sessionServices.createSession(null);
337
+ } catch (err) {
338
+ if (err instanceof CertificateRequiredError) {
339
+ const fallbackCert = this.configuration.securityConfiguration?.applicationInstanceCertificate;
340
+ if (fallbackCert) {
341
+ this.logger.info(
342
+ "Server requires a client certificate (OPC UA 1.0 fallback); retrying CreateSession with applicationInstanceCertificate."
343
+ );
344
+ sessionResult = await this.sessionServices.createSession(fallbackCert);
345
+ } else {
346
+ this.logger.warn(
347
+ "Server requires a client certificate but no applicationInstanceCertificate is configured in securityConfiguration. Cannot complete the 1.0 fallback."
348
+ );
349
+ throw err;
350
+ }
351
+ } else {
352
+ throw err;
353
+ }
354
+ }
355
+ this.sessionServices = this.sessionServices.recreate(sessionResult.authToken);
356
+ const session = new Session(sessionResult.sessionId, sessionResult.authToken, sessionResult.endpoint, this.sessionServices);
357
+ this.validateUserTokenPolicy(identity, sessionResult.endpoint);
248
358
  await session.activateSession(identity);
249
359
  return session;
250
360
  }
@@ -276,6 +386,17 @@ var SessionHandler = class {
276
386
  async closeSession(deleteSubscriptions = true) {
277
387
  await this.sessionServices.closeSession(deleteSubscriptions);
278
388
  }
389
+ /**
390
+ * Sends a CancelRequest to the server to abandon a pending service call
391
+ * (OPC UA Part 4, Section 5.7.5).
392
+ *
393
+ * @param requestHandle - The `requestHandle` from the `RequestHeader` of the
394
+ * pending request to cancel.
395
+ * @returns The number of requests the server actually cancelled.
396
+ */
397
+ async cancel(requestHandle) {
398
+ return this.sessionServices.cancel(requestHandle);
399
+ }
279
400
  /**
280
401
  * Validates the requested user-identity token type against:
281
402
  * 1. The `allowedUserTokenTypes` from the client security configuration — the
@@ -318,9 +439,10 @@ var AttributeService = class extends ServiceBase {
318
439
  * @param nodeIds - NodeIds of the Nodes to read.
319
440
  * @param maxAge - Maximum age of the cached value in milliseconds the server may return. 0 = always current value.
320
441
  * @param timestampsToReturn - Which timestamps to include in results. Default: Source.
321
- * @returns Array of results containing value and raw status code number, one per requested NodeId.
442
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
443
+ * @returns Array of results containing value, raw status code, and optional diagnostic info, one per requested NodeId.
322
444
  */
323
- async ReadValue(nodeIds, maxAge = 0, timestampsToReturn = TimestampsToReturnEnum.Source) {
445
+ async ReadValue(nodeIds, maxAge = 0, timestampsToReturn = TimestampsToReturnEnum.Source, returnDiagnostics = 0, requestHandle) {
324
446
  const readValueIds = nodeIds.map((ni) => {
325
447
  const readValueId = new ReadValueId();
326
448
  readValueId.nodeId = ni;
@@ -330,7 +452,7 @@ var AttributeService = class extends ServiceBase {
330
452
  return readValueId;
331
453
  });
332
454
  const request = new ReadRequest();
333
- request.requestHeader = this.createRequestHeader();
455
+ request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
334
456
  request.maxAge = maxAge;
335
457
  request.timestampsToReturn = timestampsToReturn;
336
458
  request.nodesToRead = readValueIds;
@@ -338,10 +460,13 @@ var AttributeService = class extends ServiceBase {
338
460
  const response = await this.secureChannel.issueServiceRequest(request);
339
461
  this.checkServiceResult(response.responseHeader?.serviceResult, "ReadRequest");
340
462
  const results = new Array();
341
- for (const dataValue of response.results ?? []) {
463
+ const diagInfos = response.diagnosticInfos ?? [];
464
+ for (let i = 0; i < (response.results ?? []).length; i++) {
465
+ const dataValue = response.results[i];
342
466
  results.push({
343
467
  statusCode: dataValue.statusCode ?? StatusCode.Good,
344
- value: dataValue.value
468
+ value: dataValue.value,
469
+ diagnosticInfo: diagInfos[i]
345
470
  });
346
471
  }
347
472
  return results;
@@ -353,9 +478,10 @@ var AttributeService = class extends ServiceBase {
353
478
 
354
479
  // src/readValueResult.ts
355
480
  var ReadValueResult = class {
356
- constructor(value, statusCode) {
481
+ constructor(value, statusCode, diagnosticInfo) {
357
482
  this.value = value;
358
483
  this.statusCode = statusCode;
484
+ this.diagnosticInfo = diagnosticInfo;
359
485
  }
360
486
  };
361
487
 
@@ -383,6 +509,15 @@ var SubscriptionHandler = class {
383
509
  isRunning = false;
384
510
  /** Guards against multiple concurrent publish loops. */
385
511
  publishInFlight = false;
512
+ /**
513
+ * Optional callback invoked when the server announces a shutdown via a
514
+ * `StatusChangeNotification` with status `BadShutdown` or `BadServerHalted`
515
+ * (OPC UA Part 4, §5.13.6.2 — Session Client Detect Shutdown).
516
+ *
517
+ * Assign this before calling `subscribe()`. The client sets it automatically
518
+ * in `initServices()` to trigger a reconnect.
519
+ */
520
+ onShutdown;
386
521
  /** Returns true when at least one subscription is active and the publish loop is running. */
387
522
  hasActiveSubscription() {
388
523
  return this.isRunning && this.entries.length > 0;
@@ -447,6 +582,11 @@ var SubscriptionHandler = class {
447
582
  `Subscription ${subscriptionId} status changed: 0x${statusChange.status?.toString(16).toUpperCase()}`
448
583
  );
449
584
  this.isRunning = false;
585
+ const status = statusChange.status;
586
+ if (status === StatusCode.BadShutdown || status === StatusCode.BadServerHalted) {
587
+ this.logger.warn("Server shutdown announced via StatusChangeNotification \u2014 triggering reconnect.");
588
+ this.onShutdown?.();
589
+ }
450
590
  return;
451
591
  } else {
452
592
  this.logger.warn(
@@ -564,18 +704,22 @@ var MethodService = class extends ServiceBase {
564
704
  /**
565
705
  * Calls one or more methods on the server (OPC UA Part 4, Section 5.11.2).
566
706
  * @param methodsToCall - Array of CallMethodRequest describing each method to invoke.
567
- * @returns Array of results containing output argument values and raw status code number, one per requested method.
707
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
708
+ * @returns Array of results containing output argument values, raw status code, and optional diagnostic info,
709
+ * one per requested method.
568
710
  */
569
- async call(methodsToCall) {
711
+ async call(methodsToCall, returnDiagnostics = 0, requestHandle) {
570
712
  const request = new CallRequest();
571
- request.requestHeader = this.createRequestHeader();
713
+ request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
572
714
  request.methodsToCall = methodsToCall;
573
715
  this.logger.debug("Sending CallRequest...");
574
716
  const response = await this.secureChannel.issueServiceRequest(request);
575
717
  this.checkServiceResult(response.responseHeader?.serviceResult, "CallRequest");
576
- return response.results.map((result) => ({
718
+ const diagInfos = response.diagnosticInfos ?? [];
719
+ return response.results.map((result, i) => ({
577
720
  statusCode: result.statusCode ?? StatusCode.Good,
578
- value: result.outputArguments.map((arg) => arg.value)
721
+ value: result.outputArguments.map((arg) => arg.value),
722
+ diagnosticInfo: diagInfos[i]
579
723
  }));
580
724
  }
581
725
  constructor(authToken, secureChannel) {
@@ -585,9 +729,10 @@ var MethodService = class extends ServiceBase {
585
729
 
586
730
  // src/method/callMethodResult.ts
587
731
  var CallMethodResult = class {
588
- constructor(values, statusCode) {
732
+ constructor(values, statusCode, diagnosticInfo) {
589
733
  this.values = values;
590
734
  this.statusCode = statusCode;
735
+ this.diagnosticInfo = diagnosticInfo;
591
736
  }
592
737
  };
593
738
  var BrowseService = class extends ServiceBase {
@@ -595,15 +740,16 @@ var BrowseService = class extends ServiceBase {
595
740
  /**
596
741
  * Browses one or more Nodes and returns their References (OPC UA Part 4, Section 5.9.2).
597
742
  * @param nodesToBrowse - Array of BrowseDescriptions specifying nodes and filter criteria.
743
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
598
744
  * @returns Array of BrowseResult, one per requested node.
599
745
  */
600
- async browse(nodesToBrowse) {
746
+ async browse(nodesToBrowse, returnDiagnostics = 0, requestHandle) {
601
747
  const view = new ViewDescription();
602
748
  view.viewId = NodeId.newNumeric(0, 0);
603
749
  view.timestamp = /* @__PURE__ */ new Date(-116444736e5);
604
750
  view.viewVersion = 0;
605
751
  const request = new BrowseRequest();
606
- request.requestHeader = this.createRequestHeader();
752
+ request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
607
753
  request.view = view;
608
754
  request.requestedMaxReferencesPerNode = 0;
609
755
  request.nodesToBrowse = nodesToBrowse;
@@ -616,11 +762,12 @@ var BrowseService = class extends ServiceBase {
616
762
  * Continues a Browse operation using continuation points (OPC UA Part 4, Section 5.9.3).
617
763
  * @param continuationPoints - Continuation points returned by a prior Browse or BrowseNext call.
618
764
  * @param releaseContinuationPoints - If true, releases the continuation points without returning results.
765
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
619
766
  * @returns Array of BrowseResult, one per continuation point.
620
767
  */
621
- async browseNext(continuationPoints, releaseContinuationPoints) {
768
+ async browseNext(continuationPoints, releaseContinuationPoints, returnDiagnostics = 0) {
622
769
  const request = new BrowseNextRequest();
623
- request.requestHeader = this.createRequestHeader();
770
+ request.requestHeader = this.createRequestHeader(returnDiagnostics);
624
771
  request.releaseContinuationPoints = releaseContinuationPoints;
625
772
  request.continuationPoints = continuationPoints;
626
773
  this.logger.debug("Sending BrowseNextRequest...");
@@ -645,10 +792,67 @@ var BrowseNodeResult = class {
645
792
  this.typeDefinition = typeDefinition;
646
793
  }
647
794
  };
795
+ var NamespaceTable = class {
796
+ uris;
797
+ constructor(uris = []) {
798
+ this.uris = Object.freeze([...uris]);
799
+ }
800
+ /** Returns the namespace URI at the given index, or `undefined` if out of range. */
801
+ getUri(index) {
802
+ return this.uris[index];
803
+ }
804
+ /** Returns the namespace index for the given URI, or `undefined` if not found. */
805
+ getIndex(uri) {
806
+ const idx = this.uris.indexOf(uri);
807
+ return idx >= 0 ? idx : void 0;
808
+ }
809
+ /** Returns the full ordered array of namespace URIs. */
810
+ getUris() {
811
+ return this.uris;
812
+ }
813
+ /** Returns `true` when both tables contain the same URIs in the same order. */
814
+ equals(other) {
815
+ if (this.uris.length !== other.uris.length) return false;
816
+ return this.uris.every((uri, i) => uri === other.uris[i]);
817
+ }
818
+ /**
819
+ * Remaps the namespace index of `nodeId` from this table to the equivalent
820
+ * index in `newTable` by matching namespace URIs.
821
+ *
822
+ * - Namespace index `0` (OPC UA base namespace) is always returned unchanged.
823
+ * - Returns the **same** `NodeId` instance when the index has not changed.
824
+ * - Returns a **new** `NodeId` instance with the updated index when remapping
825
+ * is required.
826
+ *
827
+ * @throws {Error} When the namespace URI at `nodeId.namespace` is not present
828
+ * in this (old) table or when the URI is not present in `newTable`.
829
+ */
830
+ remapNodeId(nodeId, newTable) {
831
+ if (nodeId.namespace === 0) return nodeId;
832
+ const uri = this.getUri(nodeId.namespace);
833
+ if (uri === void 0) {
834
+ throw new Error(
835
+ `Cannot remap ${nodeId.toString()}: namespace index ${nodeId.namespace} is not present in the old NamespaceTable (length ${this.uris.length}).`
836
+ );
837
+ }
838
+ const newIndex = newTable.getIndex(uri);
839
+ if (newIndex === void 0) {
840
+ throw new Error(
841
+ `Cannot remap ${nodeId.toString()}: namespace URI '${uri}' is not present in the new NamespaceTable.`
842
+ );
843
+ }
844
+ if (newIndex === nodeId.namespace) return nodeId;
845
+ return new NodeId(newIndex, nodeId.identifier);
846
+ }
847
+ };
648
848
 
649
849
  // src/client.ts
650
850
  var SERVER_STATUS_NODE_ID = NodeId.newNumeric(0, 2256);
851
+ var NAMESPACE_ARRAY_NODE_ID = NodeId.newNumeric(0, 2255);
852
+ var ESTIMATED_RETURN_TIME_NODE_ID = NodeId.newNumeric(0, 2992);
853
+ var HAS_PROPERTY_REF_TYPE_ID = NodeId.newNumeric(0, 46);
651
854
  var KEEP_ALIVE_INTERVAL_MS = 25e3;
855
+ var OPC_UA_MIN_DATE_TIME_MS = -116444736e5;
652
856
  var Client = class {
653
857
  constructor(endpointUrl, configuration, identity) {
654
858
  this.configuration = configuration;
@@ -670,6 +874,40 @@ var Client = class {
670
874
  ws;
671
875
  sessionHandler;
672
876
  keepAliveTimer;
877
+ /** Set to true while a shutdown-triggered reconnect is pending to avoid duplicate attempts. */
878
+ shutdownReconnectPending = false;
879
+ /** Most recently read NamespaceArray from the server (Session Client Renew NodeIds). */
880
+ namespaceTable;
881
+ /**
882
+ * Called whenever the server's NamespaceArray changes after a session (re-)establishment
883
+ * (OPC UA Part 4, Section 5.7.1 — Session Client Renew NodeIds conformance unit).
884
+ *
885
+ * Use `oldTable.remapNodeId(nodeId, newTable)` to recalculate cached NodeIds.
886
+ *
887
+ * @example
888
+ * ```ts
889
+ * client.onNamespaceTableChanged = (oldTable, newTable) => {
890
+ * cachedNodeId = oldTable.remapNodeId(cachedNodeId, newTable)
891
+ * }
892
+ * ```
893
+ */
894
+ onNamespaceTableChanged;
895
+ /**
896
+ * Called when the server sends `EstimatedReturnTime = MinDateTime`, indicating it does not
897
+ * expect to restart (OPC UA Part 5, Section 12.6 — Base Info Client Estimated Return Time
898
+ * conformance unit).
899
+ *
900
+ * When this fires the automatic reconnect is suppressed. The application is responsible for
901
+ * deciding whether to keep the `Client` instance or dispose it.
902
+ *
903
+ * @example
904
+ * ```ts
905
+ * client.onPermanentShutdown = () => {
906
+ * console.warn('Server will not restart — closing client.')
907
+ * }
908
+ * ```
909
+ */
910
+ onPermanentShutdown;
673
911
  getSession() {
674
912
  if (!this.session) {
675
913
  throw new Error("No session available");
@@ -690,6 +928,47 @@ var Client = class {
690
928
  new SubscriptionService(authToken, sc),
691
929
  new MonitoredItemService(authToken, sc)
692
930
  );
931
+ this.subscriptionHandler.onShutdown = () => this.handleServerShutdownDetected();
932
+ void this.refreshNamespaceTable();
933
+ }
934
+ /**
935
+ * Reads `Server.NamespaceArray` (ns=0, i=2255) and updates the stored
936
+ * `NamespaceTable`. When the table changes compared to the previous read the
937
+ * `onNamespaceTableChanged` callback is fired so the application can remap
938
+ * any cached NodeIds (OPC UA Part 4, Section 5.7.1 — Session Client Renew
939
+ * NodeIds conformance unit).
940
+ *
941
+ * Errors are logged as warnings and do not propagate to the caller.
942
+ */
943
+ async refreshNamespaceTable() {
944
+ if (!this.attributeService) return;
945
+ try {
946
+ const results = await this.attributeService.ReadValue([NAMESPACE_ARRAY_NODE_ID]);
947
+ const variant = results[0]?.value;
948
+ const uris = variant?.value;
949
+ if (!Array.isArray(uris)) {
950
+ this.logger.warn("NamespaceArray read returned an unexpected value; skipping table update.");
951
+ return;
952
+ }
953
+ const newTable = new NamespaceTable(uris);
954
+ const oldTable = this.namespaceTable;
955
+ this.namespaceTable = newTable;
956
+ if (oldTable !== void 0 && !oldTable.equals(newTable)) {
957
+ this.logger.info("NamespaceArray changed; notifying application to renew NodeIds.");
958
+ this.onNamespaceTableChanged?.(oldTable, newTable);
959
+ }
960
+ } catch (err) {
961
+ this.logger.warn("NamespaceArray read failed; namespace table not updated:", err);
962
+ }
963
+ }
964
+ /**
965
+ * Returns the most recently read `NamespaceTable` for this session.
966
+ *
967
+ * Available after `connect()` completes (the table is read as part of session
968
+ * establishment). Returns `undefined` before the first successful read.
969
+ */
970
+ getNamespaceTable() {
971
+ return this.namespaceTable;
693
972
  }
694
973
  /**
695
974
  * Executes `fn` and, if it throws a `SessionInvalidError`, creates a fresh
@@ -731,6 +1010,11 @@ var Client = class {
731
1010
  * Starts a periodic keep-alive timer that reads Server_ServerStatus when no subscription is
732
1011
  * active. OPC UA Part 4, Section 5.7.1 requires clients to keep the session alive; when no
733
1012
  * subscription Publish loop is running this is the only mechanism that does so.
1013
+ *
1014
+ * The keep-alive read also serves as the **Detect Shutdown** mechanism (Session Client Detect
1015
+ * Shutdown conformance unit): when the returned `ServerStatusDataType.state` equals
1016
+ * `ServerStateEnum.Shutdown` the client schedules a reconnect after
1017
+ * `SHUTDOWN_RECONNECT_DELAY_MS` to let the server finish its shutdown sequence.
734
1018
  */
735
1019
  startKeepAlive() {
736
1020
  this.keepAliveTimer = setInterval(() => {
@@ -738,7 +1022,12 @@ var Client = class {
738
1022
  return;
739
1023
  }
740
1024
  if (this.attributeService) {
741
- void this.attributeService.ReadValue([SERVER_STATUS_NODE_ID]).catch((err) => {
1025
+ void this.attributeService.ReadValue([SERVER_STATUS_NODE_ID]).then((results) => {
1026
+ const statusData = results[0]?.value;
1027
+ if (statusData?.state === ServerStateEnum.Shutdown) {
1028
+ this.handleServerShutdownDetected();
1029
+ }
1030
+ }).catch((err) => {
742
1031
  this.logger.warn("Keep-alive read failed:", err);
743
1032
  });
744
1033
  }
@@ -748,6 +1037,87 @@ var Client = class {
748
1037
  clearInterval(this.keepAliveTimer);
749
1038
  this.keepAliveTimer = void 0;
750
1039
  }
1040
+ /**
1041
+ * Called when a server-shutdown announcement is detected — either via the keep-alive read
1042
+ * returning `ServerStateEnum.Shutdown` or via a subscription `StatusChangeNotification`
1043
+ * with status `BadShutdown` / `BadServerHalted`.
1044
+ *
1045
+ * Reads `Server/ServerStatus/EstimatedReturnTime` (ns=0, i=2992) to decide how long to
1046
+ * wait before reconnecting (Base Info Client Estimated Return Time conformance unit).
1047
+ * Falls back to `configuration.shutdownReconnectDelayMs` when the read fails or the
1048
+ * attributed service is not yet available. Fires `onPermanentShutdown` and suppresses the
1049
+ * reconnect when the server sends `MinDateTime`.
1050
+ *
1051
+ * Only one reconnect attempt is scheduled at a time; a second detection while one is already
1052
+ * pending is silently ignored.
1053
+ */
1054
+ handleServerShutdownDetected() {
1055
+ if (this.shutdownReconnectPending) {
1056
+ return;
1057
+ }
1058
+ this.shutdownReconnectPending = true;
1059
+ this.stopKeepAlive();
1060
+ this.logger.warn("Server shutdown detected; reading EstimatedReturnTime...");
1061
+ void this.computeReconnectDelayMs().then((delayMs) => {
1062
+ if (delayMs === null) {
1063
+ this.logger.warn(
1064
+ "Server indicated it will not restart (MinDateTime). Firing onPermanentShutdown."
1065
+ );
1066
+ this.onPermanentShutdown?.();
1067
+ this.shutdownReconnectPending = false;
1068
+ return;
1069
+ }
1070
+ this.logger.warn(`Scheduling reconnect in ${delayMs} ms...`);
1071
+ setTimeout(() => {
1072
+ void this.reconnectAndReactivate().then(() => {
1073
+ this.initServices();
1074
+ this.startKeepAlive();
1075
+ this.logger.info("Reconnected after server shutdown.");
1076
+ }).catch((err) => {
1077
+ this.logger.warn("Reconnect after server shutdown failed:", err);
1078
+ }).finally(() => {
1079
+ this.shutdownReconnectPending = false;
1080
+ });
1081
+ }, delayMs);
1082
+ });
1083
+ }
1084
+ /**
1085
+ * Reads `Server/ServerStatus/EstimatedReturnTime` (ns=0, i=2992) and returns the reconnect
1086
+ * delay in milliseconds (Base Info Client Estimated Return Time — OPC UA Part 5, §12.6):
1087
+ *
1088
+ * - Valid future `DateTime` → delay = `estimatedReturnTime − now`, clamped to at least
1089
+ * `MIN_RECONNECT_DELAY_MS`.
1090
+ * - Past `DateTime` (server should already be available) → `MIN_RECONNECT_DELAY_MS`.
1091
+ * - OPC UA `MinDateTime` (server will not restart) → `null`.
1092
+ * - Unreadable / unavailable → falls back to `configuration.shutdownReconnectDelayMs`.
1093
+ */
1094
+ async computeReconnectDelayMs() {
1095
+ if (!this.attributeService) {
1096
+ return this.configuration.shutdownReconnectDelayMs;
1097
+ }
1098
+ try {
1099
+ const results = await this.attributeService.ReadValue([ESTIMATED_RETURN_TIME_NODE_ID]);
1100
+ const variant = results[0]?.value;
1101
+ const estimatedReturnTime = variant?.value;
1102
+ if (estimatedReturnTime instanceof Date && !isNaN(estimatedReturnTime.getTime())) {
1103
+ if (estimatedReturnTime.getTime() <= OPC_UA_MIN_DATE_TIME_MS) {
1104
+ return null;
1105
+ }
1106
+ const delayMs = estimatedReturnTime.getTime() - Date.now();
1107
+ if (delayMs > 0) {
1108
+ this.logger.info(
1109
+ `EstimatedReturnTime: ${estimatedReturnTime.toISOString()} (reconnect in ${delayMs} ms).`
1110
+ );
1111
+ return delayMs;
1112
+ }
1113
+ this.logger.info("EstimatedReturnTime is in the past; reconnecting immediately.");
1114
+ return this.configuration.minReconnectDelayMs;
1115
+ }
1116
+ } catch (err) {
1117
+ this.logger.debug("Failed to read EstimatedReturnTime; using configured delay:", err);
1118
+ }
1119
+ return this.configuration.shutdownReconnectDelayMs;
1120
+ }
751
1121
  async connect() {
752
1122
  const { ws, sc } = await this.openTransportAndChannel();
753
1123
  this.secureChannel = sc;
@@ -912,37 +1282,78 @@ var Client = class {
912
1282
  this.ws = void 0;
913
1283
  this.logger.info("Disconnected.");
914
1284
  }
915
- async read(ids) {
916
- return this.withSessionRefresh(async () => {
917
- const result = await this.attributeService?.ReadValue(ids);
918
- return result?.map((r) => new ReadValueResult(r.value, r.statusCode)) ?? [];
1285
+ /**
1286
+ * Reads the Value attribute of one or more Nodes.
1287
+ *
1288
+ * The returned object is a `Promise` that also exposes `requestHandle` — the
1289
+ * OPC UA `requestHandle` assigned to the underlying `ReadRequest`. The handle
1290
+ * is available synchronously (before `await`) so it can be passed to
1291
+ * `cancel()` to abort the in-flight request.
1292
+ *
1293
+ * @example
1294
+ * ```ts
1295
+ * const req = client.read([nodeId])
1296
+ * await client.cancel(req.requestHandle) // abort before response
1297
+ * const results = await req // ReadValueResult[]
1298
+ * ```
1299
+ */
1300
+ read(ids, options) {
1301
+ const requestHandle = nextRequestHandle();
1302
+ const promise = this.withSessionRefresh(async () => {
1303
+ const result = await this.attributeService?.ReadValue(ids, 0, void 0, options?.returnDiagnostics, requestHandle);
1304
+ return result?.map((r) => new ReadValueResult(r.value, r.statusCode, r.diagnosticInfo)) ?? [];
919
1305
  });
1306
+ return Object.assign(promise, { requestHandle });
920
1307
  }
921
1308
  /**
922
1309
  * Method for calling a single method on the server.
1310
+ *
1311
+ * The returned object is a `Promise` that also exposes `requestHandle` — the
1312
+ * OPC UA `requestHandle` assigned to the underlying `CallRequest`. The handle
1313
+ * is available synchronously (before `await`) so it can be passed to
1314
+ * `cancel()` to abort the in-flight request.
1315
+ *
923
1316
  * @param objectId - NodeId of the Object that owns the method.
924
1317
  * @param methodId - NodeId of the Method to invoke.
925
1318
  * @param inputArguments - Input argument Variants (default: empty).
926
- * @returns The CallMethodResult for the invoked method.
1319
+ * @param options - Request options (e.g. `returnDiagnostics`).
1320
+ * @returns A promise resolving to the CallMethodResult, with `requestHandle` available synchronously.
927
1321
  */
928
- async callMethod(objectId, methodId, inputArguments = []) {
929
- return this.withSessionRefresh(async () => {
1322
+ callMethod(objectId, methodId, inputArguments = [], options) {
1323
+ const requestHandle = nextRequestHandle();
1324
+ const promise = this.withSessionRefresh(async () => {
930
1325
  const request = new CallMethodRequest();
931
1326
  request.objectId = objectId;
932
1327
  request.methodId = methodId;
933
1328
  request.inputArguments = inputArguments.map((arg) => Variant.newFrom(arg));
934
- const responses = await this.methodService.call([request]);
1329
+ const responses = await this.methodService.call([request], options?.returnDiagnostics, requestHandle);
935
1330
  const response = responses[0];
936
- return new CallMethodResult(response.value, response.statusCode);
1331
+ return new CallMethodResult(response.value, response.statusCode, response.diagnosticInfo);
937
1332
  });
1333
+ return Object.assign(promise, { requestHandle });
938
1334
  }
939
- async browse(nodeId, recursive = false) {
940
- return this.withSessionRefresh(() => {
1335
+ /**
1336
+ * Browses the Address Space starting from `nodeId`.
1337
+ *
1338
+ * The returned object is a `Promise` that also exposes `requestHandle` — the
1339
+ * OPC UA `requestHandle` assigned to the initial `BrowseRequest`. The handle
1340
+ * is available synchronously (before `await`) so it can be passed to
1341
+ * `cancel()` to abort the in-flight request.
1342
+ *
1343
+ * @param nodeId - Starting node.
1344
+ * @param recursive - When true, recursively follows HierarchicalReferences.
1345
+ * @param options - Request options (e.g. `returnDiagnostics`).
1346
+ * @returns A promise resolving to the list of referenced nodes, with `requestHandle` available synchronously.
1347
+ */
1348
+ browse(nodeId, recursive = false, options) {
1349
+ const requestHandle = nextRequestHandle();
1350
+ const promise = this.withSessionRefresh(() => {
941
1351
  const visited = /* @__PURE__ */ new Set();
942
- return this.browseRecursive(nodeId, recursive, visited);
1352
+ return this.browseRecursive(nodeId, recursive, visited, options?.returnDiagnostics ?? 0, requestHandle);
943
1353
  });
1354
+ return Object.assign(promise, { requestHandle });
944
1355
  }
945
- async browseRecursive(nodeId, recursive, visited) {
1356
+ async browseRecursive(nodeId, recursive, visited, returnDiagnostics, requestHandle) {
946
1357
  const nodeKey = `${nodeId.namespace}:${nodeId.identifier}`;
947
1358
  if (visited.has(nodeKey)) {
948
1359
  return [];
@@ -955,12 +1366,12 @@ var Client = class {
955
1366
  description.includeSubtypes = true;
956
1367
  description.nodeClassMask = 0;
957
1368
  description.resultMask = BrowseResultMaskEnum.All;
958
- const browseResults = await this.browseService.browse([description]);
1369
+ const browseResults = await this.browseService.browse([description], returnDiagnostics, requestHandle);
959
1370
  const browseResult = browseResults[0];
960
1371
  const allReferences = [...browseResult.references ?? []];
961
1372
  let continuationPoint = browseResult.continuationPoint;
962
1373
  while (continuationPoint && continuationPoint.byteLength > 0) {
963
- const nextResults = await this.browseService.browseNext([continuationPoint], false);
1374
+ const nextResults = await this.browseService.browseNext([continuationPoint], false, returnDiagnostics);
964
1375
  const nextResult = nextResults[0];
965
1376
  allReferences.push(...nextResult.references ?? []);
966
1377
  continuationPoint = nextResult.continuationPoint;
@@ -985,7 +1396,8 @@ var Client = class {
985
1396
  const childResults = await this.browseRecursive(
986
1397
  childNodeId,
987
1398
  true,
988
- visited
1399
+ visited,
1400
+ returnDiagnostics
989
1401
  );
990
1402
  results.push(...childResults);
991
1403
  }
@@ -995,6 +1407,170 @@ var Client = class {
995
1407
  async subscribe(ids, callback, options) {
996
1408
  this.subscriptionHandler?.subscribe(ids, callback, options);
997
1409
  }
1410
+ /**
1411
+ * Asks the server to cancel a pending service request
1412
+ * (OPC UA Part 4, Section 5.7.5 — Session Client Cancel conformance unit).
1413
+ *
1414
+ * The `requestHandle` uniquely identifies the pending request. It is the value
1415
+ * assigned to `RequestHeader.requestHandle` when the request was initially sent.
1416
+ * Service calls made through this client automatically assign monotonically
1417
+ * increasing handles, so the caller can capture the handle before or after issuing
1418
+ * Each method (`read`, `browse`, `callMethod`) returns a `Promise` with a
1419
+ * `requestHandle` property that is available synchronously. Pass that handle
1420
+ * here to abort the corresponding in-flight request.
1421
+ *
1422
+ * The server makes a best-effort attempt to cancel the matching request. Cancelled
1423
+ * requests complete with status `BadRequestCancelledByClient`. Not all servers
1424
+ * guarantee that a request in flight can be cancelled.
1425
+ *
1426
+ * @param requestHandle - Handle of the pending request to cancel.
1427
+ * @returns The number of pending requests the server actually cancelled.
1428
+ * @throws If no session is active or the server returns a non-Good status.
1429
+ *
1430
+ * @example
1431
+ * ```ts
1432
+ * // Issue a potentially slow operation and immediately cancel it.
1433
+ * const req = client.read([nodeId])
1434
+ * const cancelled = await client.cancel(req.requestHandle)
1435
+ * console.log(`Cancelled ${cancelled} request(s)`)
1436
+ * const results = await req // resolves with BadRequestCancelledByClient
1437
+ * ```
1438
+ */
1439
+ async cancel(requestHandle) {
1440
+ if (!this.sessionHandler) {
1441
+ throw new Error("Not connected: call connect() before cancel()");
1442
+ }
1443
+ return this.sessionHandler.cancel(requestHandle);
1444
+ }
1445
+ /**
1446
+ * Switches the active user identity for the current session by calling ActivateSession
1447
+ * with a new identity token (OPC UA Part 4, Section 5.7.3 — Session Client Impersonate
1448
+ * conformance unit).
1449
+ *
1450
+ * The server re-evaluates authorisation under the new identity while keeping all existing
1451
+ * Subscriptions and MonitoredItems intact. The new identity is also stored so that any
1452
+ * subsequent auto-reconnect or session refresh uses it instead of the original identity.
1453
+ *
1454
+ * @param identity - The new user identity to apply to the session.
1455
+ * @throws {Error} When not connected (call `connect()` first).
1456
+ * @throws {Error} When the server rejects the identity (e.g. `BadIdentityTokenRejected`
1457
+ * or `BadUserAccessDenied`).
1458
+ *
1459
+ * @example
1460
+ * ```ts
1461
+ * await client.connect()
1462
+ * // ... work as the original user ...
1463
+ * await client.impersonate(UserIdentity.newWithUserName('admin', 'secret'))
1464
+ * // ... subsequent calls run under the admin identity ...
1465
+ * ```
1466
+ */
1467
+ async impersonate(identity) {
1468
+ if (!this.session) {
1469
+ throw new Error("Not connected: call connect() before impersonate()");
1470
+ }
1471
+ await this.session.impersonate(identity);
1472
+ this.identity = identity;
1473
+ }
1474
+ /**
1475
+ * Reads the `SelectionListType` metadata for a Variable
1476
+ * (OPC UA Part 5, §7.18 — Base Info Client Selection List conformance unit).
1477
+ *
1478
+ * The client browses the node's `HasProperty` references for `Selections`,
1479
+ * `SelectionDescriptions`, and `RestrictToList`, then reads their values in a
1480
+ * single batch Read request.
1481
+ *
1482
+ * Works transparently for instances of `SelectionListType` (ns=0; i=19726) and
1483
+ * any of its subtypes, because all subtypes inherit the `Selections` mandatory
1484
+ * property.
1485
+ *
1486
+ * @param nodeId - NodeId of the Variable to inspect.
1487
+ * @returns `SelectionList` when the node has a `Selections` property, `null` otherwise.
1488
+ * @throws When not connected or if the server returns a non-Good service status.
1489
+ *
1490
+ * @example
1491
+ * ```ts
1492
+ * const list = await client.getSelectionList(nodeId)
1493
+ * if (list) {
1494
+ * list.selectionDescriptions.forEach((desc, i) =>
1495
+ * console.log(`[${i}] ${desc.text}:`, list.selections[i])
1496
+ * )
1497
+ * }
1498
+ * ```
1499
+ */
1500
+ getSelectionList(nodeId) {
1501
+ return this.withSessionRefresh(() => this.fetchSelectionList(nodeId));
1502
+ }
1503
+ /**
1504
+ * Internal implementation of `getSelectionList`. Browses the node's
1505
+ * HasProperty references to locate Selections/SelectionDescriptions/RestrictToList,
1506
+ * then batch-reads their values.
1507
+ */
1508
+ async fetchSelectionList(nodeId) {
1509
+ if (!this.browseService || !this.attributeService) {
1510
+ throw new Error("Not connected: call connect() before getSelectionList()");
1511
+ }
1512
+ const description = new BrowseDescription();
1513
+ description.nodeId = nodeId;
1514
+ description.browseDirection = BrowseDirectionEnum.Forward;
1515
+ description.referenceTypeId = HAS_PROPERTY_REF_TYPE_ID;
1516
+ description.includeSubtypes = false;
1517
+ description.nodeClassMask = 0;
1518
+ description.resultMask = BrowseResultMaskEnum.All;
1519
+ const browseResults = await this.browseService.browse([description]);
1520
+ const refs = browseResults[0]?.references ?? [];
1521
+ let selectionsNodeId;
1522
+ let selectionsDescNodeId;
1523
+ let restrictToListNodeId;
1524
+ for (const ref of refs) {
1525
+ const name = ref.browseName?.name;
1526
+ if (name === "Selections") {
1527
+ selectionsNodeId = ref.nodeId.nodeId;
1528
+ } else if (name === "SelectionDescriptions") {
1529
+ selectionsDescNodeId = ref.nodeId.nodeId;
1530
+ } else if (name === "RestrictToList") {
1531
+ restrictToListNodeId = ref.nodeId.nodeId;
1532
+ }
1533
+ }
1534
+ if (!selectionsNodeId) {
1535
+ return null;
1536
+ }
1537
+ const nodeIdsToRead = [selectionsNodeId];
1538
+ if (selectionsDescNodeId) nodeIdsToRead.push(selectionsDescNodeId);
1539
+ if (restrictToListNodeId) nodeIdsToRead.push(restrictToListNodeId);
1540
+ const readResults = await this.attributeService.ReadValue(nodeIdsToRead);
1541
+ const selectionsVariant = readResults[0]?.value;
1542
+ const selections = Array.isArray(selectionsVariant?.value) ? selectionsVariant.value : [];
1543
+ let selectionDescriptions = [];
1544
+ let descReadIdx = 1;
1545
+ if (selectionsDescNodeId) {
1546
+ const descVariant = readResults[descReadIdx]?.value;
1547
+ if (Array.isArray(descVariant?.value)) {
1548
+ selectionDescriptions = descVariant.value;
1549
+ }
1550
+ descReadIdx++;
1551
+ }
1552
+ let restrictToList = false;
1553
+ if (restrictToListNodeId) {
1554
+ const rtlVariant = readResults[descReadIdx]?.value;
1555
+ if (typeof rtlVariant?.value === "boolean") {
1556
+ restrictToList = rtlVariant.value;
1557
+ }
1558
+ }
1559
+ return { nodeId, selections, selectionDescriptions, restrictToList };
1560
+ }
1561
+ /**
1562
+ * The `requestHandle` value that was assigned to the most recently issued
1563
+ * service request in this session.
1564
+ *
1565
+ * @deprecated Prefer accessing `requestHandle` directly on the promise returned
1566
+ * by `read()`, `browse()`, or `callMethod()`, which is available synchronously
1567
+ * before `await` and avoids relying on shared module state.
1568
+ *
1569
+ * Returns `0` before any request has been sent.
1570
+ */
1571
+ get lastRequestHandle() {
1572
+ return lastAssignedHandle();
1573
+ }
998
1574
  };
999
1575
  var ConfigurationClient = class _ConfigurationClient extends Configuration {
1000
1576
  /**
@@ -1005,6 +1581,26 @@ var ConfigurationClient = class _ConfigurationClient extends Configuration {
1005
1581
  * @see SecurityConfiguration
1006
1582
  */
1007
1583
  securityConfiguration;
1584
+ /**
1585
+ * How long to wait (ms) before attempting a reconnect after a server-shutdown
1586
+ * is detected via `ServerStatus/State = Shutdown` or a subscription
1587
+ * `StatusChangeNotification` with `BadShutdown` / `BadServerHalted`.
1588
+ *
1589
+ * Gives the server process time to exit fully before the client tries to
1590
+ * re-connect. Defaults to 5 000 ms.
1591
+ */
1592
+ shutdownReconnectDelayMs = 5e3;
1593
+ /**
1594
+ * Minimum reconnect delay in milliseconds used when
1595
+ * `Server/ServerStatus/EstimatedReturnTime` is already in the past (the server
1596
+ * should already be available again).
1597
+ *
1598
+ * Also acts as the lower bound for the ERT-derived delay, ensuring the client
1599
+ * always waits at least this long before retrying.
1600
+ *
1601
+ * Defaults to 1 000 ms.
1602
+ */
1603
+ minReconnectDelayMs = 1e3;
1008
1604
  static getSimple(name, company, loggerFactory) {
1009
1605
  if (!loggerFactory) {
1010
1606
  loggerFactory = new LoggerFactory({
@@ -1094,6 +1690,36 @@ var UserIdentity = class _UserIdentity {
1094
1690
  }
1095
1691
  };
1096
1692
 
1097
- export { BrowseNodeResult, CallMethodResult, Client, ConfigurationClient, SECURITY_POLICY_NONE_URI, SessionInvalidError, UserIdentity };
1693
+ // src/requestOptions.ts
1694
+ var ReturnDiagnosticsMask = {
1695
+ /** All service-level diagnostic fields. */
1696
+ ServiceLevel: 31,
1697
+ /** All operation-level diagnostic fields. */
1698
+ OperationLevel: 992,
1699
+ /** All diagnostic fields (service level + operation level). */
1700
+ All: 1023,
1701
+ /** Service-level: index to SymbolicId in the server string table. */
1702
+ ServiceSymbolicId: 1,
1703
+ /** Service-level: index to LocalizedText in the server string table. */
1704
+ ServiceLocalizedText: 2,
1705
+ /** Service-level: additional info string. */
1706
+ ServiceAdditionalInfo: 4,
1707
+ /** Service-level: inner status code. */
1708
+ ServiceInnerStatusCode: 8,
1709
+ /** Service-level: inner diagnostic info. */
1710
+ ServiceInnerDiagnostics: 16,
1711
+ /** Operation-level: index to SymbolicId in the server string table. */
1712
+ OperationSymbolicId: 32,
1713
+ /** Operation-level: index to LocalizedText in the server string table. */
1714
+ OperationLocalizedText: 64,
1715
+ /** Operation-level: additional info string. */
1716
+ OperationAdditionalInfo: 128,
1717
+ /** Operation-level: inner status code. */
1718
+ OperationInnerStatusCode: 256,
1719
+ /** Operation-level: inner diagnostic info. */
1720
+ OperationInnerDiagnostics: 512
1721
+ };
1722
+
1723
+ export { BrowseNodeResult, CERTIFICATE_REQUIRED_STATUS_CODES, CallMethodResult, CertificateRequiredError, Client, ConfigurationClient, NamespaceTable, ReturnDiagnosticsMask, SECURITY_POLICY_NONE_URI, SessionInvalidError, UserIdentity };
1098
1724
  //# sourceMappingURL=index.js.map
1099
1725
  //# sourceMappingURL=index.js.map