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 +145 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +146 -76
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
-
|
|
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
|
-
|
|
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.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
298
|
-
|
|
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
|
|
302
|
-
|
|
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(
|
|
346
|
+
this.logger.warn(
|
|
347
|
+
`Notification data type ${typeNodeId.namespace}:${typeNodeId.identifier} is not supported.`
|
|
348
|
+
);
|
|
311
349
|
}
|
|
312
350
|
}
|
|
313
|
-
|
|
314
|
-
this.
|
|
315
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
579
|
-
this.
|
|
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.
|
|
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
|
-
|
|
595
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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
|