opcjs-client 0.1.20-alpha → 0.1.26-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 +141 -71
- 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 +142 -73
- 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, 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,66 @@ 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
328
|
const typeNodeId = notificationData.typeId;
|
|
296
|
-
if (typeNodeId.namespace === 0 && typeNodeId.identifier ===
|
|
329
|
+
if (typeNodeId.namespace === 0 && typeNodeId.identifier === NODE_ID_DATA_CHANGE_NOTIFICATION) {
|
|
297
330
|
const dataChangeNotification = decodedData;
|
|
298
331
|
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
|
-
}]);
|
|
332
|
+
const entry = this.entries.find((e) => e.handle === item.clientHandle);
|
|
333
|
+
entry?.callback([{ id: entry.id, value: item.value.value?.value }]);
|
|
306
334
|
}
|
|
335
|
+
} else if (typeNodeId.namespace === 0 && typeNodeId.identifier === NODE_ID_STATUS_CHANGE_NOTIFICATION) {
|
|
336
|
+
const statusChange = decodedData;
|
|
337
|
+
this.logger.warn(
|
|
338
|
+
`Subscription ${subscriptionId} status changed: 0x${statusChange.status?.toString(16).toUpperCase()}`
|
|
339
|
+
);
|
|
340
|
+
this.isRunning = false;
|
|
341
|
+
return;
|
|
307
342
|
} else {
|
|
308
|
-
this.logger.warn(
|
|
343
|
+
this.logger.warn(
|
|
344
|
+
`Notification data type ${typeNodeId.namespace}:${typeNodeId.identifier} is not supported.`
|
|
345
|
+
);
|
|
309
346
|
}
|
|
310
347
|
}
|
|
311
|
-
|
|
312
|
-
this.
|
|
313
|
-
}
|
|
348
|
+
if (moreNotifications) {
|
|
349
|
+
void this.publishLoop(nextAcknowledgements);
|
|
350
|
+
} else {
|
|
351
|
+
setTimeout(() => void this.publishLoop(nextAcknowledgements), 0);
|
|
352
|
+
}
|
|
314
353
|
}
|
|
315
354
|
};
|
|
316
355
|
var SubscriptionService = class extends ServiceBase {
|
|
@@ -424,10 +463,7 @@ var MethodService = class extends ServiceBase {
|
|
|
424
463
|
request.methodsToCall = methodsToCall;
|
|
425
464
|
this.logger.debug("Sending CallRequest...");
|
|
426
465
|
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
|
-
}
|
|
466
|
+
this.checkServiceResult(response.responseHeader?.serviceResult, "CallRequest");
|
|
431
467
|
return response.results.map((result) => ({
|
|
432
468
|
statusCode: result.statusCode ?? StatusCode.Good,
|
|
433
469
|
value: result.outputArguments.map((arg) => arg.value)
|
|
@@ -464,10 +500,7 @@ var BrowseService = class extends ServiceBase {
|
|
|
464
500
|
request.nodesToBrowse = nodesToBrowse;
|
|
465
501
|
this.logger.debug("Sending BrowseRequest...");
|
|
466
502
|
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
|
-
}
|
|
503
|
+
this.checkServiceResult(response.responseHeader?.serviceResult, "BrowseRequest");
|
|
471
504
|
return response.results ?? [];
|
|
472
505
|
}
|
|
473
506
|
/**
|
|
@@ -483,10 +516,7 @@ var BrowseService = class extends ServiceBase {
|
|
|
483
516
|
request.continuationPoints = continuationPoints;
|
|
484
517
|
this.logger.debug("Sending BrowseNextRequest...");
|
|
485
518
|
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
|
-
}
|
|
519
|
+
this.checkServiceResult(response.responseHeader?.serviceResult, "BrowseNextRequest");
|
|
490
520
|
return response.results ?? [];
|
|
491
521
|
}
|
|
492
522
|
constructor(authToken, secureChannel) {
|
|
@@ -523,12 +553,50 @@ var Client = class {
|
|
|
523
553
|
session;
|
|
524
554
|
subscriptionHandler;
|
|
525
555
|
logger;
|
|
556
|
+
// Stored after connect() so that refreshSession() can recreate services.
|
|
557
|
+
secureChannel;
|
|
558
|
+
sessionHandler;
|
|
526
559
|
getSession() {
|
|
527
560
|
if (!this.session) {
|
|
528
561
|
throw new Error("No session available");
|
|
529
562
|
}
|
|
530
563
|
return this.session;
|
|
531
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* (Re-)initialises all session-scoped services from the current `this.session`.
|
|
567
|
+
* Called both after the initial `connect()` and after a session refresh.
|
|
568
|
+
*/
|
|
569
|
+
initServices() {
|
|
570
|
+
const authToken = this.session.getAuthToken();
|
|
571
|
+
const sc = this.secureChannel;
|
|
572
|
+
this.attributeService = new AttributeService(authToken, sc);
|
|
573
|
+
this.methodService = new MethodService(authToken, sc);
|
|
574
|
+
this.browseService = new BrowseService(authToken, sc);
|
|
575
|
+
this.subscriptionHandler = new SubscriptionHandler(
|
|
576
|
+
new SubscriptionService(authToken, sc),
|
|
577
|
+
new MonitoredItemService(authToken, sc)
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Executes `fn` and, if it throws a `SessionInvalidError`, creates a fresh
|
|
582
|
+
* session and retries the operation exactly once.
|
|
583
|
+
*
|
|
584
|
+
* This covers the reactive case: a service call reveals that the server has
|
|
585
|
+
* already dropped the session (e.g. due to timeout). The new session is
|
|
586
|
+
* established transparently before re-running the original operation.
|
|
587
|
+
*/
|
|
588
|
+
async withSessionRefresh(fn) {
|
|
589
|
+
try {
|
|
590
|
+
return await fn();
|
|
591
|
+
} catch (err) {
|
|
592
|
+
if (!(err instanceof SessionInvalidError)) throw err;
|
|
593
|
+
this.logger.info(`Session invalid (${err.statusCode.toString(16)}), refreshing session...`);
|
|
594
|
+
this.session = await this.sessionHandler.createNewSession(this.identity);
|
|
595
|
+
this.initServices();
|
|
596
|
+
this.logger.info("Session refreshed, retrying operation.");
|
|
597
|
+
return await fn();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
532
600
|
async connect() {
|
|
533
601
|
const wsOptions = { endpoint: this.endpointUrl };
|
|
534
602
|
const ws = new WebSocketFascade(wsOptions);
|
|
@@ -573,24 +641,21 @@ var Client = class {
|
|
|
573
641
|
await sc.openSecureChannel();
|
|
574
642
|
this.logger.debug("Secure channel established.");
|
|
575
643
|
this.logger.debug("Creating session...");
|
|
576
|
-
|
|
577
|
-
this.
|
|
644
|
+
this.sessionHandler = new SessionHandler(sc, this.configuration);
|
|
645
|
+
this.secureChannel = sc;
|
|
646
|
+
this.session = await this.sessionHandler.createNewSession(this.identity);
|
|
578
647
|
this.logger.debug("Session created.");
|
|
579
648
|
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
|
-
);
|
|
649
|
+
this.initServices();
|
|
587
650
|
}
|
|
588
651
|
async disconnect() {
|
|
589
652
|
this.logger.info("Disconnecting from OPC UA server...");
|
|
590
653
|
}
|
|
591
654
|
async read(ids) {
|
|
592
|
-
|
|
593
|
-
|
|
655
|
+
return this.withSessionRefresh(async () => {
|
|
656
|
+
const result = await this.attributeService?.ReadValue(ids);
|
|
657
|
+
return result?.map((r) => new ReadValueResult(r.value, r.statusCode)) ?? [];
|
|
658
|
+
});
|
|
594
659
|
}
|
|
595
660
|
/**
|
|
596
661
|
* Method for calling a single method on the server.
|
|
@@ -600,17 +665,21 @@ var Client = class {
|
|
|
600
665
|
* @returns The CallMethodResult for the invoked method.
|
|
601
666
|
*/
|
|
602
667
|
async callMethod(objectId, methodId, inputArguments = []) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
668
|
+
return this.withSessionRefresh(async () => {
|
|
669
|
+
const request = new CallMethodRequest();
|
|
670
|
+
request.objectId = objectId;
|
|
671
|
+
request.methodId = methodId;
|
|
672
|
+
request.inputArguments = inputArguments.map((arg) => Variant.newFrom(arg));
|
|
673
|
+
const responses = await this.methodService.call([request]);
|
|
674
|
+
const response = responses[0];
|
|
675
|
+
return new CallMethodResult(response.value, response.statusCode);
|
|
676
|
+
});
|
|
610
677
|
}
|
|
611
678
|
async browse(nodeId, recursive = false) {
|
|
612
|
-
|
|
613
|
-
|
|
679
|
+
return this.withSessionRefresh(() => {
|
|
680
|
+
const visited = /* @__PURE__ */ new Set();
|
|
681
|
+
return this.browseRecursive(nodeId, recursive, visited);
|
|
682
|
+
});
|
|
614
683
|
}
|
|
615
684
|
async browseRecursive(nodeId, recursive, visited) {
|
|
616
685
|
const nodeKey = `${nodeId.namespace}:${nodeId.identifier}`;
|
|
@@ -756,6 +825,6 @@ var UserIdentity = class _UserIdentity {
|
|
|
756
825
|
}
|
|
757
826
|
};
|
|
758
827
|
|
|
759
|
-
export { BrowseNodeResult, Client, ConfigurationClient, UserIdentity };
|
|
828
|
+
export { BrowseNodeResult, Client, ConfigurationClient, SessionInvalidError, UserIdentity };
|
|
760
829
|
//# sourceMappingURL=index.js.map
|
|
761
830
|
//# sourceMappingURL=index.js.map
|