opcjs-client 0.1.20-alpha → 0.1.29-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,11 +1,38 @@
1
- import { initLoggerProvider, getLogger, WebSocketFascade, WebSocketReadableStream, WebSocketWritableStream, SecureChannelContext, TcpMessageInjector, TcpConnectionHandler, TcpMessageDecoupler, SecureChannelMesssageEncoder, SecureChannelTypeDecoder, SecureChannelMessageDecoder, SecureChannelTypeEncoder, SecureChannelChunkWriter, SecureChannelChunkReader, SecureChannelFacade, CallMethodRequest, Variant, BrowseDescription, BrowseDirectionEnum, NodeId, BrowseResultMaskEnum, Configuration, LoggerFactory, Encoder, BinaryWriter, registerEncoders, Decoder, BinaryReader, registerTypeDecoders, registerBinaryDecoders, UserTokenTypeEnum, AnonymousIdentityToken, UserNameIdentityToken, IssuedIdentityToken, TimestampsToReturnEnum, ReadValueId, QualifiedName, ReadRequest, StatusCode, StatusCodeToString, StatusCodeToStringNumber, CallRequest, ViewDescription, BrowseRequest, BrowseNextRequest, SubscriptionAcknowledgement, CreateSubscriptionRequest, PublishRequest, MonitoringParameters, ExtensionObject, MonitoredItemCreateRequest, MonitoringModeEnum, CreateMonitoredItemsRequest, ApplicationDescription, LocalizedText, ApplicationTypeEnum, CreateSessionRequest, CreateSessionResponse, SignatureData, ActivateSessionRequest, RequestHeader } from 'opcjs-base';
1
+ import { StatusCodeToString, initLoggerProvider, getLogger, WebSocketFascade, WebSocketReadableStream, WebSocketWritableStream, SecureChannelContext, TcpMessageInjector, TcpConnectionHandler, TcpMessageDecoupler, SecureChannelMesssageEncoder, SecureChannelTypeDecoder, SecureChannelMessageDecoder, SecureChannelTypeEncoder, SecureChannelChunkWriter, SecureChannelChunkReader, SecureChannelFacade, CallMethodRequest, Variant, BrowseDescription, BrowseDirectionEnum, NodeId, 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 } from 'opcjs-base';
2
2
 
3
3
  // src/client.ts
