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