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.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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
296
|
-
|
|
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
|
|
300
|
-
|
|
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(
|
|
344
|
+
this.logger.warn(
|
|
345
|
+
`Notification data type ${typeNodeId.namespace}:${typeNodeId.identifier} is not supported.`
|
|
346
|
+
);
|
|
309
347
|
}
|
|
310
348
|
}
|
|
311
|
-
|
|
312
|
-
this.
|
|
313
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
this.
|
|
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.
|
|
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
|
-
|
|
593
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
613
|
-
|
|
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
|