4
+ var SessionInvalidError = class extends Error {
5
+ statusCode;
6
+ constructor(statusCode) {
7
+ super(`Session is no longer valid: ${StatusCodeToString(statusCode)} (0x${statusCode.toString(16).toUpperCase()})`);
8
+ this.name = "SessionInvalidError";
9
+ this.statusCode = statusCode;
10
+ }
11
+ };
12
+
13
+ // src/services/serviceBase.ts
4
14
  var ServiceBase = class {
5
15
  constructor(authToken, secureChannel) {
6
16
  this.authToken = authToken;
7
17
  this.secureChannel = secureChannel;
8
18
  }
19
+ /**
20
+ * Validates the `serviceResult` value from a response header.
21
+ *
22
+ * Throws `SessionInvalidError` for session-related status codes so callers
23
+ * can detect a dropped session and act accordingly (e.g. reconnect).
24
+ * Throws a generic `Error` for all other non-Good codes.
25
+ *
26
+ * @param result - The `serviceResult` value from the response header.
27
+ * @param context - Short description used in the error message (e.g. "ReadRequest").
28
+ */
29
+ checkServiceResult(result, context) {
30
+ if (result === void 0 || result === StatusCode.Good) return;
31
+ if (result === StatusCode.BadSessionIdInvalid || result === StatusCode.BadSessionClosed) {
32
+ throw new SessionInvalidError(result);
33
+ }
34
+ throw new Error(`${context} failed: ${StatusCodeToString(result)} (${StatusCodeToStringNumber(result)})`);
35
+ }
9
36
  createRequestHeader() {
10
37
  const requestHeader = new RequestHeader();
11
38
  requestHeader.authenticationToken = this.authToken;
@@ -211,10 +238,7 @@ var AttributeService = class extends ServiceBase {
211
238
  request.nodesToRead = readValueIds;
212
239
  this.logger.debug("Sending ReadRequest...");
213
240
  const response = await this.secureChannel.issueServiceRequest(request);
214
- const serviceResult = response.responseHeader?.serviceResult;
215
- if (serviceResult !== void 0 && serviceResult !== StatusCode.Good) {
216
- throw new Error(`ReadRequest failed: ${StatusCodeToString(serviceResult)} (${StatusCodeToStringNumber(serviceResult)})`);
217
- }
241
+ this.checkServiceResult(response.responseHeader?.serviceResult, "ReadRequest");
218
242
  const results = new Array();
219
243
  for (const dataValue of response.results ?? []) {
220
244
  results.push({
@@ -248,6 +272,8 @@ var SubscriptionHandlerEntry = class {
248
272
  };
249
273
 
250
274
  // src/subscriptionHandler.ts
275
+ var NODE_ID_DATA_CHANGE_NOTIFICATION = 811;
276
+ var NODE_ID_STATUS_CHANGE_NOTIFICATION = 818;
251
277
  var SubscriptionHandler = class {
252
278
  constructor(subscriptionService, monitoredItemService) {
253
279
  this.subscriptionService = subscriptionService;
@@ -256,6 +282,7 @@ var SubscriptionHandler = class {
256
282
  logger = getLogger("SubscriptionHandler");
257
283
  entries = new Array();
258
284
  nextHandle = 0;
285
+ isRunning = false;
259
286
  async subscribe(ids, callback) {
260
287
  if (this.entries.length > 0) {
261
288
  throw new Error("Subscribing more than once is not implemented");
@@ -263,54 +290,67 @@ var SubscriptionHandler = class {
263
290
  const subscriptionId = await this.subscriptionService.createSubscription();
264
291
  const items = [];
265
292
  for (const id of ids) {
266
- const entry = new SubscriptionHandlerEntry(
267
- subscriptionId,
268
- this.nextHandle++,
269
- id,
270
- callback
271
- );
293
+ const entry = new SubscriptionHandlerEntry(subscriptionId, this.nextHandle++, id, callback);
272
294
  this.entries.push(entry);
273
- const item = {
274
- id,
275
- handle: entry.handle
276
- };
277
- items.push(item);
295
+ items.push({ id, handle: entry.handle });
278
296
  }
279
297
  await this.monitoredItemService.createMonitoredItems(subscriptionId, items);
280
- this.publish([]);
281
- }
282
- async publish(acknowledgeSequenceNumbers) {
283
- const acknowledgements = [];
284
- for (let i = 0; i < acknowledgeSequenceNumbers.length; i++) {
285
- const acknowledgement = new SubscriptionAcknowledgement();
286
- acknowledgement.subscriptionId = this.entries[i].subscriptionId;
287
- acknowledgement.sequenceNumber = acknowledgeSequenceNumbers[i];
288
- acknowledgements.push(acknowledgement);
298
+ this.isRunning = true;
299
+ void this.publishLoop([]);
300
+ }
301
+ // https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.5
302
+ async publishLoop(pendingAcknowledgements) {
303
+ if (!this.isRunning) return;
304
+ let response;
305
+ try {
306
+ response = await this.subscriptionService.publish(pendingAcknowledgements);
307
+ } catch (err) {
308
+ this.logger.error(`Publish failed, stopping publish loop: ${err}`);
309
+ this.isRunning = false;
310
+ return;
311
+ }
312
+ const { subscriptionId, availableSequenceNumbers, moreNotifications, notificationMessage } = response;
313
+ const notificationDatas = notificationMessage?.notificationData ?? [];
314
+ const seqNumber = notificationMessage?.sequenceNumber;
315
+ const nextAcknowledgements = [];
316
+ const isKeepAlive = notificationDatas.length === 0;
317
+ if (!isKeepAlive && seqNumber !== void 0) {
318
+ const isAvailable = !availableSequenceNumbers || availableSequenceNumbers.includes(seqNumber);
319
+ if (isAvailable) {
320
+ const ack = new SubscriptionAcknowledgement();
321
+ ack.subscriptionId = subscriptionId;
322
+ ack.sequenceNumber = seqNumber;
323
+ nextAcknowledgements.push(ack);
324
+ }
289
325
  }
290
- const response = await this.subscriptionService.publish(acknowledgements);
291
- const messagesToAcknowledge = response.notificationMessage.sequenceNumber;
292
- const notificationDatas = response.notificationMessage.notificationData;
293
326
  for (const notificationData of notificationDatas) {
294
327
  const decodedData = notificationData.data;
295
- const typeNodeId = notificationData.typeId;
296
- if (typeNodeId.namespace === 0 && typeNodeId.identifier === 811) {
328
+ const rawTypeId = notificationData.typeId;
329
+ const typeNodeId = rawTypeId instanceof ExpandedNodeId ? rawTypeId.nodeId : rawTypeId;
330
+ if (typeNodeId.namespace === 0 && typeNodeId.identifier === NODE_ID_DATA_CHANGE_NOTIFICATION) {
297
331
  const dataChangeNotification = decodedData;
298
332
  for (const item of dataChangeNotification.monitoredItems) {
299
- const clientHandle = item.clientHandle;
300
- const value = item.value;
301
- const entry = this.entries.find((e) => e.handle == clientHandle);
302
- entry?.callback([{
303
- id: entry.id,
304
- value: value.value?.value
305
- }]);
333
+ const entry = this.entries.find((e) => e.handle === item.clientHandle);
334
+ entry?.callback([{ id: entry.id, value: item.value.value?.value }]);
306
335
  }
336
+ } else if (typeNodeId.namespace === 0 && typeNodeId.identifier === NODE_ID_STATUS_CHANGE_NOTIFICATION) {
337
+ const statusChange = decodedData;
338
+ this.logger.warn(
339
+ `Subscription ${subscriptionId} status changed: 0x${statusChange.status?.toString(16).toUpperCase()}`
340
+ );
341
+ this.isRunning = false;
342
+ return;
307
343
  } else {
308
- this.logger.warn(`Notification data type ${typeNodeId.namespace}:${typeNodeId.identifier} is not supported.`);
344
+ this.logger.warn(
345
+ `Notification data type ${typeNodeId.namespace}:${typeNodeId.identifier} is not supported.`
346
+ );
309
347
  }
310
348
  }
311
- setTimeout(() => {
312
- this.publish([messagesToAcknowledge]);
313
- }, 500);
349
+ if (moreNotifications) {
350
+ void this.publishLoop(nextAcknowledgements);
351
+ } else {
352
+ setTimeout(() => void this.publishLoop(nextAcknowledgements), 0);
353
+ }
314
354
  }
315
355
  };
316
356
  var SubscriptionService = class extends ServiceBase {
@@ -424,10 +464,7 @@ var MethodService = class extends ServiceBase {
424
464
  request.methodsToCall = methodsToCall;
425
465
  this.logger.debug("Sending CallRequest...");
426
466
  const response = await this.secureChannel.issueServiceRequest(request);
427
- const serviceResult = response.responseHeader?.serviceResult;
428
- if (serviceResult !== void 0 && serviceResult !== StatusCode.Good) {
429
- throw new Error(`CallRequest failed: ${StatusCodeToString(serviceResult)} (${StatusCodeToStringNumber(serviceResult)})`);
430
- }
467
+ this.checkServiceResult(response.responseHeader?.serviceResult, "CallRequest");
431
468
  return response.results.map((result) => ({
432
469
  statusCode: result.statusCode ?? StatusCode.Good,
433
470
  value: result.outputArguments.map((arg) => arg.value)
@@ -464,10 +501,7 @@ var BrowseService = class extends ServiceBase {
464
501
  request.nodesToBrowse = nodesToBrowse;
465
502
  this.logger.debug("Sending BrowseRequest...");
466
503
  const response = await this.secureChannel.issueServiceRequest(request);
467
- const serviceResult = response.responseHeader?.serviceResult;
468
- if (serviceResult !== void 0 && serviceResult !== StatusCode.Good) {
469
- throw new Error(`BrowseRequest failed: ${StatusCodeToString(serviceResult)}`);
470
- }
504
+ this.checkServiceResult(response.responseHeader?.serviceResult, "BrowseRequest");
471
505
  return response.results ?? [];
472
506
  }
473
507
  /**
@@ -483,10 +517,7 @@ var BrowseService = class extends ServiceBase {
483
517
  request.continuationPoints = continuationPoints;
484
518
  this.logger.debug("Sending BrowseNextRequest...");
485
519
  const response = await this.secureChannel.issueServiceRequest(request);
486
- const serviceResult = response.responseHeader?.serviceResult;
487
- if (serviceResult !== void 0 && serviceResult !== StatusCode.Good) {
488
- throw new Error(`BrowseNextRequest failed: ${StatusCodeToString(serviceResult)}`);
489
- }
520
+ this.checkServiceResult(response.responseHeader?.serviceResult, "BrowseNextRequest");
490
521
  return response.results ?? [];
491
522
  }
492
523
  constructor(authToken, secureChannel) {
@@ -523,12 +554,50 @@ var Client = class {
523
554
  session;
524
555
  subscriptionHandler;
525
556
  logger;
557
+ // Stored after connect() so that refreshSession() can recreate services.
558
+ secureChannel;
559
+ sessionHandler;
526
560
  getSession() {
527
561
  if (!this.session) {
528
562
  throw new Error("No session available");
529
563
  }
530
564
  return this.session;
531
565
  }
566
+ /**
567
+ * (Re-)initialises all session-scoped services from the current `this.session`.
568
+ * Called both after the initial `connect()` and after a session refresh.
569
+ */
570
+ initServices() {
571
+ const authToken = this.session.getAuthToken();
572
+ const sc = this.secureChannel;
573
+ this.attributeService = new AttributeService(authToken, sc);
574
+ this.methodService = new MethodService(authToken, sc);
575
+ this.browseService = new BrowseService(authToken, sc);
576
+ this.subscriptionHandler = new SubscriptionHandler(
577
+ new SubscriptionService(authToken, sc),
578
+ new MonitoredItemService(authToken, sc)
579
+ );
580
+ }
581
+ /**
582
+ * Executes `fn` and, if it throws a `SessionInvalidError`, creates a fresh
583
+ * session and retries the operation exactly once.
584
+ *
585
+ * This covers the reactive case: a service call reveals that the server has
586
+ * already dropped the session (e.g. due to timeout). The new session is
587
+ * established transparently before re-running the original operation.
588
+ */
589
+ async withSessionRefresh(fn) {
590
+ try {
591
+ return await fn();
592
+ } catch (err) {
593
+ if (!(err instanceof SessionInvalidError)) throw err;
594
+ this.logger.info(`Session invalid (${err.statusCode.toString(16)}), refreshing session...`);
595
+ this.session = await this.sessionHandler.createNewSession(this.identity);
596
+ this.initServices();
597
+ this.logger.info("Session refreshed, retrying operation.");
598
+ return await fn();
599
+ }
600
+ }
532
601
  async connect() {
533
602
  const wsOptions = { endpoint: this.endpointUrl };
534
603
  const ws = new WebSocketFascade(wsOptions);
@@ -573,24 +642,21 @@ var Client = class {
573
642
  await sc.openSecureChannel();
574
643
  this.logger.debug("Secure channel established.");
575
644
  this.logger.debug("Creating session...");
576
- const sessionHandler = new SessionHandler(sc, this.configuration);
577
- this.session = await sessionHandler.createNewSession(this.identity);
645
+ this.sessionHandler = new SessionHandler(sc, this.configuration);
646
+ this.secureChannel = sc;
647
+ this.session = await this.sessionHandler.createNewSession(this.identity);
578
648
  this.logger.debug("Session created.");
579
649
  this.logger.debug("Initializing services...");
580
- this.attributeService = new AttributeService(this.session.getAuthToken(), sc);
581
- this.methodService = new MethodService(this.session.getAuthToken(), sc);
582
- this.browseService = new BrowseService(this.session.getAuthToken(), sc);
583
- this.subscriptionHandler = new SubscriptionHandler(
584
- new SubscriptionService(this.session.getAuthToken(), sc),
585
- new MonitoredItemService(this.session.getAuthToken(), sc)
586
- );
650
+ this.initServices();
587
651
  }
588
652
  async disconnect() {
589
653
  this.logger.info("Disconnecting from OPC UA server...");
590
654
  }
591
655
  async read(ids) {
592
- const result = await this.attributeService?.ReadValue(ids);
593
- return result?.map((r) => new ReadValueResult(r.value, r.statusCode)) || [];
656
+ return this.withSessionRefresh(async () => {
657
+ const result = await this.attributeService?.ReadValue(ids);
658
+ return result?.map((r) => new ReadValueResult(r.value, r.statusCode)) ?? [];
659
+ });
594
660
  }
595
661
  /**
596
662
  * Method for calling a single method on the server.
@@ -600,17 +666,21 @@ var Client = class {
600
666
  * @returns The CallMethodResult for the invoked method.
601
667
  */
602
668
  async callMethod(objectId, methodId, inputArguments = []) {
603
- const request = new CallMethodRequest();
604
- request.objectId = objectId;
605
- request.methodId = methodId;
606
- request.inputArguments = inputArguments.map((arg) => Variant.newFrom(arg));
607
- const responses = await this.methodService.call([request]);
608
- const response = responses[0];
609
- return new CallMethodResult(response.value, response.statusCode);
669
+ return this.withSessionRefresh(async () => {
670
+ const request = new CallMethodRequest();
671
+ request.objectId = objectId;
672
+ request.methodId = methodId;
673
+ request.inputArguments = inputArguments.map((arg) => Variant.newFrom(arg));
674
+ const responses = await this.methodService.call([request]);
675
+ const response = responses[0];
676
+ return new CallMethodResult(response.value, response.statusCode);
677
+ });
610
678
  }
611
679
  async browse(nodeId, recursive = false) {
612
- const visited = /* @__PURE__ */ new Set();
613
- return this.browseRecursive(nodeId, recursive, visited);
680
+ return this.withSessionRefresh(() => {
681
+ const visited = /* @__PURE__ */ new Set();
682
+ return this.browseRecursive(nodeId, recursive, visited);
683
+ });
614
684
  }
615
685
  async browseRecursive(nodeId, recursive, visited) {
616
686
  const nodeKey = `${nodeId.namespace}:${nodeId.identifier}`;
@@ -649,8 +719,8 @@ var Client = class {
649
719
  if (recursive) {
650
720
  for (const ref of allReferences) {
651
721
  const childNodeId = NodeId.newNumeric(
652
- ref.nodeId.namespace,
653
- ref.nodeId.identifier
722
+ ref.nodeId.nodeId.namespace,
723
+ ref.nodeId.nodeId.identifier
654
724
  );
655
725
  const childResults = await this.browseRecursive(
656
726
  childNodeId,
@@ -756,6 +826,6 @@ var UserIdentity = class _UserIdentity {
756
826
  }
757
827
  };
758
828
 
759
- export { BrowseNodeResult, Client, ConfigurationClient, UserIdentity };
829
+ export { BrowseNodeResult, Client, ConfigurationClient, SessionInvalidError, UserIdentity };
760
830
  //# sourceMappingURL=index.js.map
761
831
  //# sourceMappingURL=index.js.map