opcjs-client 0.1.10 → 0.1.14

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.cjs CHANGED
@@ -3,23 +3,87 @@
3
3
  var opcjsBase = require('opcjs-base');
4
4
 
5
5
  // src/client.ts
6
+ var SessionInvalidError = class extends Error {
7
+ statusCode;
8
+ constructor(statusCode) {
9
+ super(`Session is no longer valid: ${opcjsBase.StatusCodeToString(statusCode)} (0x${statusCode.toString(16).toUpperCase()})`);
10
+ this.name = "SessionInvalidError";
11
+ this.statusCode = statusCode;
12
+ }
13
+ };
14
+
15
+ // src/services/serviceBase.ts
16
+ var requestHandleCounter = 0;
17
+ function nextRequestHandle() {
18
+ if (requestHandleCounter >= 2147483647) {
19
+ requestHandleCounter = 1;
20
+ } else {
21
+ requestHandleCounter++;
22
+ }
23
+ return requestHandleCounter;
24
+ }
25
+ function lastAssignedHandle() {
26
+ return requestHandleCounter;
27
+ }
6
28
  var ServiceBase = class {
7
29
  constructor(authToken, secureChannel) {
8
30
  this.authToken = authToken;
9
31
  this.secureChannel = secureChannel;
10
32
  }
11
- createRequestHeader() {
33
+ /**
34
+ * Validates the `serviceResult` value from a response header.
35
+ *
36
+ * Throws `SessionInvalidError` for session-related status codes so callers
37
+ * can detect a dropped session and act accordingly (e.g. reconnect).
38
+ * Throws a generic `Error` for all other non-Good codes.
39
+ *
40
+ * @param result - The `serviceResult` value from the response header.
41
+ * @param context - Short description used in the error message (e.g. "ReadRequest").
42
+ */
43
+ checkServiceResult(result, context) {
44
+ if (result === void 0 || result === opcjsBase.StatusCode.Good) return;
45
+ if (result === opcjsBase.StatusCode.BadSessionIdInvalid || result === opcjsBase.StatusCode.BadSessionClosed) {
46
+ throw new SessionInvalidError(result);
47
+ }
48
+ throw new Error(`${context} failed: ${opcjsBase.StatusCodeToString(result)} (${opcjsBase.StatusCodeToStringNumber(result)})`);
49
+ }
50
+ /**
51
+ * Builds a RequestHeader for an outgoing service request.
52
+ *
53
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request from the
54
+ * server (OPC UA Part 4, §7.15). Use {@link ReturnDiagnosticsMask} constants
55
+ * to compose the value. Default `0` = no diagnostics.
56
+ */
57
+ createRequestHeader(returnDiagnostics = 0, preAllocatedHandle) {
12
58
  const requestHeader = new opcjsBase.RequestHeader();
13
59
  requestHeader.authenticationToken = this.authToken;
14
60
  requestHeader.timestamp = /* @__PURE__ */ new Date();
15
- requestHeader.requestHandle = 0;
16
- requestHeader.returnDiagnostics = 0;
61
+ requestHeader.requestHandle = preAllocatedHandle ?? nextRequestHandle();
62
+ requestHeader.returnDiagnostics = returnDiagnostics;
17
63
  requestHeader.auditEntryId = "";
18
64
  requestHeader.timeoutHint = 6e4;
19
65
  requestHeader.additionalHeader = opcjsBase.ExtensionObject.newEmpty();
20
66
  return requestHeader;
21
67
  }
22
68
  };
69
+ var CertificateRequiredError = class extends Error {
70
+ statusCode;
71
+ constructor(statusCode) {
72
+ super(
73
+ `Server requires a client certificate: ${opcjsBase.StatusCodeToString(statusCode)} (0x${statusCode.toString(16).toUpperCase().padStart(8, "0")})`
74
+ );
75
+ this.name = "CertificateRequiredError";
76
+ this.statusCode = statusCode;
77
+ }
78
+ };
79
+ var CERTIFICATE_REQUIRED_STATUS_CODES = /* @__PURE__ */ new Set([
80
+ 2148663296,
81
+ // BadCertificateInvalid
82
+ 2148728832,
83
+ // BadSecurityChecksFailed
84
+ 2153316352
85
+ // BadNoValidCertificates
86
+ ]);
23
87
 
24
88
  // src/services/sessionService.ts
25
89
  var SessionService = class _SessionService extends ServiceBase {
@@ -27,8 +91,20 @@ var SessionService = class _SessionService extends ServiceBase {
27
91
  super(authToken, secureChannel);
28
92
  this.configuration = configuration;
29
93
  }
30
- async createSession() {
31
- console.log("Creating session...");
94
+ logger = opcjsBase.getLogger("services.SessionService");
95
+ /**
96
+ * Creates a new session on the server (OPC UA Part 4, Section 5.7.2).
97
+ *
98
+ * @param clientCertificate - Optional DER-encoded client certificate to include in
99
+ * the request. Pass `null` (default) for SecurityPolicy None without a cert.
100
+ * When the server rejects a `null`-cert request with a certificate-related status
101
+ * code, the caller should retry with a `Uint8Array` certificate (OPC UA 1.0
102
+ * fallback — see `CertificateRequiredError`).
103
+ * @returns The session ID, authentication token, and selected server endpoint.
104
+ * @throws {CertificateRequiredError} when the server demands a client certificate.
105
+ */
106
+ async createSession(clientCertificate = null) {
107
+ this.logger.debug("Creating session...");
32
108
  const clientDescription = new opcjsBase.ApplicationDescription();
33
109
  clientDescription.applicationUri = this.configuration.applicationUri;
34
110
  clientDescription.productUri = this.configuration.productUri;
@@ -44,10 +120,10 @@ var SessionService = class _SessionService extends ServiceBase {
44
120
  request.endpointUrl = this.secureChannel.getEndpointUrl();
45
121
  request.sessionName = "";
46
122
  request.clientNonce = null;
47
- request.clientCertificate = null;
123
+ request.clientCertificate = clientCertificate ?? null;
48
124
  request.requestedSessionTimeout = 6e4;
49
125
  request.maxResponseMessageSize = 0;
50
- console.log("Sending CreateSessionRequest...");
126
+ this.logger.debug("Sending CreateSessionRequest...");
51
127
  const response = await this.secureChannel.issueServiceRequest(request);
52
128
  if (!response || !(response instanceof opcjsBase.CreateSessionResponse)) {
53
129
  throw new Error("Invalid response type for CreateSessionRequest");
@@ -56,24 +132,41 @@ var SessionService = class _SessionService extends ServiceBase {
56
132
  if (!castedResponse || !castedResponse.sessionId || !castedResponse.authenticationToken) {
57
133
  throw new Error("CreateSessionResponse missing SessionId or AuthenticationToken");
58
134
  }
59
- const endpoint = "opc." + this.secureChannel.getEndpointUrl();
60
- const endpointUrl = new URL(endpoint);
135
+ const serviceResult = castedResponse.responseHeader?.serviceResult;
136
+ if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
137
+ if (CERTIFICATE_REQUIRED_STATUS_CODES.has(serviceResult)) {
138
+ throw new CertificateRequiredError(serviceResult);
139
+ }
140
+ throw new Error(`CreateSessionRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
141
+ }
142
+ const clientConnectionUrl = new URL("opc." + this.secureChannel.getEndpointUrl());
61
143
  const securityMode = this.secureChannel.getSecurityMode();
62
144
  const securityPolicyUri = this.secureChannel.getSecurityPolicy();
145
+ const normalizeEndpointUrl = (serverEndpointUrl) => {
146
+ const url = new URL(serverEndpointUrl);
147
+ url.hostname = clientConnectionUrl.hostname;
148
+ url.port = clientConnectionUrl.port;
149
+ return url;
150
+ };
63
151
  const serverEndpoint = castedResponse?.serverEndpoints?.find((currentEndpoint) => {
64
- const currentEndpointUrl = new URL(currentEndpoint.endpointUrl);
65
- return currentEndpointUrl.protocol === endpointUrl.protocol && currentEndpointUrl.pathname === endpointUrl.pathname && currentEndpointUrl.port === endpointUrl.port && currentEndpoint.securityMode === securityMode && currentEndpoint.securityPolicyUri === securityPolicyUri;
152
+ const normalized = normalizeEndpointUrl(currentEndpoint.endpointUrl);
153
+ return normalized.protocol === clientConnectionUrl.protocol && normalized.pathname === clientConnectionUrl.pathname && currentEndpoint.securityMode === securityMode && currentEndpoint.securityPolicyUri === securityPolicyUri;
66
154
  });
67
155
  if (!serverEndpoint) {
68
- throw new Error(`Server endpoint ${endpoint} not found in CreateSessionResponse`);
156
+ throw new Error(`Server endpoint ${clientConnectionUrl.toString()} not found in CreateSessionResponse`);
69
157
  }
70
- console.log("Session created with id:", castedResponse.sessionId.identifier);
158
+ serverEndpoint.endpointUrl = normalizeEndpointUrl(serverEndpoint.endpointUrl).toString();
159
+ this.logger.debug("Session created with id:", castedResponse.sessionId.identifier);
71
160
  return {
72
161
  sessionId: castedResponse.sessionId.identifier,
73
162
  authToken: castedResponse.authenticationToken,
74
163
  endpoint: serverEndpoint
75
164
  };
76
165
  }
166
+ /**
167
+ * Activates an existing session using the supplied identity token (OPC UA Part 4, Section 5.7.3).
168
+ * @param identityToken - User identity token (anonymous, username/password, certificate, or issued token).
169
+ */
77
170
  async activateSession(identityToken) {
78
171
  const signatureData = new opcjsBase.SignatureData();
79
172
  signatureData.algorithm = this.secureChannel.getSecurityPolicy();
@@ -85,16 +178,65 @@ var SessionService = class _SessionService extends ServiceBase {
85
178
  request.localeIds = ["en-US"];
86
179
  request.userIdentityToken = opcjsBase.ExtensionObject.newBinary(identityToken);
87
180
  request.userTokenSignature = signatureData;
88
- console.log("Sending ActivateSessionRequest...");
89
- await this.secureChannel.issueServiceRequest(request);
90
- console.log("Session activated.");
181
+ this.logger.debug("Sending ActivateSessionRequest...");
182
+ const activateResponse = await this.secureChannel.issueServiceRequest(request);
183
+ const activateResult = activateResponse?.responseHeader?.serviceResult;
184
+ if (activateResult !== void 0 && activateResult !== opcjsBase.StatusCode.Good) {
185
+ throw new Error(`ActivateSessionRequest failed: ${opcjsBase.StatusCodeToString(activateResult)}`);
186
+ }
187
+ this.logger.debug("Session activated.");
188
+ }
189
+ /**
190
+ * Sends a CancelRequest to the server asking it to abandon the pending
191
+ * service request identified by `requestHandle` (OPC UA Part 4, Section 5.7.5).
192
+ *
193
+ * The server makes a best-effort attempt to cancel matching requests.
194
+ * Cancelled requests complete with a status of `BadRequestCancelledByClient`.
195
+ *
196
+ * @param requestHandle - The `requestHeader.requestHandle` value of the pending
197
+ * request to cancel. Pass `0` to attempt to cancel all outstanding requests
198
+ * (server behaviour for handle 0 is implementation-specific).
199
+ * @returns The number of pending requests that were actually cancelled
200
+ * (`CancelResponse.cancelCount`).
201
+ */
202
+ async cancel(requestHandle) {
203
+ this.logger.debug(`Sending CancelRequest for requestHandle ${requestHandle}...`);
204
+ const request = new opcjsBase.CancelRequest();
205
+ request.requestHeader = this.createRequestHeader();
206
+ request.requestHandle = requestHandle;
207
+ const response = await this.secureChannel.issueServiceRequest(request);
208
+ const result = response?.responseHeader?.serviceResult;
209
+ if (result !== void 0 && result !== opcjsBase.StatusCode.Good) {
210
+ throw new Error(`CancelRequest failed: ${opcjsBase.StatusCodeToString(result)}`);
211
+ }
212
+ const cancelCount = response?.cancelCount ?? 0;
213
+ this.logger.debug(`CancelRequest completed; cancelCount = ${cancelCount}.`);
214
+ return cancelCount;
215
+ }
216
+ /**
217
+ * Closes the current session on the server (OPC UA Part 4, Section 5.7.4).
218
+ * @param deleteSubscriptions - When true the server deletes all Subscriptions
219
+ * associated with this Session, freeing their resources immediately.
220
+ * Pass false to keep Subscriptions alive for transfer to another Session.
221
+ */
222
+ async closeSession(deleteSubscriptions) {
223
+ this.logger.debug("Sending CloseSessionRequest...");
224
+ const request = new opcjsBase.CloseSessionRequest();
225
+ request.requestHeader = this.createRequestHeader();
226
+ request.deleteSubscriptions = deleteSubscriptions;
227
+ const response = await this.secureChannel.issueServiceRequest(request);
228
+ const result = response?.responseHeader?.serviceResult;
229
+ if (result !== void 0 && result !== opcjsBase.StatusCode.Good) {
230
+ throw new Error(`CloseSession failed: ${opcjsBase.StatusCodeToString(result)}`);
231
+ }
232
+ this.logger.debug("Session closed.");
91
233
  }
92
234
  recreate(authToken) {
93
235
  return new _SessionService(authToken, this.secureChannel, this.configuration);
94
236
  }
95
237
  };
96
238
 
97
- // src/issuerConfiguration.ts
239
+ // src/configuration/issuerConfiguration.ts
98
240
  var IssuerConfiguration = class _IssuerConfiguration {
99
241
  constructor(resourceId, authorityUrl, authorityProfileUri, tokenEndpoint, authorizationEndpoint, requestTypes, scopes) {
100
242
  this.resourceId = resourceId;
@@ -150,29 +292,159 @@ var Session = class {
150
292
  getAuthToken() {
151
293
  return this.authToken;
152
294
  }
295
+ getSessionId() {
296
+ return this.sessionId;
297
+ }
298
+ getEndpoint() {
299
+ return this.endpoint;
300
+ }
301
+ /**
302
+ * Switches the active user identity for this session by calling ActivateSession with
303
+ * a new identity token (OPC UA Part 4, Section 5.7.3 — Session Client Impersonate
304
+ * conformance unit).
305
+ *
306
+ * The server re-evaluates authorisation for the session under the new identity.
307
+ * All existing Subscriptions and MonitoredItems are preserved; only the security
308
+ * context changes.
309
+ *
310
+ * @param identity - The new user identity to apply to the session.
311
+ * @throws When the server returns a non-Good ServiceResult (e.g. `BadIdentityTokenRejected`
312
+ * or `BadUserAccessDenied`).
313
+ */
314
+ async impersonate(identity) {
315
+ await this.activateSession(identity);
316
+ }
317
+ /**
318
+ * Closes the session on the server (OPC UA Part 4, Section 5.7.4).
319
+ * @param deleteSubscriptions - When true the server deletes all Subscriptions
320
+ * tied to this Session. Defaults to true.
321
+ */
322
+ async close(deleteSubscriptions = true) {
323
+ await this.sessionServices.closeSession(deleteSubscriptions);
324
+ }
153
325
  };
154
326
 
155
327
  // src/sessions/sessionHandler.ts
156
328
  var SessionHandler = class {
329
+ constructor(secureChannel, configuration) {
330
+ this.configuration = configuration;
331
+ this.sessionServices = new SessionService(opcjsBase.NodeId.newTwoByte(0), secureChannel, configuration);
332
+ }
157
333
  sessionServices;
334
+ logger = opcjsBase.getLogger("sessions.SessionHandler");
158
335
  async createNewSession(identity) {
159
- const ret = await this.sessionServices.createSession();
160
- this.sessionServices = this.sessionServices.recreate(ret.authToken);
161
- const session = new Session(ret.sessionId, ret.authToken, ret.endpoint, this.sessionServices);
336
+ let sessionResult;
337
+ try {
338
+ sessionResult = await this.sessionServices.createSession(null);
339
+ } catch (err) {
340
+ if (err instanceof CertificateRequiredError) {
341
+ const fallbackCert = this.configuration.securityConfiguration?.applicationInstanceCertificate;
342
+ if (fallbackCert) {
343
+ this.logger.info(
344
+ "Server requires a client certificate (OPC UA 1.0 fallback); retrying CreateSession with applicationInstanceCertificate."
345
+ );
346
+ sessionResult = await this.sessionServices.createSession(fallbackCert);
347
+ } else {
348
+ this.logger.warn(
349
+ "Server requires a client certificate but no applicationInstanceCertificate is configured in securityConfiguration. Cannot complete the 1.0 fallback."
350
+ );
351
+ throw err;
352
+ }
353
+ } else {
354
+ throw err;
355
+ }
356
+ }
357
+ this.sessionServices = this.sessionServices.recreate(sessionResult.authToken);
358
+ const session = new Session(sessionResult.sessionId, sessionResult.authToken, sessionResult.endpoint, this.sessionServices);
359
+ this.validateUserTokenPolicy(identity, sessionResult.endpoint);
162
360
  await session.activateSession(identity);
163
361
  return session;
164
362
  }
165
- constructor(secureChannel, configuration) {
166
- this.sessionServices = new SessionService(opcjsBase.NodeId.newTwoByte(0), secureChannel, configuration);
363
+ /**
364
+ * Attempts to reactivate an existing OPC UA session on the current (new) SecureChannel
365
+ * without calling CreateSession first (OPC UA Part 4, Section 5.7.3).
366
+ *
367
+ * This is the preferred recovery path when the SecureChannel drops but the server-side
368
+ * session has not yet timed out: only a new channel is needed, not a new session.
369
+ *
370
+ * @returns The reactivated Session if ActivateSession succeeded, or `null` if the server
371
+ * rejected the request (e.g. the session had already expired).
372
+ */
373
+ async tryActivateExistingSession(existingAuthToken, existingSessionId, existingEndpoint, identity) {
374
+ const serviceForExistingSession = this.sessionServices.recreate(existingAuthToken);
375
+ try {
376
+ const session = new Session(existingSessionId, existingAuthToken, existingEndpoint, serviceForExistingSession);
377
+ await session.activateSession(identity);
378
+ return session;
379
+ } catch (err) {
380
+ this.logger.debug("ActivateSession for existing session failed:", err);
381
+ return null;
382
+ }
383
+ }
384
+ /**
385
+ * Closes the active session on the server (OPC UA Part 4, Section 5.7.4).
386
+ * @param deleteSubscriptions - Forwarded to CloseSessionRequest. Defaults to true.
387
+ */
388
+ async closeSession(deleteSubscriptions = true) {
389
+ await this.sessionServices.closeSession(deleteSubscriptions);
390
+ }
391
+ /**
392
+ * Sends a CancelRequest to the server to abandon a pending service call
393
+ * (OPC UA Part 4, Section 5.7.5).
394
+ *
395
+ * @param requestHandle - The `requestHandle` from the `RequestHeader` of the
396
+ * pending request to cancel.
397
+ * @returns The number of requests the server actually cancelled.
398
+ */
399
+ async cancel(requestHandle) {
400
+ return this.sessionServices.cancel(requestHandle);
401
+ }
402
+ /**
403
+ * Validates the requested user-identity token type against:
404
+ * 1. The `allowedUserTokenTypes` from the client security configuration — the
405
+ * client has explicitly restricted which token types it will use.
406
+ * 2. The token policies advertised by the server endpoint — verifies that the
407
+ * server actually supports at least one of the allowed types.
408
+ *
409
+ * Throws with a descriptive message if either check fails.
410
+ */
411
+ validateUserTokenPolicy(identity, endpoint) {
412
+ const allowedTypes = this.configuration.securityConfiguration?.allowedUserTokenTypes;
413
+ if (!allowedTypes) return;
414
+ const requestedType = identity.getTokenType();
415
+ if (!allowedTypes.includes(requestedType)) {
416
+ throw new Error(
417
+ `User token type '${opcjsBase.UserTokenTypeEnum[requestedType]}' is not permitted by the client security configuration. Allowed types: ${allowedTypes.map((t) => opcjsBase.UserTokenTypeEnum[t]).join(", ")}.`
418
+ );
419
+ }
420
+ const serverTypes = endpoint.userIdentityTokens?.map((p) => p.tokenType) ?? [];
421
+ const intersection = allowedTypes.filter((t) => serverTypes.includes(t));
422
+ if (intersection.length === 0) {
423
+ throw new Error(
424
+ `Server endpoint does not offer any user token type from the allowed list: ${allowedTypes.map((t) => opcjsBase.UserTokenTypeEnum[t]).join(", ")}. Server offers: ${serverTypes.map((t) => opcjsBase.UserTokenTypeEnum[t]).join(", ")}.`
425
+ );
426
+ }
167
427
  }
168
428
  };
169
429
 
430
+ // src/securityConfiguration.ts
431
+ var SECURITY_POLICY_NONE_URI = "http://opcfoundation.org/UA/SecurityPolicy#None";
432
+
170
433
  // src/services/attributeServiceAttributes.ts
171
434
  var AttrIdValue = 13;
172
435
 
173
436
  // src/services/attributeService.ts
174
437
  var AttributeService = class extends ServiceBase {
175
- async ReadValue(nodeIds) {
438
+ logger = opcjsBase.getLogger("services.AttributeService");
439
+ /**
440
+ * Reads the Value attribute of one or more Nodes (OPC UA Part 4, Section 5.10.2).
441
+ * @param nodeIds - NodeIds of the Nodes to read.
442
+ * @param maxAge - Maximum age of the cached value in milliseconds the server may return. 0 = always current value.
443
+ * @param timestampsToReturn - Which timestamps to include in results. Default: Source.
444
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
445
+ * @returns Array of results containing value, raw status code, and optional diagnostic info, one per requested NodeId.
446
+ */
447
+ async ReadValue(nodeIds, maxAge = 0, timestampsToReturn = opcjsBase.TimestampsToReturnEnum.Source, returnDiagnostics = 0, requestHandle) {
176
448
  const readValueIds = nodeIds.map((ni) => {
177
449
  const readValueId = new opcjsBase.ReadValueId();
178
450
  readValueId.nodeId = ni;
@@ -182,19 +454,22 @@ var AttributeService = class extends ServiceBase {
182
454
  return readValueId;
183
455
  });
184
456
  const request = new opcjsBase.ReadRequest();
185
- request.requestHeader = this.createRequestHeader();
186
- request.maxAge = 6e4;
187
- request.timestampsToReturn = opcjsBase.TimestampsToReturnEnum.Source;
457
+ request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
458
+ request.maxAge = maxAge;
459
+ request.timestampsToReturn = timestampsToReturn;
188
460
  request.nodesToRead = readValueIds;
189
- console.log("Sending ReadRequest...");
461
+ this.logger.debug("Sending ReadRequest...");
190
462
  const response = await this.secureChannel.issueServiceRequest(request);
463
+ this.checkServiceResult(response.responseHeader?.serviceResult, "ReadRequest");
191
464
  const results = new Array();
192
- for (let dataValue of response.results ?? []) {
193
- const result = {
194
- status: dataValue.statusCode?.toString() ?? "Unknown",
195
- value: dataValue.value
196
- };
197
- results.push(result);
465
+ const diagInfos = response.diagnosticInfos ?? [];
466
+ for (let i = 0; i < (response.results ?? []).length; i++) {
467
+ const dataValue = response.results[i];
468
+ results.push({
469
+ statusCode: dataValue.statusCode ?? opcjsBase.StatusCode.Good,
470
+ value: dataValue.value,
471
+ diagnosticInfo: diagInfos[i]
472
+ });
198
473
  }
199
474
  return results;
200
475
  }
@@ -205,13 +480,14 @@ var AttributeService = class extends ServiceBase {
205
480
 
206
481
  // src/readValueResult.ts
207
482
  var ReadValueResult = class {
208
- constructor(value, status) {
483
+ constructor(value, statusCode, diagnosticInfo) {
209
484
  this.value = value;
210
- this.status = status;
485
+ this.statusCode = statusCode;
486
+ this.diagnosticInfo = diagnosticInfo;
211
487
  }
212
488
  };
213
489
 
214
- // src/subscriptionHandlerEntry.ts
490
+ // src/subscription/subscriptionHandlerEntry.ts
215
491
  var SubscriptionHandlerEntry = class {
216
492
  constructor(subscriptionId, handle, id, callback) {
217
493
  this.subscriptionId = subscriptionId;
@@ -221,95 +497,155 @@ var SubscriptionHandlerEntry = class {
221
497
  }
222
498
  };
223
499
 
224
- // src/subscriptionHandler.ts
500
+ // src/subscription/subscriptionHandler.ts
501
+ var NODE_ID_DATA_CHANGE_NOTIFICATION = 811;
502
+ var NODE_ID_STATUS_CHANGE_NOTIFICATION = 818;
225
503
  var SubscriptionHandler = class {
226
504
  constructor(subscriptionService, monitoredItemService) {
227
505
  this.subscriptionService = subscriptionService;
228
506
  this.monitoredItemService = monitoredItemService;
229
507
  }
508
+ logger = opcjsBase.getLogger("SubscriptionHandler");
230
509
  entries = new Array();
231
510
  nextHandle = 0;
232
- async subscribe(ids, callback) {
511
+ isRunning = false;
512
+ /** Guards against multiple concurrent publish loops. */
513
+ publishInFlight = false;
514
+ /**
515
+ * Optional callback invoked when the server announces a shutdown via a
516
+ * `StatusChangeNotification` with status `BadShutdown` or `BadServerHalted`
517
+ * (OPC UA Part 4, §5.13.6.2 — Session Client Detect Shutdown).
518
+ *
519
+ * Assign this before calling `subscribe()`. The client sets it automatically
520
+ * in `initServices()` to trigger a reconnect.
521
+ */
522
+ onShutdown;
523
+ /** Returns true when at least one subscription is active and the publish loop is running. */
524
+ hasActiveSubscription() {
525
+ return this.isRunning && this.entries.length > 0;
526
+ }
527
+ async subscribe(ids, callback, options) {
233
528
  if (this.entries.length > 0) {
234
529
  throw new Error("Subscribing more than once is not implemented");
235
530
  }
236
- const subscriptionId = await this.subscriptionService.createSubscription();
531
+ const subscriptionId = await this.subscriptionService.createSubscription(options);
237
532
  const items = [];
238
- for (let id of ids) {
239
- const entry = new SubscriptionHandlerEntry(
240
- subscriptionId,
241
- this.nextHandle++,
242
- id,
243
- callback
244
- );
533
+ for (const id of ids) {
534
+ const entry = new SubscriptionHandlerEntry(subscriptionId, this.nextHandle++, id, callback);
245
535
  this.entries.push(entry);
246
- const item = {
247
- id: id.toNodeId(),
248
- handle: entry.handle
249
- };
250
- items.push(item);
251
- }
252
- await this.monitoredItemService.createMonitoredItems(subscriptionId, items);
253
- this.publish([]);
254
- }
255
- async publish(acknowledgeSequenceNumbers) {
256
- const acknowledgements = [];
257
- for (let i = 0; i < acknowledgeSequenceNumbers.length; i++) {
258
- const acknowledgement = new opcjsBase.SubscriptionAcknowledgement();
259
- acknowledgement.subscriptionId = this.entries[i].subscriptionId;
260
- acknowledgement.sequenceNumber = acknowledgeSequenceNumbers[i];
261
- acknowledgements.push(acknowledgement);
262
- }
263
- const response = await this.subscriptionService.publish(acknowledgements);
264
- const messagesToAcknowledge = response.notificationMessage.sequenceNumber;
265
- const notificationDatas = response.notificationMessage.notificationData;
266
- for (let notificationData of notificationDatas) {
536
+ items.push({ id, handle: entry.handle });
537
+ }
538
+ await this.monitoredItemService.createMonitoredItems(subscriptionId, items, options);
539
+ this.isRunning = true;
540
+ void this.publishLoop([]);
541
+ }
542
+ // https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.5
543
+ async publishLoop(pendingAcknowledgements) {
544
+ if (!this.isRunning) return;
545
+ if (this.publishInFlight) return;
546
+ this.publishInFlight = true;
547
+ let response;
548
+ try {
549
+ response = await this.subscriptionService.publish(pendingAcknowledgements);
550
+ } catch (err) {
551
+ this.logger.error(`Publish failed, stopping publish loop: ${err}`);
552
+ this.isRunning = false;
553
+ this.publishInFlight = false;
554
+ return;
555
+ }
556
+ this.publishInFlight = false;
557
+ const { subscriptionId, availableSequenceNumbers, moreNotifications, notificationMessage } = response;
558
+ const notificationDatas = notificationMessage?.notificationData ?? [];
559
+ const seqNumber = notificationMessage?.sequenceNumber;
560
+ const nextAcknowledgements = [];
561
+ const isKeepAlive = notificationDatas.length === 0;
562
+ if (!isKeepAlive && seqNumber !== void 0) {
563
+ const isAvailable = !availableSequenceNumbers || availableSequenceNumbers.includes(seqNumber);
564
+ if (isAvailable) {
565
+ const ack = new opcjsBase.SubscriptionAcknowledgement();
566
+ ack.subscriptionId = subscriptionId;
567
+ ack.sequenceNumber = seqNumber;
568
+ nextAcknowledgements.push(ack);
569
+ }
570
+ }
571
+ for (const notificationData of notificationDatas) {
267
572
  const decodedData = notificationData.data;
268
- const typeNodeId = notificationData.typeId;
269
- if (typeNodeId.namespace === 0 && typeNodeId.identifier === 811) {
573
+ const rawTypeId = notificationData.typeId;
574
+ const typeNodeId = rawTypeId instanceof opcjsBase.ExpandedNodeId ? rawTypeId.nodeId : rawTypeId;
575
+ if (typeNodeId.namespace === 0 && typeNodeId.identifier === NODE_ID_DATA_CHANGE_NOTIFICATION) {
270
576
  const dataChangeNotification = decodedData;
271
- for (let item of dataChangeNotification.monitoredItems) {
272
- const clientHandle = item.clientHandle;
273
- const value = item.value;
274
- const entry = this.entries.find((e) => e.handle == clientHandle);
275
- entry?.callback([{
276
- id: entry.id,
277
- value: value.value?.value
278
- }]);
577
+ for (const item of dataChangeNotification.monitoredItems) {
578
+ const entry = this.entries.find((e) => e.handle === item.clientHandle);
579
+ entry?.callback([{ id: entry.id, value: item.value.value?.value }]);
279
580
  }
581
+ } else if (typeNodeId.namespace === 0 && typeNodeId.identifier === NODE_ID_STATUS_CHANGE_NOTIFICATION) {
582
+ const statusChange = decodedData;
583
+ this.logger.warn(
584
+ `Subscription ${subscriptionId} status changed: 0x${statusChange.status?.toString(16).toUpperCase()}`
585
+ );
586
+ this.isRunning = false;
587
+ const status = statusChange.status;
588
+ if (status === opcjsBase.StatusCode.BadShutdown || status === opcjsBase.StatusCode.BadServerHalted) {
589
+ this.logger.warn("Server shutdown announced via StatusChangeNotification \u2014 triggering reconnect.");
590
+ this.onShutdown?.();
591
+ }
592
+ return;
280
593
  } else {
281
- console.log(`The change notification data type ${typeNodeId.namespace}:${typeNodeId.identifier} is not supported.`);
594
+ this.logger.warn(
595
+ `Notification data type ${typeNodeId.namespace}:${typeNodeId.identifier} is not supported.`
596
+ );
282
597
  }
283
598
  }
284
- setTimeout(() => {
285
- this.publish([messagesToAcknowledge]);
286
- }, 500);
599
+ if (moreNotifications) {
600
+ void this.publishLoop(nextAcknowledgements);
601
+ } else {
602
+ setTimeout(() => void this.publishLoop(nextAcknowledgements), 0);
603
+ }
287
604
  }
288
605
  };
289
606
  var SubscriptionService = class extends ServiceBase {
607
+ logger = opcjsBase.getLogger("services.SubscriptionService");
608
+ /**
609
+ * Creates a new subscription on the server (OPC UA Part 4, Section 5.14.2).
610
+ * @param options - Optional subscription parameters; server revises all values.
611
+ * @returns The server-assigned subscription ID.
612
+ */
290
613
  // https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.2
291
- async createSubscription() {
614
+ async createSubscription(options) {
292
615
  const request = new opcjsBase.CreateSubscriptionRequest();
293
616
  request.requestHeader = this.createRequestHeader();
294
- request.requestedPublishingInterval = 2e3;
295
- request.requestedLifetimeCount = 36e4;
296
- request.requestedMaxKeepAliveCount = 6e4;
297
- request.maxNotificationsPerPublish = 200;
617
+ request.requestedPublishingInterval = options?.requestedPublishingInterval ?? 2e3;
618
+ request.requestedLifetimeCount = options?.requestedLifetimeCount ?? 36e4;
619
+ request.requestedMaxKeepAliveCount = options?.requestedMaxKeepAliveCount ?? 6e4;
620
+ request.maxNotificationsPerPublish = options?.maxNotificationsPerPublish ?? 200;
298
621
  request.publishingEnabled = true;
299
- request.priority = 1;
300
- console.log("Sending createSubscription...");
622
+ request.priority = options?.priority ?? 1;
623
+ this.logger.debug("Sending CreateSubscriptionRequest...");
301
624
  const response = await this.secureChannel.issueServiceRequest(request);
302
- console.log("Subscription created with id:", response.subscriptionId);
625
+ const serviceResult = response.responseHeader?.serviceResult;
626
+ if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
627
+ throw new Error(`CreateSubscriptionRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
628
+ }
629
+ this.logger.debug(`Subscription created with id: ${response.subscriptionId}`);
303
630
  return response.subscriptionId;
304
631
  }
632
+ /**
633
+ * Sends a publish request to receive notifications from active subscriptions (OPC UA Part 4, Section 5.14.5).
634
+ * @param acknowledgements - Sequence numbers to acknowledge from previous publish responses.
635
+ * @returns The publish response containing notification data.
636
+ */
305
637
  // https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.5
306
638
  async publish(acknowledgements) {
307
639
  const request = new opcjsBase.PublishRequest();
308
640
  request.requestHeader = this.createRequestHeader();
309
641
  request.subscriptionAcknowledgements = acknowledgements;
310
- console.log("Sending publish...");
642
+ this.logger.debug("Sending PublishRequest...");
311
643
  const response = await this.secureChannel.issueServiceRequest(request);
312
- console.log("Received publish response.");
644
+ const serviceResult = response.responseHeader?.serviceResult;
645
+ if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
646
+ throw new Error(`PublishRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
647
+ }
648
+ this.logger.debug("Received PublishResponse.");
313
649
  return response;
314
650
  }
315
651
  constructor(authToken, secureChannel) {
@@ -317,7 +653,15 @@ var SubscriptionService = class extends ServiceBase {
317
653
  }
318
654
  };
319
655
  var MonitoredItemService = class extends ServiceBase {
320
- async createMonitoredItems(subscriptionId, ids) {
656
+ logger = opcjsBase.getLogger("services.MonitoredItemService");
657
+ /**
658
+ * Creates monitored items within a subscription (OPC UA Part 4, Section 5.13.2).
659
+ * @param subscriptionId - ID of the subscription to add monitored items to.
660
+ * @param ids - Array of NodeIds and client handles identifying the items to monitor.
661
+ * @param options - Monitoring options (samplingInterval, queueSize).
662
+ */
663
+ async createMonitoredItems(subscriptionId, ids, options = {}) {
664
+ const { samplingInterval = 1e3, queueSize = 100 } = options;
321
665
  const items = ids.map((ni) => {
322
666
  const readValueId = new opcjsBase.ReadValueId();
323
667
  readValueId.nodeId = ni.id;
@@ -326,9 +670,9 @@ var MonitoredItemService = class extends ServiceBase {
326
670
  readValueId.dataEncoding = new opcjsBase.QualifiedName(0, "");
327
671
  const monitoringParameters = new opcjsBase.MonitoringParameters();
328
672
  monitoringParameters.clientHandle = ni.handle;
329
- monitoringParameters.samplingInterval = 1e3;
673
+ monitoringParameters.samplingInterval = samplingInterval;
330
674
  monitoringParameters.filter = opcjsBase.ExtensionObject.newEmpty();
331
- monitoringParameters.queueSize = 100;
675
+ monitoringParameters.queueSize = queueSize;
332
676
  monitoringParameters.discardOldest = true;
333
677
  const monitoredItemCreateRequest = new opcjsBase.MonitoredItemCreateRequest();
334
678
  monitoredItemCreateRequest.itemToMonitor = readValueId;
@@ -341,69 +685,935 @@ var MonitoredItemService = class extends ServiceBase {
341
685
  request.subscriptionId = subscriptionId;
342
686
  request.timestampsToReturn = opcjsBase.TimestampsToReturnEnum.Source;
343
687
  request.itemsToCreate = items;
344
- console.log("Sending createMonitoredItems...");
345
- await this.secureChannel.issueServiceRequest(request);
688
+ this.logger.debug("Sending CreateMonitoredItemsRequest...");
689
+ const response = await this.secureChannel.issueServiceRequest(request);
690
+ const serviceResult = response.responseHeader?.serviceResult;
691
+ if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
692
+ throw new Error(`CreateMonitoredItemsRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
693
+ }
694
+ for (const result of response.results ?? []) {
695
+ if (result.statusCode !== void 0 && result.statusCode !== opcjsBase.StatusCode.Good) {
696
+ this.logger.warn(`Failed to create monitored item: ${opcjsBase.StatusCodeToString(result.statusCode)}`);
697
+ }
698
+ }
699
+ }
700
+ constructor(authToken, secureChannel) {
701
+ super(authToken, secureChannel);
702
+ }
703
+ };
704
+ var MethodService = class extends ServiceBase {
705
+ logger = opcjsBase.getLogger("services.MethodService");
706
+ /**
707
+ * Calls one or more methods on the server (OPC UA Part 4, Section 5.11.2).
708
+ * @param methodsToCall - Array of CallMethodRequest describing each method to invoke.
709
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
710
+ * @returns Array of results containing output argument values, raw status code, and optional diagnostic info,
711
+ * one per requested method.
712
+ */
713
+ async call(methodsToCall, returnDiagnostics = 0, requestHandle) {
714
+ const request = new opcjsBase.CallRequest();
715
+ request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
716
+ request.methodsToCall = methodsToCall;
717
+ this.logger.debug("Sending CallRequest...");
718
+ const response = await this.secureChannel.issueServiceRequest(request);
719
+ this.checkServiceResult(response.responseHeader?.serviceResult, "CallRequest");
720
+ const diagInfos = response.diagnosticInfos ?? [];
721
+ return response.results.map((result, i) => ({
722
+ statusCode: result.statusCode ?? opcjsBase.StatusCode.Good,
723
+ value: result.outputArguments.map((arg) => arg.value),
724
+ diagnosticInfo: diagInfos[i]
725
+ }));
726
+ }
727
+ constructor(authToken, secureChannel) {
728
+ super(authToken, secureChannel);
729
+ }
730
+ };
731
+
732
+ // src/method/callMethodResult.ts
733
+ var CallMethodResult = class {
734
+ constructor(values, statusCode, diagnosticInfo) {
735
+ this.values = values;
736
+ this.statusCode = statusCode;
737
+ this.diagnosticInfo = diagnosticInfo;
738
+ }
739
+ };
740
+ var BrowseService = class extends ServiceBase {
741
+ logger = opcjsBase.getLogger("services.BrowseService");
742
+ /**
743
+ * Browses one or more Nodes and returns their References (OPC UA Part 4, Section 5.9.2).
744
+ * @param nodesToBrowse - Array of BrowseDescriptions specifying nodes and filter criteria.
745
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
746
+ * @returns Array of BrowseResult, one per requested node.
747
+ */
748
+ async browse(nodesToBrowse, returnDiagnostics = 0, requestHandle) {
749
+ const view = new opcjsBase.ViewDescription();
750
+ view.viewId = opcjsBase.NodeId.newNumeric(0, 0);
751
+ view.timestamp = /* @__PURE__ */ new Date(-116444736e5);
752
+ view.viewVersion = 0;
753
+ const request = new opcjsBase.BrowseRequest();
754
+ request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
755
+ request.view = view;
756
+ request.requestedMaxReferencesPerNode = 0;
757
+ request.nodesToBrowse = nodesToBrowse;
758
+ this.logger.debug("Sending BrowseRequest...");
759
+ const response = await this.secureChannel.issueServiceRequest(request);
760
+ this.checkServiceResult(response.responseHeader?.serviceResult, "BrowseRequest");
761
+ return response.results ?? [];
762
+ }
763
+ /**
764
+ * Continues a Browse operation using continuation points (OPC UA Part 4, Section 5.9.3).
765
+ * @param continuationPoints - Continuation points returned by a prior Browse or BrowseNext call.
766
+ * @param releaseContinuationPoints - If true, releases the continuation points without returning results.
767
+ * @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
768
+ * @returns Array of BrowseResult, one per continuation point.
769
+ */
770
+ async browseNext(continuationPoints, releaseContinuationPoints, returnDiagnostics = 0) {
771
+ const request = new opcjsBase.BrowseNextRequest();
772
+ request.requestHeader = this.createRequestHeader(returnDiagnostics);
773
+ request.releaseContinuationPoints = releaseContinuationPoints;
774
+ request.continuationPoints = continuationPoints;
775
+ this.logger.debug("Sending BrowseNextRequest...");
776
+ const response = await this.secureChannel.issueServiceRequest(request);
777
+ this.checkServiceResult(response.responseHeader?.serviceResult, "BrowseNextRequest");
778
+ return response.results ?? [];
346
779
  }
347
780
  constructor(authToken, secureChannel) {
348
781
  super(authToken, secureChannel);
349
782
  }
350
783
  };
351
784
 
785
+ // src/browseNodeResult.ts
786
+ var BrowseNodeResult = class {
787
+ constructor(referenceTypeId, isForward, nodeId, browseName, displayName, nodeClass, typeDefinition) {
788
+ this.referenceTypeId = referenceTypeId;
789
+ this.isForward = isForward;
790
+ this.nodeId = nodeId;
791
+ this.browseName = browseName;
792
+ this.displayName = displayName;
793
+ this.nodeClass = nodeClass;
794
+ this.typeDefinition = typeDefinition;
795
+ }
796
+ };
797
+ var NamespaceTable = class {
798
+ uris;
799
+ constructor(uris = []) {
800
+ this.uris = Object.freeze([...uris]);
801
+ }
802
+ /** Returns the namespace URI at the given index, or `undefined` if out of range. */
803
+ getUri(index) {
804
+ return this.uris[index];
805
+ }
806
+ /** Returns the namespace index for the given URI, or `undefined` if not found. */
807
+ getIndex(uri) {
808
+ const idx = this.uris.indexOf(uri);
809
+ return idx >= 0 ? idx : void 0;
810
+ }
811
+ /** Returns the full ordered array of namespace URIs. */
812
+ getUris() {
813
+ return this.uris;
814
+ }
815
+ /** Returns `true` when both tables contain the same URIs in the same order. */
816
+ equals(other) {
817
+ if (this.uris.length !== other.uris.length) return false;
818
+ return this.uris.every((uri, i) => uri === other.uris[i]);
819
+ }
820
+ /**
821
+ * Remaps the namespace index of `nodeId` from this table to the equivalent
822
+ * index in `newTable` by matching namespace URIs.
823
+ *
824
+ * - Namespace index `0` (OPC UA base namespace) is always returned unchanged.
825
+ * - Returns the **same** `NodeId` instance when the index has not changed.
826
+ * - Returns a **new** `NodeId` instance with the updated index when remapping
827
+ * is required.
828
+ *
829
+ * @throws {Error} When the namespace URI at `nodeId.namespace` is not present
830
+ * in this (old) table or when the URI is not present in `newTable`.
831
+ */
832
+ remapNodeId(nodeId, newTable) {
833
+ if (nodeId.namespace === 0) return nodeId;
834
+ const uri = this.getUri(nodeId.namespace);
835
+ if (uri === void 0) {
836
+ throw new Error(
837
+ `Cannot remap ${nodeId.toString()}: namespace index ${nodeId.namespace} is not present in the old NamespaceTable (length ${this.uris.length}).`
838
+ );
839
+ }
840
+ const newIndex = newTable.getIndex(uri);
841
+ if (newIndex === void 0) {
842
+ throw new Error(
843
+ `Cannot remap ${nodeId.toString()}: namespace URI '${uri}' is not present in the new NamespaceTable.`
844
+ );
845
+ }
846
+ if (newIndex === nodeId.namespace) return nodeId;
847
+ return new opcjsBase.NodeId(newIndex, nodeId.identifier);
848
+ }
849
+ };
850
+
352
851
  // src/client.ts
852
+ var SERVER_STATUS_NODE_ID = opcjsBase.NodeId.newNumeric(0, 2256);
853
+ var NAMESPACE_ARRAY_NODE_ID = opcjsBase.NodeId.newNumeric(0, 2255);
854
+ var ESTIMATED_RETURN_TIME_NODE_ID = opcjsBase.NodeId.newNumeric(0, 2992);
855
+ var HAS_PROPERTY_REF_TYPE_ID = opcjsBase.NodeId.newNumeric(0, 46);
856
+ var KEEP_ALIVE_INTERVAL_MS = 25e3;
857
+ var OPC_UA_MIN_DATE_TIME_MS = -116444736e5;
353
858
  var Client = class {
354
859
  constructor(endpointUrl, configuration, identity) {
355
860
  this.configuration = configuration;
356
861
  this.identity = identity;
357
862
  this.endpointUrl = endpointUrl;
863
+ opcjsBase.initLoggerProvider(configuration.loggerFactory);
864
+ this.logger = opcjsBase.getLogger("Client");
358
865
  }
359
866
  endpointUrl;
360
- channel;
867
+ attributeService;
868
+ methodService;
869
+ browseService;
361
870
  session;
362
871
  subscriptionHandler;
872
+ logger;
873
+ // Stored after connect() so that refreshSession() and disconnect() can use them.
874
+ secureChannel;
875
+ secureChannelFacade;
876
+ ws;
877
+ sessionHandler;
878
+ keepAliveTimer;
879
+ /** Set to true while a shutdown-triggered reconnect is pending to avoid duplicate attempts. */
880
+ shutdownReconnectPending = false;
881
+ /** Most recently read NamespaceArray from the server (Session Client Renew NodeIds). */
882
+ namespaceTable;
883
+ /**
884
+ * Called whenever the server's NamespaceArray changes after a session (re-)establishment
885
+ * (OPC UA Part 4, Section 5.7.1 — Session Client Renew NodeIds conformance unit).
886
+ *
887
+ * Use `oldTable.remapNodeId(nodeId, newTable)` to recalculate cached NodeIds.
888
+ *
889
+ * @example
890
+ * ```ts
891
+ * client.onNamespaceTableChanged = (oldTable, newTable) => {
892
+ * cachedNodeId = oldTable.remapNodeId(cachedNodeId, newTable)
893
+ * }
894
+ * ```
895
+ */
896
+ onNamespaceTableChanged;
897
+ /**
898
+ * Called when the server sends `EstimatedReturnTime = MinDateTime`, indicating it does not
899
+ * expect to restart (OPC UA Part 5, Section 12.6 — Base Info Client Estimated Return Time
900
+ * conformance unit).
901
+ *
902
+ * When this fires the automatic reconnect is suppressed. The application is responsible for
903
+ * deciding whether to keep the `Client` instance or dispose it.
904
+ *
905
+ * @example
906
+ * ```ts
907
+ * client.onPermanentShutdown = () => {
908
+ * console.warn('Server will not restart — closing client.')
909
+ * }
910
+ * ```
911
+ */
912
+ onPermanentShutdown;
363
913
  getSession() {
364
914
  if (!this.session) {
365
915
  throw new Error("No session available");
366
916
  }
367
917
  return this.session;
368
918
  }
919
+ /**
920
+ * (Re-)initialises all session-scoped services from the current `this.session`.
921
+ * Called both after the initial `connect()` and after a session refresh.
922
+ */
923
+ initServices() {
924
+ const authToken = this.session.getAuthToken();
925
+ const sc = this.secureChannel;
926
+ this.attributeService = new AttributeService(authToken, sc);
927
+ this.methodService = new MethodService(authToken, sc);
928
+ this.browseService = new BrowseService(authToken, sc);
929
+ this.subscriptionHandler = new SubscriptionHandler(
930
+ new SubscriptionService(authToken, sc),
931
+ new MonitoredItemService(authToken, sc)
932
+ );
933
+ this.subscriptionHandler.onShutdown = () => this.handleServerShutdownDetected();
934
+ void this.refreshNamespaceTable();
935
+ }
936
+ /**
937
+ * Reads `Server.NamespaceArray` (ns=0, i=2255) and updates the stored
938
+ * `NamespaceTable`. When the table changes compared to the previous read the
939
+ * `onNamespaceTableChanged` callback is fired so the application can remap
940
+ * any cached NodeIds (OPC UA Part 4, Section 5.7.1 — Session Client Renew
941
+ * NodeIds conformance unit).
942
+ *
943
+ * Errors are logged as warnings and do not propagate to the caller.
944
+ */
945
+ async refreshNamespaceTable() {
946
+ if (!this.attributeService) return;
947
+ try {
948
+ const results = await this.attributeService.ReadValue([NAMESPACE_ARRAY_NODE_ID]);
949
+ const variant = results[0]?.value;
950
+ const uris = variant?.value;
951
+ if (!Array.isArray(uris)) {
952
+ this.logger.warn("NamespaceArray read returned an unexpected value; skipping table update.");
953
+ return;
954
+ }
955
+ const newTable = new NamespaceTable(uris);
956
+ const oldTable = this.namespaceTable;
957
+ this.namespaceTable = newTable;
958
+ if (oldTable !== void 0 && !oldTable.equals(newTable)) {
959
+ this.logger.info("NamespaceArray changed; notifying application to renew NodeIds.");
960
+ this.onNamespaceTableChanged?.(oldTable, newTable);
961
+ }
962
+ } catch (err) {
963
+ this.logger.warn("NamespaceArray read failed; namespace table not updated:", err);
964
+ }
965
+ }
966
+ /**
967
+ * Returns the most recently read `NamespaceTable` for this session.
968
+ *
969
+ * Available after `connect()` completes (the table is read as part of session
970
+ * establishment). Returns `undefined` before the first successful read.
971
+ */
972
+ getNamespaceTable() {
973
+ return this.namespaceTable;
974
+ }
975
+ /**
976
+ * Executes `fn` and, if it throws a `SessionInvalidError`, creates a fresh
977
+ * session and retries the operation exactly once.
978
+ *
979
+ * This covers the reactive case: a service call reveals that the server has
980
+ * already dropped the session (e.g. due to timeout). The new session is
981
+ * established transparently before re-running the original operation.
982
+ *
983
+ * For any other error (e.g. transport-level failures when the SecureChannel
984
+ * drops), this method attempts to reconnect the channel and reactivate the
985
+ * existing session first — falling back to a brand-new session only when
986
+ * reactivation fails — before retrying the operation once.
987
+ */
988
+ async withSessionRefresh(fn) {
989
+ try {
990
+ return await fn();
991
+ } catch (err) {
992
+ if (err instanceof SessionInvalidError) {
993
+ this.logger.info(`Session invalid (${err.statusCode.toString(16)}), refreshing session...`);
994
+ this.session = await this.sessionHandler.createNewSession(this.identity);
995
+ this.initServices();
996
+ this.logger.info("Session refreshed, retrying operation.");
997
+ return await fn();
998
+ }
999
+ this.logger.info("Service call failed, attempting channel reconnect and session reactivation...");
1000
+ try {
1001
+ await this.reconnectAndReactivate();
1002
+ this.initServices();
1003
+ this.logger.info("Reconnected successfully, retrying operation.");
1004
+ } catch (reconnectErr) {
1005
+ this.logger.warn("Channel reconnect failed:", reconnectErr);
1006
+ throw err;
1007
+ }
1008
+ return await fn();
1009
+ }
1010
+ }
1011
+ /**
1012
+ * Starts a periodic keep-alive timer that reads Server_ServerStatus when no subscription is
1013
+ * active. OPC UA Part 4, Section 5.7.1 requires clients to keep the session alive; when no
1014
+ * subscription Publish loop is running this is the only mechanism that does so.
1015
+ *
1016
+ * The keep-alive read also serves as the **Detect Shutdown** mechanism (Session Client Detect
1017
+ * Shutdown conformance unit): when the returned `ServerStatusDataType.state` equals
1018
+ * `ServerStateEnum.Shutdown` the client schedules a reconnect after
1019
+ * `SHUTDOWN_RECONNECT_DELAY_MS` to let the server finish its shutdown sequence.
1020
+ */
1021
+ startKeepAlive() {
1022
+ this.keepAliveTimer = setInterval(() => {
1023
+ if (this.subscriptionHandler?.hasActiveSubscription()) {
1024
+ return;
1025
+ }
1026
+ if (this.attributeService) {
1027
+ void this.attributeService.ReadValue([SERVER_STATUS_NODE_ID]).then((results) => {
1028
+ const statusData = results[0]?.value;
1029
+ if (statusData?.state === opcjsBase.ServerStateEnum.Shutdown) {
1030
+ this.handleServerShutdownDetected();
1031
+ }
1032
+ }).catch((err) => {
1033
+ this.logger.warn("Keep-alive read failed:", err);
1034
+ });
1035
+ }
1036
+ }, KEEP_ALIVE_INTERVAL_MS);
1037
+ }
1038
+ stopKeepAlive() {
1039
+ clearInterval(this.keepAliveTimer);
1040
+ this.keepAliveTimer = void 0;
1041
+ }
1042
+ /**
1043
+ * Called when a server-shutdown announcement is detected — either via the keep-alive read
1044
+ * returning `ServerStateEnum.Shutdown` or via a subscription `StatusChangeNotification`
1045
+ * with status `BadShutdown` / `BadServerHalted`.
1046
+ *
1047
+ * Reads `Server/ServerStatus/EstimatedReturnTime` (ns=0, i=2992) to decide how long to
1048
+ * wait before reconnecting (Base Info Client Estimated Return Time conformance unit).
1049
+ * Falls back to `configuration.shutdownReconnectDelayMs` when the read fails or the
1050
+ * attributed service is not yet available. Fires `onPermanentShutdown` and suppresses the
1051
+ * reconnect when the server sends `MinDateTime`.
1052
+ *
1053
+ * Only one reconnect attempt is scheduled at a time; a second detection while one is already
1054
+ * pending is silently ignored.
1055
+ */
1056
+ handleServerShutdownDetected() {
1057
+ if (this.shutdownReconnectPending) {
1058
+ return;
1059
+ }
1060
+ this.shutdownReconnectPending = true;
1061
+ this.stopKeepAlive();
1062
+ this.logger.warn("Server shutdown detected; reading EstimatedReturnTime...");
1063
+ void this.computeReconnectDelayMs().then((delayMs) => {
1064
+ if (delayMs === null) {
1065
+ this.logger.warn(
1066
+ "Server indicated it will not restart (MinDateTime). Firing onPermanentShutdown."
1067
+ );
1068
+ this.onPermanentShutdown?.();
1069
+ this.shutdownReconnectPending = false;
1070
+ return;
1071
+ }
1072
+ this.logger.warn(`Scheduling reconnect in ${delayMs} ms...`);
1073
+ setTimeout(() => {
1074
+ void this.reconnectAndReactivate().then(() => {
1075
+ this.initServices();
1076
+ this.startKeepAlive();
1077
+ this.logger.info("Reconnected after server shutdown.");
1078
+ }).catch((err) => {
1079
+ this.logger.warn("Reconnect after server shutdown failed:", err);
1080
+ }).finally(() => {
1081
+ this.shutdownReconnectPending = false;
1082
+ });
1083
+ }, delayMs);
1084
+ });
1085
+ }
1086
+ /**
1087
+ * Reads `Server/ServerStatus/EstimatedReturnTime` (ns=0, i=2992) and returns the reconnect
1088
+ * delay in milliseconds (Base Info Client Estimated Return Time — OPC UA Part 5, §12.6):
1089
+ *
1090
+ * - Valid future `DateTime` → delay = `estimatedReturnTime − now`, clamped to at least
1091
+ * `MIN_RECONNECT_DELAY_MS`.
1092
+ * - Past `DateTime` (server should already be available) → `MIN_RECONNECT_DELAY_MS`.
1093
+ * - OPC UA `MinDateTime` (server will not restart) → `null`.
1094
+ * - Unreadable / unavailable → falls back to `configuration.shutdownReconnectDelayMs`.
1095
+ */
1096
+ async computeReconnectDelayMs() {
1097
+ if (!this.attributeService) {
1098
+ return this.configuration.shutdownReconnectDelayMs;
1099
+ }
1100
+ try {
1101
+ const results = await this.attributeService.ReadValue([ESTIMATED_RETURN_TIME_NODE_ID]);
1102
+ const variant = results[0]?.value;
1103
+ const estimatedReturnTime = variant?.value;
1104
+ if (estimatedReturnTime instanceof Date && !isNaN(estimatedReturnTime.getTime())) {
1105
+ if (estimatedReturnTime.getTime() <= OPC_UA_MIN_DATE_TIME_MS) {
1106
+ return null;
1107
+ }
1108
+ const delayMs = estimatedReturnTime.getTime() - Date.now();
1109
+ if (delayMs > 0) {
1110
+ this.logger.info(
1111
+ `EstimatedReturnTime: ${estimatedReturnTime.toISOString()} (reconnect in ${delayMs} ms).`
1112
+ );
1113
+ return delayMs;
1114
+ }
1115
+ this.logger.info("EstimatedReturnTime is in the past; reconnecting immediately.");
1116
+ return this.configuration.minReconnectDelayMs;
1117
+ }
1118
+ } catch (err) {
1119
+ this.logger.debug("Failed to read EstimatedReturnTime; using configured delay:", err);
1120
+ }
1121
+ return this.configuration.shutdownReconnectDelayMs;
1122
+ }
369
1123
  async connect() {
370
- const channel = opcjsBase.ChannelFactory.createChannel(this.endpointUrl);
1124
+ const { ws, sc } = await this.openTransportAndChannel();
1125
+ this.secureChannel = sc;
1126
+ this.secureChannelFacade = sc;
1127
+ this.ws = ws;
1128
+ this.logger.debug("Creating session...");
1129
+ this.sessionHandler = new SessionHandler(sc, this.configuration);
1130
+ this.session = await this.sessionHandler.createNewSession(this.identity);
1131
+ this.logger.debug("Session created.");
1132
+ this.logger.debug("Initializing services...");
1133
+ this.initServices();
1134
+ this.startKeepAlive();
1135
+ }
1136
+ /**
1137
+ * Builds the full WebSocket → TCP → SecureChannel pipeline and returns the
1138
+ * two objects needed to drive it: the raw WebSocket facade (for teardown)
1139
+ * and the SecureChannelFacade (for service requests/session management).
1140
+ *
1141
+ * Extracted from `connect()` so it can be reused by `reconnectAndReactivate()`.
1142
+ */
1143
+ async openTransportAndChannel() {
1144
+ const wsOptions = { endpoint: this.endpointUrl };
1145
+ const ws = new opcjsBase.WebSocketFascade(wsOptions);
1146
+ const webSocketReadableStream = new opcjsBase.WebSocketReadableStream(ws, 1e3);
1147
+ const webSocketWritableStream = new opcjsBase.WebSocketWritableStream(ws);
1148
+ const scContext = new opcjsBase.SecureChannelContext(this.endpointUrl);
1149
+ const tcpMessageInjector = new opcjsBase.TcpMessageInjector();
1150
+ const tcpConnectionHandler = new opcjsBase.TcpConnectionHandler(tcpMessageInjector, scContext);
1151
+ const tcpMessageDecoupler = new opcjsBase.TcpMessageDecoupler(tcpConnectionHandler.onTcpMessage.bind(tcpConnectionHandler));
1152
+ const scMessageEncoder = new opcjsBase.SecureChannelMessageEncoder(scContext);
1153
+ const scTypeDecoder = new opcjsBase.SecureChannelTypeDecoder(
1154
+ this.configuration.decoder
1155
+ );
1156
+ const scMessageDecoder = new opcjsBase.SecureChannelMessageDecoder(scContext);
1157
+ const scTypeEncoder = new opcjsBase.SecureChannelTypeEncoder(
1158
+ this.configuration.encoder
1159
+ );
1160
+ const scChunkWriter = new opcjsBase.SecureChannelChunkWriter(scContext);
1161
+ const scChunkReader = new opcjsBase.SecureChannelChunkReader(scContext);
1162
+ webSocketReadableStream.pipeTo(tcpMessageDecoupler.writable);
1163
+ tcpMessageDecoupler.readable.pipeTo(scMessageDecoder.writable);
1164
+ scMessageDecoder.readable.pipeTo(scChunkReader.writable);
1165
+ scChunkReader.readable.pipeTo(scTypeDecoder.writable);
1166
+ scTypeEncoder.readable.pipeTo(scChunkWriter.writable);
1167
+ scChunkWriter.readable.pipeTo(scMessageEncoder.writable);
1168
+ scMessageEncoder.readable.pipeTo(tcpMessageInjector.writable);
1169
+ tcpMessageInjector.readable.pipeTo(webSocketWritableStream);
1170
+ const sc = new opcjsBase.SecureChannelFacade(scContext, scTypeDecoder, scTypeEncoder);
371
1171
  let connected = false;
372
1172
  while (!connected) {
373
- console.log(`Connecting to OPC UA server at ${this.endpointUrl}...`);
374
- connected = await channel.connect();
1173
+ this.logger.debug(`Connecting to OPC UA server at ${this.endpointUrl}...`);
1174
+ await ws.connect();
1175
+ this.logger.debug("WebSocket connection established, now establishing TCP connection...");
1176
+ connected = await tcpConnectionHandler.connect(this.endpointUrl);
375
1177
  if (!connected) {
376
- console.log("Connection failed, retrying in 2 seconds...");
1178
+ this.logger.info("Connection failed, retrying in 2 seconds...");
377
1179
  await new Promise((resolve) => setTimeout(resolve, 2e3));
378
1180
  }
379
1181
  }
380
- console.log("Connected to OPC UA server.");
381
- this.channel = new opcjsBase.SecureChannel(channel, this.configuration);
382
- await this.channel.openSecureChannelRequest();
383
- const sessionHandler = new SessionHandler(this.channel, this.configuration);
384
- this.session = await sessionHandler.createNewSession(this.identity);
385
- this.subscriptionHandler = new SubscriptionHandler(
386
- new SubscriptionService(this.session.getAuthToken(), this.channel),
387
- new MonitoredItemService(this.session.getAuthToken(), this.channel)
388
- );
1182
+ this.logger.info("Connected to OPC UA server.");
1183
+ this.logger.debug("Opening secure channel...");
1184
+ await sc.openSecureChannel();
1185
+ this.logger.debug("Secure channel established.");
1186
+ this.enforceChannelSecurityConfig(sc);
1187
+ return { ws, sc };
389
1188
  }
1189
+ /**
1190
+ * Validates the negotiated channel's security policy and mode against the
1191
+ * client's `SecurityConfiguration` (OPC UA Part 2, Security Administration).
1192
+ *
1193
+ * Throws if:
1194
+ * - `allowSecurityPolicyNone` is `false` and the channel uses SecurityPolicy None.
1195
+ * - `messageSecurityMode` is set and does not match the channel's actual mode.
1196
+ */
1197
+ enforceChannelSecurityConfig(sc) {
1198
+ const config = this.configuration.securityConfiguration;
1199
+ if (!config) return;
1200
+ const negotiatedPolicy = sc.getSecurityPolicy();
1201
+ const negotiatedMode = sc.getSecurityMode();
1202
+ if (config.allowSecurityPolicyNone === false && negotiatedPolicy === SECURITY_POLICY_NONE_URI) {
1203
+ throw new Error(
1204
+ "Connection refused: SecurityPolicy None is disabled by the client security configuration. Only SecurityPolicy None is currently supported by this client implementation."
1205
+ );
1206
+ }
1207
+ if (config.messageSecurityMode !== void 0 && config.messageSecurityMode !== negotiatedMode) {
1208
+ throw new Error(
1209
+ `Connection refused: negotiated MessageSecurityMode ${negotiatedMode} does not match the required mode ${config.messageSecurityMode} from the security configuration.`
1210
+ );
1211
+ }
1212
+ }
1213
+ /**
1214
+ * Tears down the current (dead) channel and establishes a fresh one, then
1215
+ * attempts to recover the existing OPC UA session via ActivateSession before
1216
+ * falling back to a full CreateSession + ActivateSession.
1217
+ *
1218
+ * OPC UA Part 4, Section 5.7.1 / Session Client Auto Reconnect conformance unit:
1219
+ * When the SecureChannel drops but the server-side session has not yet timed
1220
+ * out, the client SHOULD reuse the existing session by calling ActivateSession
1221
+ * on the new channel. Only if that fails should the client create a new session.
1222
+ */
1223
+ async reconnectAndReactivate() {
1224
+ this.logger.info("Tearing down dead channel before reconnect...");
1225
+ try {
1226
+ this.secureChannelFacade?.close();
1227
+ } catch {
1228
+ }
1229
+ try {
1230
+ this.ws?.close();
1231
+ } catch {
1232
+ }
1233
+ this.secureChannelFacade = void 0;
1234
+ this.secureChannel = void 0;
1235
+ this.ws = void 0;
1236
+ const { ws, sc } = await this.openTransportAndChannel();
1237
+ this.secureChannel = sc;
1238
+ this.secureChannelFacade = sc;
1239
+ this.ws = ws;
1240
+ this.sessionHandler = new SessionHandler(sc, this.configuration);
1241
+ if (this.session) {
1242
+ const reactivated = await this.sessionHandler.tryActivateExistingSession(
1243
+ this.session.getAuthToken(),
1244
+ this.session.getSessionId(),
1245
+ this.session.getEndpoint(),
1246
+ this.identity
1247
+ );
1248
+ if (reactivated !== null) {
1249
+ this.session = reactivated;
1250
+ this.logger.info("Existing session successfully reactivated on new channel.");
1251
+ return;
1252
+ }
1253
+ this.logger.info("ActivateSession for existing session failed; creating a fresh session...");
1254
+ }
1255
+ this.session = await this.sessionHandler.createNewSession(this.identity);
1256
+ this.logger.info("Fresh session established on new channel.");
1257
+ }
1258
+ /**
1259
+ * Gracefully disconnects from the OPC UA server.
1260
+ *
1261
+ * Sequence per OPC UA Part 4, Section 5.7.4:
1262
+ * 1. CloseSession (deleteSubscriptions=true) so the server frees all resources.
1263
+ * 2. Close the SecureChannel, which cancels the pending token-renewal timer.
1264
+ * 3. Close the WebSocket transport.
1265
+ *
1266
+ * CloseSession errors are swallowed so transport teardown always completes even
1267
+ * when the session has already expired on the server side.
1268
+ */
390
1269
  async disconnect() {
391
- console.log("Disconnecting from OPC UA server...");
392
- if (this.channel) {
393
- await this.channel.disconnect();
1270
+ this.logger.info("Disconnecting from OPC UA server...");
1271
+ this.stopKeepAlive();
1272
+ if (this.session && this.sessionHandler) {
1273
+ try {
1274
+ await this.sessionHandler.closeSession(true);
1275
+ } catch (err) {
1276
+ this.logger.warn("CloseSession failed (continuing teardown):", err);
1277
+ }
1278
+ this.session = void 0;
1279
+ }
1280
+ this.secureChannelFacade?.close();
1281
+ this.secureChannelFacade = void 0;
1282
+ this.secureChannel = void 0;
1283
+ this.ws?.close();
1284
+ this.ws = void 0;
1285
+ this.logger.info("Disconnected.");
1286
+ }
1287
+ /**
1288
+ * Reads the Value attribute of one or more Nodes.
1289
+ *
1290
+ * The returned object is a `Promise` that also exposes `requestHandle` — the
1291
+ * OPC UA `requestHandle` assigned to the underlying `ReadRequest`. The handle
1292
+ * is available synchronously (before `await`) so it can be passed to
1293
+ * `cancel()` to abort the in-flight request.
1294
+ *
1295
+ * @example
1296
+ * ```ts
1297
+ * const req = client.read([nodeId])
1298
+ * await client.cancel(req.requestHandle) // abort before response
1299
+ * const results = await req // ReadValueResult[]
1300
+ * ```
1301
+ */
1302
+ read(ids, options) {
1303
+ const requestHandle = nextRequestHandle();
1304
+ const promise = this.withSessionRefresh(async () => {
1305
+ const result = await this.attributeService?.ReadValue(ids, 0, void 0, options?.returnDiagnostics, requestHandle);
1306
+ return result?.map((r) => new ReadValueResult(r.value, r.statusCode, r.diagnosticInfo)) ?? [];
1307
+ });
1308
+ return Object.assign(promise, { requestHandle });
1309
+ }
1310
+ /**
1311
+ * Method for calling a single method on the server.
1312
+ *
1313
+ * The returned object is a `Promise` that also exposes `requestHandle` — the
1314
+ * OPC UA `requestHandle` assigned to the underlying `CallRequest`. The handle
1315
+ * is available synchronously (before `await`) so it can be passed to
1316
+ * `cancel()` to abort the in-flight request.
1317
+ *
1318
+ * @param objectId - NodeId of the Object that owns the method.
1319
+ * @param methodId - NodeId of the Method to invoke.
1320
+ * @param inputArguments - Input argument Variants (default: empty).
1321
+ * @param options - Request options (e.g. `returnDiagnostics`).
1322
+ * @returns A promise resolving to the CallMethodResult, with `requestHandle` available synchronously.
1323
+ */
1324
+ callMethod(objectId, methodId, inputArguments = [], options) {
1325
+ const requestHandle = nextRequestHandle();
1326
+ const promise = this.withSessionRefresh(async () => {
1327
+ const request = new opcjsBase.CallMethodRequest();
1328
+ request.objectId = objectId;
1329
+ request.methodId = methodId;
1330
+ request.inputArguments = inputArguments.map((arg) => opcjsBase.Variant.newFrom(arg));
1331
+ const responses = await this.methodService.call([request], options?.returnDiagnostics, requestHandle);
1332
+ const response = responses[0];
1333
+ return new CallMethodResult(response.value, response.statusCode, response.diagnosticInfo);
1334
+ });
1335
+ return Object.assign(promise, { requestHandle });
1336
+ }
1337
+ /**
1338
+ * Browses the Address Space starting from `nodeId`.
1339
+ *
1340
+ * The returned object is a `Promise` that also exposes `requestHandle` — the
1341
+ * OPC UA `requestHandle` assigned to the initial `BrowseRequest`. The handle
1342
+ * is available synchronously (before `await`) so it can be passed to
1343
+ * `cancel()` to abort the in-flight request.
1344
+ *
1345
+ * @param nodeId - Starting node.
1346
+ * @param recursive - When true, recursively follows HierarchicalReferences.
1347
+ * @param options - Request options (e.g. `returnDiagnostics`).
1348
+ * @returns A promise resolving to the list of referenced nodes, with `requestHandle` available synchronously.
1349
+ */
1350
+ browse(nodeId, recursive = false, options) {
1351
+ const requestHandle = nextRequestHandle();
1352
+ const promise = this.withSessionRefresh(() => {
1353
+ const visited = /* @__PURE__ */ new Set();
1354
+ return this.browseRecursive(nodeId, recursive, visited, options?.returnDiagnostics ?? 0, requestHandle);
1355
+ });
1356
+ return Object.assign(promise, { requestHandle });
1357
+ }
1358
+ async browseRecursive(nodeId, recursive, visited, returnDiagnostics, requestHandle) {
1359
+ const nodeKey = `${nodeId.namespace}:${nodeId.identifier}`;
1360
+ if (visited.has(nodeKey)) {
1361
+ return [];
1362
+ }
1363
+ visited.add(nodeKey);
1364
+ const description = new opcjsBase.BrowseDescription();
1365
+ description.nodeId = nodeId;
1366
+ description.browseDirection = opcjsBase.BrowseDirectionEnum.Forward;
1367
+ description.referenceTypeId = opcjsBase.NodeId.newNumeric(0, 33);
1368
+ description.includeSubtypes = true;
1369
+ description.nodeClassMask = 0;
1370
+ description.resultMask = opcjsBase.BrowseResultMaskEnum.All;
1371
+ const browseResults = await this.browseService.browse([description], returnDiagnostics, requestHandle);
1372
+ const browseResult = browseResults[0];
1373
+ const allReferences = [...browseResult.references ?? []];
1374
+ let continuationPoint = browseResult.continuationPoint;
1375
+ while (continuationPoint && continuationPoint.byteLength > 0) {
1376
+ const nextResults = await this.browseService.browseNext([continuationPoint], false, returnDiagnostics);
1377
+ const nextResult = nextResults[0];
1378
+ allReferences.push(...nextResult.references ?? []);
1379
+ continuationPoint = nextResult.continuationPoint;
1380
+ }
1381
+ const results = allReferences.map(
1382
+ (ref) => new BrowseNodeResult(
1383
+ ref.referenceTypeId,
1384
+ ref.isForward,
1385
+ ref.nodeId,
1386
+ ref.browseName,
1387
+ ref.displayName,
1388
+ ref.nodeClass,
1389
+ ref.typeDefinition
1390
+ )
1391
+ );
1392
+ if (recursive) {
1393
+ for (const ref of allReferences) {
1394
+ const childNodeId = opcjsBase.NodeId.newNumeric(
1395
+ ref.nodeId.nodeId.namespace,
1396
+ ref.nodeId.nodeId.identifier
1397
+ );
1398
+ const childResults = await this.browseRecursive(
1399
+ childNodeId,
1400
+ true,
1401
+ visited,
1402
+ returnDiagnostics
1403
+ );
1404
+ results.push(...childResults);
1405
+ }
1406
+ }
1407
+ return results;
1408
+ }
1409
+ async subscribe(ids, callback, options) {
1410
+ this.subscriptionHandler?.subscribe(ids, callback, options);
1411
+ }
1412
+ /**
1413
+ * Asks the server to cancel a pending service request
1414
+ * (OPC UA Part 4, Section 5.7.5 — Session Client Cancel conformance unit).
1415
+ *
1416
+ * The `requestHandle` uniquely identifies the pending request. It is the value
1417
+ * assigned to `RequestHeader.requestHandle` when the request was initially sent.
1418
+ * Service calls made through this client automatically assign monotonically
1419
+ * increasing handles, so the caller can capture the handle before or after issuing
1420
+ * Each method (`read`, `browse`, `callMethod`) returns a `Promise` with a
1421
+ * `requestHandle` property that is available synchronously. Pass that handle
1422
+ * here to abort the corresponding in-flight request.
1423
+ *
1424
+ * The server makes a best-effort attempt to cancel the matching request. Cancelled
1425
+ * requests complete with status `BadRequestCancelledByClient`. Not all servers
1426
+ * guarantee that a request in flight can be cancelled.
1427
+ *
1428
+ * @param requestHandle - Handle of the pending request to cancel.
1429
+ * @returns The number of pending requests the server actually cancelled.
1430
+ * @throws If no session is active or the server returns a non-Good status.
1431
+ *
1432
+ * @example
1433
+ * ```ts
1434
+ * // Issue a potentially slow operation and immediately cancel it.
1435
+ * const req = client.read([nodeId])
1436
+ * const cancelled = await client.cancel(req.requestHandle)
1437
+ * console.log(`Cancelled ${cancelled} request(s)`)
1438
+ * const results = await req // resolves with BadRequestCancelledByClient
1439
+ * ```
1440
+ */
1441
+ async cancel(requestHandle) {
1442
+ if (!this.sessionHandler) {
1443
+ throw new Error("Not connected: call connect() before cancel()");
1444
+ }
1445
+ return this.sessionHandler.cancel(requestHandle);
1446
+ }
1447
+ /**
1448
+ * Switches the active user identity for the current session by calling ActivateSession
1449
+ * with a new identity token (OPC UA Part 4, Section 5.7.3 — Session Client Impersonate
1450
+ * conformance unit).
1451
+ *
1452
+ * The server re-evaluates authorisation under the new identity while keeping all existing
1453
+ * Subscriptions and MonitoredItems intact. The new identity is also stored so that any
1454
+ * subsequent auto-reconnect or session refresh uses it instead of the original identity.
1455
+ *
1456
+ * @param identity - The new user identity to apply to the session.
1457
+ * @throws {Error} When not connected (call `connect()` first).
1458
+ * @throws {Error} When the server rejects the identity (e.g. `BadIdentityTokenRejected`
1459
+ * or `BadUserAccessDenied`).
1460
+ *
1461
+ * @example
1462
+ * ```ts
1463
+ * await client.connect()
1464
+ * // ... work as the original user ...
1465
+ * await client.impersonate(UserIdentity.newWithUserName('admin', 'secret'))
1466
+ * // ... subsequent calls run under the admin identity ...
1467
+ * ```
1468
+ */
1469
+ async impersonate(identity) {
1470
+ if (!this.session) {
1471
+ throw new Error("Not connected: call connect() before impersonate()");
394
1472
  }
1473
+ await this.session.impersonate(identity);
1474
+ this.identity = identity;
395
1475
  }
396
- async read(ids) {
397
- const service = new AttributeService(this.getSession().getAuthToken(), this.channel);
398
- const result = await service.ReadValue(ids.map((i) => i.toNodeId()));
399
- return result.map((r) => new ReadValueResult(r.value, r.status));
1476
+ /**
1477
+ * Reads the `SelectionListType` metadata for a Variable
1478
+ * (OPC UA Part 5, §7.18 Base Info Client Selection List conformance unit).
1479
+ *
1480
+ * The client browses the node's `HasProperty` references for `Selections`,
1481
+ * `SelectionDescriptions`, and `RestrictToList`, then reads their values in a
1482
+ * single batch Read request.
1483
+ *
1484
+ * Works transparently for instances of `SelectionListType` (ns=0; i=19726) and
1485
+ * any of its subtypes, because all subtypes inherit the `Selections` mandatory
1486
+ * property.
1487
+ *
1488
+ * @param nodeId - NodeId of the Variable to inspect.
1489
+ * @returns `SelectionList` when the node has a `Selections` property, `null` otherwise.
1490
+ * @throws When not connected or if the server returns a non-Good service status.
1491
+ *
1492
+ * @example
1493
+ * ```ts
1494
+ * const list = await client.getSelectionList(nodeId)
1495
+ * if (list) {
1496
+ * list.selectionDescriptions.forEach((desc, i) =>
1497
+ * console.log(`[${i}] ${desc.text}:`, list.selections[i])
1498
+ * )
1499
+ * }
1500
+ * ```
1501
+ */
1502
+ getSelectionList(nodeId) {
1503
+ return this.withSessionRefresh(() => this.fetchSelectionList(nodeId));
400
1504
  }
401
- async subscribe(ids, callback) {
402
- this.subscriptionHandler?.subscribe(ids, callback);
1505
+ /**
1506
+ * Internal implementation of `getSelectionList`. Browses the node's
1507
+ * HasProperty references to locate Selections/SelectionDescriptions/RestrictToList,
1508
+ * then batch-reads their values.
1509
+ */
1510
+ async fetchSelectionList(nodeId) {
1511
+ if (!this.browseService || !this.attributeService) {
1512
+ throw new Error("Not connected: call connect() before getSelectionList()");
1513
+ }
1514
+ const description = new opcjsBase.BrowseDescription();
1515
+ description.nodeId = nodeId;
1516
+ description.browseDirection = opcjsBase.BrowseDirectionEnum.Forward;
1517
+ description.referenceTypeId = HAS_PROPERTY_REF_TYPE_ID;
1518
+ description.includeSubtypes = false;
1519
+ description.nodeClassMask = 0;
1520
+ description.resultMask = opcjsBase.BrowseResultMaskEnum.All;
1521
+ const browseResults = await this.browseService.browse([description]);
1522
+ const refs = browseResults[0]?.references ?? [];
1523
+ let selectionsNodeId;
1524
+ let selectionsDescNodeId;
1525
+ let restrictToListNodeId;
1526
+ for (const ref of refs) {
1527
+ const name = ref.browseName?.name;
1528
+ if (name === "Selections") {
1529
+ selectionsNodeId = ref.nodeId.nodeId;
1530
+ } else if (name === "SelectionDescriptions") {
1531
+ selectionsDescNodeId = ref.nodeId.nodeId;
1532
+ } else if (name === "RestrictToList") {
1533
+ restrictToListNodeId = ref.nodeId.nodeId;
1534
+ }
1535
+ }
1536
+ if (!selectionsNodeId) {
1537
+ return null;
1538
+ }
1539
+ const nodeIdsToRead = [selectionsNodeId];
1540
+ if (selectionsDescNodeId) nodeIdsToRead.push(selectionsDescNodeId);
1541
+ if (restrictToListNodeId) nodeIdsToRead.push(restrictToListNodeId);
1542
+ const readResults = await this.attributeService.ReadValue(nodeIdsToRead);
1543
+ const selectionsVariant = readResults[0]?.value;
1544
+ const selections = Array.isArray(selectionsVariant?.value) ? selectionsVariant.value : [];
1545
+ let selectionDescriptions = [];
1546
+ let descReadIdx = 1;
1547
+ if (selectionsDescNodeId) {
1548
+ const descVariant = readResults[descReadIdx]?.value;
1549
+ if (Array.isArray(descVariant?.value)) {
1550
+ selectionDescriptions = descVariant.value;
1551
+ }
1552
+ descReadIdx++;
1553
+ }
1554
+ let restrictToList = false;
1555
+ if (restrictToListNodeId) {
1556
+ const rtlVariant = readResults[descReadIdx]?.value;
1557
+ if (typeof rtlVariant?.value === "boolean") {
1558
+ restrictToList = rtlVariant.value;
1559
+ }
1560
+ }
1561
+ return { nodeId, selections, selectionDescriptions, restrictToList };
1562
+ }
1563
+ /**
1564
+ * The `requestHandle` value that was assigned to the most recently issued
1565
+ * service request in this session.
1566
+ *
1567
+ * @deprecated Prefer accessing `requestHandle` directly on the promise returned
1568
+ * by `read()`, `browse()`, or `callMethod()`, which is available synchronously
1569
+ * before `await` and avoids relying on shared module state.
1570
+ *
1571
+ * Returns `0` before any request has been sent.
1572
+ */
1573
+ get lastRequestHandle() {
1574
+ return lastAssignedHandle();
403
1575
  }
404
1576
  };
405
1577
  var ConfigurationClient = class _ConfigurationClient extends opcjsBase.Configuration {
406
- static getSimple(name, company) {
1578
+ /**
1579
+ * Optional security restrictions applied during `Client.connect()`.
1580
+ * When not set, permissive defaults are used (SecurityPolicy None allowed,
1581
+ * all user-token types accepted).
1582
+ *
1583
+ * @see SecurityConfiguration
1584
+ */
1585
+ securityConfiguration;
1586
+ /**
1587
+ * How long to wait (ms) before attempting a reconnect after a server-shutdown
1588
+ * is detected via `ServerStatus/State = Shutdown` or a subscription
1589
+ * `StatusChangeNotification` with `BadShutdown` / `BadServerHalted`.
1590
+ *
1591
+ * Gives the server process time to exit fully before the client tries to
1592
+ * re-connect. Defaults to 5 000 ms.
1593
+ */
1594
+ shutdownReconnectDelayMs = 5e3;
1595
+ /**
1596
+ * Minimum reconnect delay in milliseconds used when
1597
+ * `Server/ServerStatus/EstimatedReturnTime` is already in the past (the server
1598
+ * should already be available again).
1599
+ *
1600
+ * Also acts as the lower bound for the ERT-derived delay, ensuring the client
1601
+ * always waits at least this long before retrying.
1602
+ *
1603
+ * Defaults to 1 000 ms.
1604
+ */
1605
+ minReconnectDelayMs = 1e3;
1606
+ static getSimple(name, company, loggerFactory) {
1607
+ if (!loggerFactory) {
1608
+ loggerFactory = new opcjsBase.LoggerFactory({
1609
+ defaultLevel: "DEBUG",
1610
+ //todo: use enum
1611
+ categoryLevels: {
1612
+ "transport.*": "TRACE",
1613
+ "secureChannel.*": "TRACE"
1614
+ }
1615
+ });
1616
+ }
407
1617
  const applicationUri = `urn:${company}:${name}`;
408
1618
  const productUri = `urn:${company}:${name}:product`;
409
1619
  const encoder = new opcjsBase.Encoder();
@@ -417,24 +1627,26 @@ var ConfigurationClient = class _ConfigurationClient extends opcjsBase.Configura
417
1627
  });
418
1628
  opcjsBase.registerTypeDecoders(decoder);
419
1629
  opcjsBase.registerBinaryDecoders(decoder);
420
- return new _ConfigurationClient(name, applicationUri, name, productUri, encoder, decoder);
421
- }
422
- constructor(applicationName, applicationUri, productName, productUri, encoder, decoder) {
423
- super(applicationName, applicationUri, productName, productUri, encoder, decoder);
424
- }
425
- };
426
- var Id = class _Id {
427
- constructor(nodeId) {
428
- this.nodeId = nodeId;
429
- }
430
- static newId(namespace, identifier) {
431
- return new _Id(new opcjsBase.NodeId(namespace, identifier));
432
- }
433
- toNodeId() {
434
- return this.nodeId;
1630
+ return new _ConfigurationClient(
1631
+ name,
1632
+ applicationUri,
1633
+ name,
1634
+ productUri,
1635
+ encoder,
1636
+ decoder,
1637
+ loggerFactory
1638
+ );
435
1639
  }
436
- toString() {
437
- return `${this.nodeId.namespace}:${this.nodeId.identifier}`;
1640
+ constructor(applicationName, applicationUri, productName, productUri, encoder, decoder, loggerFactory) {
1641
+ super(
1642
+ applicationName,
1643
+ applicationUri,
1644
+ productName,
1645
+ productUri,
1646
+ encoder,
1647
+ decoder,
1648
+ loggerFactory
1649
+ );
438
1650
  }
439
1651
  };
440
1652
  var UserIdentity = class _UserIdentity {
@@ -480,9 +1692,46 @@ var UserIdentity = class _UserIdentity {
480
1692
  }
481
1693
  };
482
1694
 
1695
+ // src/requestOptions.ts
1696
+ var ReturnDiagnosticsMask = {
1697
+ /** All service-level diagnostic fields. */
1698
+ ServiceLevel: 31,
1699
+ /** All operation-level diagnostic fields. */
1700
+ OperationLevel: 992,
1701
+ /** All diagnostic fields (service level + operation level). */
1702
+ All: 1023,
1703
+ /** Service-level: index to SymbolicId in the server string table. */
1704
+ ServiceSymbolicId: 1,
1705
+ /** Service-level: index to LocalizedText in the server string table. */
1706
+ ServiceLocalizedText: 2,
1707
+ /** Service-level: additional info string. */
1708
+ ServiceAdditionalInfo: 4,
1709
+ /** Service-level: inner status code. */
1710
+ ServiceInnerStatusCode: 8,
1711
+ /** Service-level: inner diagnostic info. */
1712
+ ServiceInnerDiagnostics: 16,
1713
+ /** Operation-level: index to SymbolicId in the server string table. */
1714
+ OperationSymbolicId: 32,
1715
+ /** Operation-level: index to LocalizedText in the server string table. */
1716
+ OperationLocalizedText: 64,
1717
+ /** Operation-level: additional info string. */
1718
+ OperationAdditionalInfo: 128,
1719
+ /** Operation-level: inner status code. */
1720
+ OperationInnerStatusCode: 256,
1721
+ /** Operation-level: inner diagnostic info. */
1722
+ OperationInnerDiagnostics: 512
1723
+ };
1724
+
1725
+ exports.BrowseNodeResult = BrowseNodeResult;
1726
+ exports.CERTIFICATE_REQUIRED_STATUS_CODES = CERTIFICATE_REQUIRED_STATUS_CODES;
1727
+ exports.CallMethodResult = CallMethodResult;
1728
+ exports.CertificateRequiredError = CertificateRequiredError;
483
1729
  exports.Client = Client;
484
1730
  exports.ConfigurationClient = ConfigurationClient;
485
- exports.Id = Id;
1731
+ exports.NamespaceTable = NamespaceTable;
1732
+ exports.ReturnDiagnosticsMask = ReturnDiagnosticsMask;
1733
+ exports.SECURITY_POLICY_NONE_URI = SECURITY_POLICY_NONE_URI;
1734
+ exports.SessionInvalidError = SessionInvalidError;
486
1735
  exports.UserIdentity = UserIdentity;
487
1736
  //# sourceMappingURL=index.cjs.map
488
1737
  //# sourceMappingURL=index.cjs.map