opcjs-client 0.1.39-alpha → 0.1.41-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/README.md +303 -0
- package/dist/index.cjs +678 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +496 -10
- package/dist/index.d.ts +496 -10
- package/dist/index.js +676 -44
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -13,6 +13,18 @@ var SessionInvalidError = class extends Error {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
// src/services/serviceBase.ts
|
|
16
|
+
var requestHandleCounter = 0;
|
|
17
|
+
function nextRequestHandle() {
|
|
18
|
+
if (requestHandleCounter >= 2147483647) {
|
|
19
|
+
requestHandleCounter = 1;
|
|
20
|
+
} else {
|
|
21
|
+
requestHandleCounter++;
|
|
22
|
+
}
|
|
23
|
+
return requestHandleCounter;
|
|
24
|
+
}
|
|
25
|
+
function lastAssignedHandle() {
|
|
26
|
+
return requestHandleCounter;
|
|
27
|
+
}
|
|
16
28
|
var ServiceBase = class {
|
|
17
29
|
constructor(authToken, secureChannel) {
|
|
18
30
|
this.authToken = authToken;
|
|
@@ -35,18 +47,43 @@ var ServiceBase = class {
|
|
|
35
47
|
}
|
|
36
48
|
throw new Error(`${context} failed: ${opcjsBase.StatusCodeToString(result)} (${opcjsBase.StatusCodeToStringNumber(result)})`);
|
|
37
49
|
}
|
|
38
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Builds a RequestHeader for an outgoing service request.
|
|
52
|
+
*
|
|
53
|
+
* @param returnDiagnostics - Bitmask of diagnostic fields to request from the
|
|
54
|
+
* server (OPC UA Part 4, §7.15). Use {@link ReturnDiagnosticsMask} constants
|
|
55
|
+
* to compose the value. Default `0` = no diagnostics.
|
|
56
|
+
*/
|
|
57
|
+
createRequestHeader(returnDiagnostics = 0, preAllocatedHandle) {
|
|
39
58
|
const requestHeader = new opcjsBase.RequestHeader();
|
|
40
59
|
requestHeader.authenticationToken = this.authToken;
|
|
41
60
|
requestHeader.timestamp = /* @__PURE__ */ new Date();
|
|
42
|
-
requestHeader.requestHandle =
|
|
43
|
-
requestHeader.returnDiagnostics =
|
|
61
|
+
requestHeader.requestHandle = preAllocatedHandle ?? nextRequestHandle();
|
|
62
|
+
requestHeader.returnDiagnostics = returnDiagnostics;
|
|
44
63
|
requestHeader.auditEntryId = "";
|
|
45
64
|
requestHeader.timeoutHint = 6e4;
|
|
46
65
|
requestHeader.additionalHeader = opcjsBase.ExtensionObject.newEmpty();
|
|
47
66
|
return requestHeader;
|
|
48
67
|
}
|
|
49
68
|
};
|
|
69
|
+
var CertificateRequiredError = class extends Error {
|
|
70
|
+
statusCode;
|
|
71
|
+
constructor(statusCode) {
|
|
72
|
+
super(
|
|
73
|
+
`Server requires a client certificate: ${opcjsBase.StatusCodeToString(statusCode)} (0x${statusCode.toString(16).toUpperCase().padStart(8, "0")})`
|
|
74
|
+
);
|
|
75
|
+
this.name = "CertificateRequiredError";
|
|
76
|
+
this.statusCode = statusCode;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var CERTIFICATE_REQUIRED_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
80
|
+
2148663296,
|
|
81
|
+
// BadCertificateInvalid
|
|
82
|
+
2148728832,
|
|
83
|
+
// BadSecurityChecksFailed
|
|
84
|
+
2153316352
|
|
85
|
+
// BadNoValidCertificates
|
|
86
|
+
]);
|
|
50
87
|
|
|
51
88
|
// src/services/sessionService.ts
|
|
52
89
|
var SessionService = class _SessionService extends ServiceBase {
|
|
@@ -57,9 +94,16 @@ var SessionService = class _SessionService extends ServiceBase {
|
|
|
57
94
|
logger = opcjsBase.getLogger("services.SessionService");
|
|
58
95
|
/**
|
|
59
96
|
* Creates a new session on the server (OPC UA Part 4, Section 5.7.2).
|
|
97
|
+
*
|
|
98
|
+
* @param clientCertificate - Optional DER-encoded client certificate to include in
|
|
99
|
+
* the request. Pass `null` (default) for SecurityPolicy None without a cert.
|
|
100
|
+
* When the server rejects a `null`-cert request with a certificate-related status
|
|
101
|
+
* code, the caller should retry with a `Uint8Array` certificate (OPC UA 1.0
|
|
102
|
+
* fallback — see `CertificateRequiredError`).
|
|
60
103
|
* @returns The session ID, authentication token, and selected server endpoint.
|
|
104
|
+
* @throws {CertificateRequiredError} when the server demands a client certificate.
|
|
61
105
|
*/
|
|
62
|
-
async createSession() {
|
|
106
|
+
async createSession(clientCertificate = null) {
|
|
63
107
|
this.logger.debug("Creating session...");
|
|
64
108
|
const clientDescription = new opcjsBase.ApplicationDescription();
|
|
65
109
|
clientDescription.applicationUri = this.configuration.applicationUri;
|
|
@@ -76,7 +120,7 @@ var SessionService = class _SessionService extends ServiceBase {
|
|
|
76
120
|
request.endpointUrl = this.secureChannel.getEndpointUrl();
|
|
77
121
|
request.sessionName = "";
|
|
78
122
|
request.clientNonce = null;
|
|
79
|
-
request.clientCertificate = null;
|
|
123
|
+
request.clientCertificate = clientCertificate ?? null;
|
|
80
124
|
request.requestedSessionTimeout = 6e4;
|
|
81
125
|
request.maxResponseMessageSize = 0;
|
|
82
126
|
this.logger.debug("Sending CreateSessionRequest...");
|
|
@@ -90,6 +134,9 @@ var SessionService = class _SessionService extends ServiceBase {
|
|
|
90
134
|
}
|
|
91
135
|
const serviceResult = castedResponse.responseHeader?.serviceResult;
|
|
92
136
|
if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
|
|
137
|
+
if (CERTIFICATE_REQUIRED_STATUS_CODES.has(serviceResult)) {
|
|
138
|
+
throw new CertificateRequiredError(serviceResult);
|
|
139
|
+
}
|
|
93
140
|
throw new Error(`CreateSessionRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
|
|
94
141
|
}
|
|
95
142
|
const clientConnectionUrl = new URL("opc." + this.secureChannel.getEndpointUrl());
|
|
@@ -139,6 +186,33 @@ var SessionService = class _SessionService extends ServiceBase {
|
|
|
139
186
|
}
|
|
140
187
|
this.logger.debug("Session activated.");
|
|
141
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Sends a CancelRequest to the server asking it to abandon the pending
|
|
191
|
+
* service request identified by `requestHandle` (OPC UA Part 4, Section 5.7.5).
|
|
192
|
+
*
|
|
193
|
+
* The server makes a best-effort attempt to cancel matching requests.
|
|
194
|
+
* Cancelled requests complete with a status of `BadRequestCancelledByClient`.
|
|
195
|
+
*
|
|
196
|
+
* @param requestHandle - The `requestHeader.requestHandle` value of the pending
|
|
197
|
+
* request to cancel. Pass `0` to attempt to cancel all outstanding requests
|
|
198
|
+
* (server behaviour for handle 0 is implementation-specific).
|
|
199
|
+
* @returns The number of pending requests that were actually cancelled
|
|
200
|
+
* (`CancelResponse.cancelCount`).
|
|
201
|
+
*/
|
|
202
|
+
async cancel(requestHandle) {
|
|
203
|
+
this.logger.debug(`Sending CancelRequest for requestHandle ${requestHandle}...`);
|
|
204
|
+
const request = new opcjsBase.CancelRequest();
|
|
205
|
+
request.requestHeader = this.createRequestHeader();
|
|
206
|
+
request.requestHandle = requestHandle;
|
|
207
|
+
const response = await this.secureChannel.issueServiceRequest(request);
|
|
208
|
+
const result = response?.responseHeader?.serviceResult;
|
|
209
|
+
if (result !== void 0 && result !== opcjsBase.StatusCode.Good) {
|
|
210
|
+
throw new Error(`CancelRequest failed: ${opcjsBase.StatusCodeToString(result)}`);
|
|
211
|
+
}
|
|
212
|
+
const cancelCount = response?.cancelCount ?? 0;
|
|
213
|
+
this.logger.debug(`CancelRequest completed; cancelCount = ${cancelCount}.`);
|
|
214
|
+
return cancelCount;
|
|
215
|
+
}
|
|
142
216
|
/**
|
|
143
217
|
* Closes the current session on the server (OPC UA Part 4, Section 5.7.4).
|
|
144
218
|
* @param deleteSubscriptions - When true the server deletes all Subscriptions
|
|
@@ -224,6 +298,22 @@ var Session = class {
|
|
|
224
298
|
getEndpoint() {
|
|
225
299
|
return this.endpoint;
|
|
226
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Switches the active user identity for this session by calling ActivateSession with
|
|
303
|
+
* a new identity token (OPC UA Part 4, Section 5.7.3 — Session Client Impersonate
|
|
304
|
+
* conformance unit).
|
|
305
|
+
*
|
|
306
|
+
* The server re-evaluates authorisation for the session under the new identity.
|
|
307
|
+
* All existing Subscriptions and MonitoredItems are preserved; only the security
|
|
308
|
+
* context changes.
|
|
309
|
+
*
|
|
310
|
+
* @param identity - The new user identity to apply to the session.
|
|
311
|
+
* @throws When the server returns a non-Good ServiceResult (e.g. `BadIdentityTokenRejected`
|
|
312
|
+
* or `BadUserAccessDenied`).
|
|
313
|
+
*/
|
|
314
|
+
async impersonate(identity) {
|
|
315
|
+
await this.activateSession(identity);
|
|
316
|
+
}
|
|
227
317
|
/**
|
|
228
318
|
* Closes the session on the server (OPC UA Part 4, Section 5.7.4).
|
|
229
319
|
* @param deleteSubscriptions - When true the server deletes all Subscriptions
|
|
@@ -243,10 +333,30 @@ var SessionHandler = class {
|
|
|
243
333
|
sessionServices;
|
|
244
334
|
logger = opcjsBase.getLogger("sessions.SessionHandler");
|
|
245
335
|
async createNewSession(identity) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
336
|
+
let sessionResult;
|
|
337
|
+
try {
|
|
338
|
+
sessionResult = await this.sessionServices.createSession(null);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
if (err instanceof CertificateRequiredError) {
|
|
341
|
+
const fallbackCert = this.configuration.securityConfiguration?.applicationInstanceCertificate;
|
|
342
|
+
if (fallbackCert) {
|
|
343
|
+
this.logger.info(
|
|
344
|
+
"Server requires a client certificate (OPC UA 1.0 fallback); retrying CreateSession with applicationInstanceCertificate."
|
|
345
|
+
);
|
|
346
|
+
sessionResult = await this.sessionServices.createSession(fallbackCert);
|
|
347
|
+
} else {
|
|
348
|
+
this.logger.warn(
|
|
349
|
+
"Server requires a client certificate but no applicationInstanceCertificate is configured in securityConfiguration. Cannot complete the 1.0 fallback."
|
|
350
|
+
);
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
this.sessionServices = this.sessionServices.recreate(sessionResult.authToken);
|
|
358
|
+
const session = new Session(sessionResult.sessionId, sessionResult.authToken, sessionResult.endpoint, this.sessionServices);
|
|
359
|
+
this.validateUserTokenPolicy(identity, sessionResult.endpoint);
|
|
250
360
|
await session.activateSession(identity);
|
|
251
361
|
return session;
|
|
252
362
|
}
|
|
@@ -278,6 +388,17 @@ var SessionHandler = class {
|
|
|
278
388
|
async closeSession(deleteSubscriptions = true) {
|
|
279
389
|
await this.sessionServices.closeSession(deleteSubscriptions);
|
|
280
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Sends a CancelRequest to the server to abandon a pending service call
|
|
393
|
+
* (OPC UA Part 4, Section 5.7.5).
|
|
394
|
+
*
|
|
395
|
+
* @param requestHandle - The `requestHandle` from the `RequestHeader` of the
|
|
396
|
+
* pending request to cancel.
|
|
397
|
+
* @returns The number of requests the server actually cancelled.
|
|
398
|
+
*/
|
|
399
|
+
async cancel(requestHandle) {
|
|
400
|
+
return this.sessionServices.cancel(requestHandle);
|
|
401
|
+
}
|
|
281
402
|
/**
|
|
282
403
|
* Validates the requested user-identity token type against:
|
|
283
404
|
* 1. The `allowedUserTokenTypes` from the client security configuration — the
|
|
@@ -320,9 +441,10 @@ var AttributeService = class extends ServiceBase {
|
|
|
320
441
|
* @param nodeIds - NodeIds of the Nodes to read.
|
|
321
442
|
* @param maxAge - Maximum age of the cached value in milliseconds the server may return. 0 = always current value.
|
|
322
443
|
* @param timestampsToReturn - Which timestamps to include in results. Default: Source.
|
|
323
|
-
* @
|
|
444
|
+
* @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
|
|
445
|
+
* @returns Array of results containing value, raw status code, and optional diagnostic info, one per requested NodeId.
|
|
324
446
|
*/
|
|
325
|
-
async ReadValue(nodeIds, maxAge = 0, timestampsToReturn = opcjsBase.TimestampsToReturnEnum.Source) {
|
|
447
|
+
async ReadValue(nodeIds, maxAge = 0, timestampsToReturn = opcjsBase.TimestampsToReturnEnum.Source, returnDiagnostics = 0, requestHandle) {
|
|
326
448
|
const readValueIds = nodeIds.map((ni) => {
|
|
327
449
|
const readValueId = new opcjsBase.ReadValueId();
|
|
328
450
|
readValueId.nodeId = ni;
|
|
@@ -332,7 +454,7 @@ var AttributeService = class extends ServiceBase {
|
|
|
332
454
|
return readValueId;
|
|
333
455
|
});
|
|
334
456
|
const request = new opcjsBase.ReadRequest();
|
|
335
|
-
request.requestHeader = this.createRequestHeader();
|
|
457
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
|
|
336
458
|
request.maxAge = maxAge;
|
|
337
459
|
request.timestampsToReturn = timestampsToReturn;
|
|
338
460
|
request.nodesToRead = readValueIds;
|
|
@@ -340,10 +462,13 @@ var AttributeService = class extends ServiceBase {
|
|
|
340
462
|
const response = await this.secureChannel.issueServiceRequest(request);
|
|
341
463
|
this.checkServiceResult(response.responseHeader?.serviceResult, "ReadRequest");
|
|
342
464
|
const results = new Array();
|
|
343
|
-
|
|
465
|
+
const diagInfos = response.diagnosticInfos ?? [];
|
|
466
|
+
for (let i = 0; i < (response.results ?? []).length; i++) {
|
|
467
|
+
const dataValue = response.results[i];
|
|
344
468
|
results.push({
|
|
345
469
|
statusCode: dataValue.statusCode ?? opcjsBase.StatusCode.Good,
|
|
346
|
-
value: dataValue.value
|
|
470
|
+
value: dataValue.value,
|
|
471
|
+
diagnosticInfo: diagInfos[i]
|
|
347
472
|
});
|
|
348
473
|
}
|
|
349
474
|
return results;
|
|
@@ -355,9 +480,10 @@ var AttributeService = class extends ServiceBase {
|
|
|
355
480
|
|
|
356
481
|
// src/readValueResult.ts
|
|
357
482
|
var ReadValueResult = class {
|
|
358
|
-
constructor(value, statusCode) {
|
|
483
|
+
constructor(value, statusCode, diagnosticInfo) {
|
|
359
484
|
this.value = value;
|
|
360
485
|
this.statusCode = statusCode;
|
|
486
|
+
this.diagnosticInfo = diagnosticInfo;
|
|
361
487
|
}
|
|
362
488
|
};
|
|
363
489
|
|
|
@@ -383,6 +509,17 @@ var SubscriptionHandler = class {
|
|
|
383
509
|
entries = new Array();
|
|
384
510
|
nextHandle = 0;
|
|
385
511
|
isRunning = false;
|
|
512
|
+
/** Guards against multiple concurrent publish loops. */
|
|
513
|
+
publishInFlight = false;
|
|
514
|
+
/**
|
|
515
|
+
* Optional callback invoked when the server announces a shutdown via a
|
|
516
|
+
* `StatusChangeNotification` with status `BadShutdown` or `BadServerHalted`
|
|
517
|
+
* (OPC UA Part 4, §5.13.6.2 — Session Client Detect Shutdown).
|
|
518
|
+
*
|
|
519
|
+
* Assign this before calling `subscribe()`. The client sets it automatically
|
|
520
|
+
* in `initServices()` to trigger a reconnect.
|
|
521
|
+
*/
|
|
522
|
+
onShutdown;
|
|
386
523
|
/** Returns true when at least one subscription is active and the publish loop is running. */
|
|
387
524
|
hasActiveSubscription() {
|
|
388
525
|
return this.isRunning && this.entries.length > 0;
|
|
@@ -405,14 +542,18 @@ var SubscriptionHandler = class {
|
|
|
405
542
|
// https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.5
|
|
406
543
|
async publishLoop(pendingAcknowledgements) {
|
|
407
544
|
if (!this.isRunning) return;
|
|
545
|
+
if (this.publishInFlight) return;
|
|
546
|
+
this.publishInFlight = true;
|
|
408
547
|
let response;
|
|
409
548
|
try {
|
|
410
549
|
response = await this.subscriptionService.publish(pendingAcknowledgements);
|
|
411
550
|
} catch (err) {
|
|
412
551
|
this.logger.error(`Publish failed, stopping publish loop: ${err}`);
|
|
413
552
|
this.isRunning = false;
|
|
553
|
+
this.publishInFlight = false;
|
|
414
554
|
return;
|
|
415
555
|
}
|
|
556
|
+
this.publishInFlight = false;
|
|
416
557
|
const { subscriptionId, availableSequenceNumbers, moreNotifications, notificationMessage } = response;
|
|
417
558
|
const notificationDatas = notificationMessage?.notificationData ?? [];
|
|
418
559
|
const seqNumber = notificationMessage?.sequenceNumber;
|
|
@@ -443,6 +584,11 @@ var SubscriptionHandler = class {
|
|
|
443
584
|
`Subscription ${subscriptionId} status changed: 0x${statusChange.status?.toString(16).toUpperCase()}`
|
|
444
585
|
);
|
|
445
586
|
this.isRunning = false;
|
|
587
|
+
const status = statusChange.status;
|
|
588
|
+
if (status === opcjsBase.StatusCode.BadShutdown || status === opcjsBase.StatusCode.BadServerHalted) {
|
|
589
|
+
this.logger.warn("Server shutdown announced via StatusChangeNotification \u2014 triggering reconnect.");
|
|
590
|
+
this.onShutdown?.();
|
|
591
|
+
}
|
|
446
592
|
return;
|
|
447
593
|
} else {
|
|
448
594
|
this.logger.warn(
|
|
@@ -560,18 +706,22 @@ var MethodService = class extends ServiceBase {
|
|
|
560
706
|
/**
|
|
561
707
|
* Calls one or more methods on the server (OPC UA Part 4, Section 5.11.2).
|
|
562
708
|
* @param methodsToCall - Array of CallMethodRequest describing each method to invoke.
|
|
563
|
-
* @
|
|
709
|
+
* @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
|
|
710
|
+
* @returns Array of results containing output argument values, raw status code, and optional diagnostic info,
|
|
711
|
+
* one per requested method.
|
|
564
712
|
*/
|
|
565
|
-
async call(methodsToCall) {
|
|
713
|
+
async call(methodsToCall, returnDiagnostics = 0, requestHandle) {
|
|
566
714
|
const request = new opcjsBase.CallRequest();
|
|
567
|
-
request.requestHeader = this.createRequestHeader();
|
|
715
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
|
|
568
716
|
request.methodsToCall = methodsToCall;
|
|
569
717
|
this.logger.debug("Sending CallRequest...");
|
|
570
718
|
const response = await this.secureChannel.issueServiceRequest(request);
|
|
571
719
|
this.checkServiceResult(response.responseHeader?.serviceResult, "CallRequest");
|
|
572
|
-
|
|
720
|
+
const diagInfos = response.diagnosticInfos ?? [];
|
|
721
|
+
return response.results.map((result, i) => ({
|
|
573
722
|
statusCode: result.statusCode ?? opcjsBase.StatusCode.Good,
|
|
574
|
-
value: result.outputArguments.map((arg) => arg.value)
|
|
723
|
+
value: result.outputArguments.map((arg) => arg.value),
|
|
724
|
+
diagnosticInfo: diagInfos[i]
|
|
575
725
|
}));
|
|
576
726
|
}
|
|
577
727
|
constructor(authToken, secureChannel) {
|
|
@@ -581,9 +731,10 @@ var MethodService = class extends ServiceBase {
|
|
|
581
731
|
|
|
582
732
|
// src/method/callMethodResult.ts
|
|
583
733
|
var CallMethodResult = class {
|
|
584
|
-
constructor(values, statusCode) {
|
|
734
|
+
constructor(values, statusCode, diagnosticInfo) {
|
|
585
735
|
this.values = values;
|
|
586
736
|
this.statusCode = statusCode;
|
|
737
|
+
this.diagnosticInfo = diagnosticInfo;
|
|
587
738
|
}
|
|
588
739
|
};
|
|
589
740
|
var BrowseService = class extends ServiceBase {
|
|
@@ -591,15 +742,16 @@ var BrowseService = class extends ServiceBase {
|
|
|
591
742
|
/**
|
|
592
743
|
* Browses one or more Nodes and returns their References (OPC UA Part 4, Section 5.9.2).
|
|
593
744
|
* @param nodesToBrowse - Array of BrowseDescriptions specifying nodes and filter criteria.
|
|
745
|
+
* @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
|
|
594
746
|
* @returns Array of BrowseResult, one per requested node.
|
|
595
747
|
*/
|
|
596
|
-
async browse(nodesToBrowse) {
|
|
748
|
+
async browse(nodesToBrowse, returnDiagnostics = 0, requestHandle) {
|
|
597
749
|
const view = new opcjsBase.ViewDescription();
|
|
598
750
|
view.viewId = opcjsBase.NodeId.newNumeric(0, 0);
|
|
599
751
|
view.timestamp = /* @__PURE__ */ new Date(-116444736e5);
|
|
600
752
|
view.viewVersion = 0;
|
|
601
753
|
const request = new opcjsBase.BrowseRequest();
|
|
602
|
-
request.requestHeader = this.createRequestHeader();
|
|
754
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
|
|
603
755
|
request.view = view;
|
|
604
756
|
request.requestedMaxReferencesPerNode = 0;
|
|
605
757
|
request.nodesToBrowse = nodesToBrowse;
|
|
@@ -612,11 +764,12 @@ var BrowseService = class extends ServiceBase {
|
|
|
612
764
|
* Continues a Browse operation using continuation points (OPC UA Part 4, Section 5.9.3).
|
|
613
765
|
* @param continuationPoints - Continuation points returned by a prior Browse or BrowseNext call.
|
|
614
766
|
* @param releaseContinuationPoints - If true, releases the continuation points without returning results.
|
|
767
|
+
* @param returnDiagnostics - Bitmask of diagnostic fields to request (OPC UA Part 4, §7.15). Default: 0.
|
|
615
768
|
* @returns Array of BrowseResult, one per continuation point.
|
|
616
769
|
*/
|
|
617
|
-
async browseNext(continuationPoints, releaseContinuationPoints) {
|
|
770
|
+
async browseNext(continuationPoints, releaseContinuationPoints, returnDiagnostics = 0) {
|
|
618
771
|
const request = new opcjsBase.BrowseNextRequest();
|
|
619
|
-
request.requestHeader = this.createRequestHeader();
|
|
772
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics);
|
|
620
773
|
request.releaseContinuationPoints = releaseContinuationPoints;
|
|
621
774
|
request.continuationPoints = continuationPoints;
|
|
622
775
|
this.logger.debug("Sending BrowseNextRequest...");
|
|
@@ -641,10 +794,67 @@ var BrowseNodeResult = class {
|
|
|
641
794
|
this.typeDefinition = typeDefinition;
|
|
642
795
|
}
|
|
643
796
|
};
|
|
797
|
+
var NamespaceTable = class {
|
|
798
|
+
uris;
|
|
799
|
+
constructor(uris = []) {
|
|
800
|
+
this.uris = Object.freeze([...uris]);
|
|
801
|
+
}
|
|
802
|
+
/** Returns the namespace URI at the given index, or `undefined` if out of range. */
|
|
803
|
+
getUri(index) {
|
|
804
|
+
return this.uris[index];
|
|
805
|
+
}
|
|
806
|
+
/** Returns the namespace index for the given URI, or `undefined` if not found. */
|
|
807
|
+
getIndex(uri) {
|
|
808
|
+
const idx = this.uris.indexOf(uri);
|
|
809
|
+
return idx >= 0 ? idx : void 0;
|
|
810
|
+
}
|
|
811
|
+
/** Returns the full ordered array of namespace URIs. */
|
|
812
|
+
getUris() {
|
|
813
|
+
return this.uris;
|
|
814
|
+
}
|
|
815
|
+
/** Returns `true` when both tables contain the same URIs in the same order. */
|
|
816
|
+
equals(other) {
|
|
817
|
+
if (this.uris.length !== other.uris.length) return false;
|
|
818
|
+
return this.uris.every((uri, i) => uri === other.uris[i]);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Remaps the namespace index of `nodeId` from this table to the equivalent
|
|
822
|
+
* index in `newTable` by matching namespace URIs.
|
|
823
|
+
*
|
|
824
|
+
* - Namespace index `0` (OPC UA base namespace) is always returned unchanged.
|
|
825
|
+
* - Returns the **same** `NodeId` instance when the index has not changed.
|
|
826
|
+
* - Returns a **new** `NodeId` instance with the updated index when remapping
|
|
827
|
+
* is required.
|
|
828
|
+
*
|
|
829
|
+
* @throws {Error} When the namespace URI at `nodeId.namespace` is not present
|
|
830
|
+
* in this (old) table or when the URI is not present in `newTable`.
|
|
831
|
+
*/
|
|
832
|
+
remapNodeId(nodeId, newTable) {
|
|
833
|
+
if (nodeId.namespace === 0) return nodeId;
|
|
834
|
+
const uri = this.getUri(nodeId.namespace);
|
|
835
|
+
if (uri === void 0) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`Cannot remap ${nodeId.toString()}: namespace index ${nodeId.namespace} is not present in the old NamespaceTable (length ${this.uris.length}).`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
const newIndex = newTable.getIndex(uri);
|
|
841
|
+
if (newIndex === void 0) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
`Cannot remap ${nodeId.toString()}: namespace URI '${uri}' is not present in the new NamespaceTable.`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
if (newIndex === nodeId.namespace) return nodeId;
|
|
847
|
+
return new opcjsBase.NodeId(newIndex, nodeId.identifier);
|
|
848
|
+
}
|
|
849
|
+
};
|
|
644
850
|
|
|
645
851
|
// src/client.ts
|
|
646
852
|
var SERVER_STATUS_NODE_ID = opcjsBase.NodeId.newNumeric(0, 2256);
|
|
853
|
+
var NAMESPACE_ARRAY_NODE_ID = opcjsBase.NodeId.newNumeric(0, 2255);
|
|
854
|
+
var ESTIMATED_RETURN_TIME_NODE_ID = opcjsBase.NodeId.newNumeric(0, 2992);
|
|
855
|
+
var HAS_PROPERTY_REF_TYPE_ID = opcjsBase.NodeId.newNumeric(0, 46);
|
|
647
856
|
var KEEP_ALIVE_INTERVAL_MS = 25e3;
|
|
857
|
+
var OPC_UA_MIN_DATE_TIME_MS = -116444736e5;
|
|
648
858
|
var Client = class {
|
|
649
859
|
constructor(endpointUrl, configuration, identity) {
|
|
650
860
|
this.configuration = configuration;
|
|
@@ -666,6 +876,40 @@ var Client = class {
|
|
|
666
876
|
ws;
|
|
667
877
|
sessionHandler;
|
|
668
878
|
keepAliveTimer;
|
|
879
|
+
/** Set to true while a shutdown-triggered reconnect is pending to avoid duplicate attempts. */
|
|
880
|
+
shutdownReconnectPending = false;
|
|
881
|
+
/** Most recently read NamespaceArray from the server (Session Client Renew NodeIds). */
|
|
882
|
+
namespaceTable;
|
|
883
|
+
/**
|
|
884
|
+
* Called whenever the server's NamespaceArray changes after a session (re-)establishment
|
|
885
|
+
* (OPC UA Part 4, Section 5.7.1 — Session Client Renew NodeIds conformance unit).
|
|
886
|
+
*
|
|
887
|
+
* Use `oldTable.remapNodeId(nodeId, newTable)` to recalculate cached NodeIds.
|
|
888
|
+
*
|
|
889
|
+
* @example
|
|
890
|
+
* ```ts
|
|
891
|
+
* client.onNamespaceTableChanged = (oldTable, newTable) => {
|
|
892
|
+
* cachedNodeId = oldTable.remapNodeId(cachedNodeId, newTable)
|
|
893
|
+
* }
|
|
894
|
+
* ```
|
|
895
|
+
*/
|
|
896
|
+
onNamespaceTableChanged;
|
|
897
|
+
/**
|
|
898
|
+
* Called when the server sends `EstimatedReturnTime = MinDateTime`, indicating it does not
|
|
899
|
+
* expect to restart (OPC UA Part 5, Section 12.6 — Base Info Client Estimated Return Time
|
|
900
|
+
* conformance unit).
|
|
901
|
+
*
|
|
902
|
+
* When this fires the automatic reconnect is suppressed. The application is responsible for
|
|
903
|
+
* deciding whether to keep the `Client` instance or dispose it.
|
|
904
|
+
*
|
|
905
|
+
* @example
|
|
906
|
+
* ```ts
|
|
907
|
+
* client.onPermanentShutdown = () => {
|
|
908
|
+
* console.warn('Server will not restart — closing client.')
|
|
909
|
+
* }
|
|
910
|
+
* ```
|
|
911
|
+
*/
|
|
912
|
+
onPermanentShutdown;
|
|
669
913
|
getSession() {
|
|
670
914
|
if (!this.session) {
|
|
671
915
|
throw new Error("No session available");
|
|
@@ -686,6 +930,47 @@ var Client = class {
|
|
|
686
930
|
new SubscriptionService(authToken, sc),
|
|
687
931
|
new MonitoredItemService(authToken, sc)
|
|
688
932
|
);
|
|
933
|
+
this.subscriptionHandler.onShutdown = () => this.handleServerShutdownDetected();
|
|
934
|
+
void this.refreshNamespaceTable();
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Reads `Server.NamespaceArray` (ns=0, i=2255) and updates the stored
|
|
938
|
+
* `NamespaceTable`. When the table changes compared to the previous read the
|
|
939
|
+
* `onNamespaceTableChanged` callback is fired so the application can remap
|
|
940
|
+
* any cached NodeIds (OPC UA Part 4, Section 5.7.1 — Session Client Renew
|
|
941
|
+
* NodeIds conformance unit).
|
|
942
|
+
*
|
|
943
|
+
* Errors are logged as warnings and do not propagate to the caller.
|
|
944
|
+
*/
|
|
945
|
+
async refreshNamespaceTable() {
|
|
946
|
+
if (!this.attributeService) return;
|
|
947
|
+
try {
|
|
948
|
+
const results = await this.attributeService.ReadValue([NAMESPACE_ARRAY_NODE_ID]);
|
|
949
|
+
const variant = results[0]?.value;
|
|
950
|
+
const uris = variant?.value;
|
|
951
|
+
if (!Array.isArray(uris)) {
|
|
952
|
+
this.logger.warn("NamespaceArray read returned an unexpected value; skipping table update.");
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const newTable = new NamespaceTable(uris);
|
|
956
|
+
const oldTable = this.namespaceTable;
|
|
957
|
+
this.namespaceTable = newTable;
|
|
958
|
+
if (oldTable !== void 0 && !oldTable.equals(newTable)) {
|
|
959
|
+
this.logger.info("NamespaceArray changed; notifying application to renew NodeIds.");
|
|
960
|
+
this.onNamespaceTableChanged?.(oldTable, newTable);
|
|
961
|
+
}
|
|
962
|
+
} catch (err) {
|
|
963
|
+
this.logger.warn("NamespaceArray read failed; namespace table not updated:", err);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Returns the most recently read `NamespaceTable` for this session.
|
|
968
|
+
*
|
|
969
|
+
* Available after `connect()` completes (the table is read as part of session
|
|
970
|
+
* establishment). Returns `undefined` before the first successful read.
|
|
971
|
+
*/
|
|
972
|
+
getNamespaceTable() {
|
|
973
|
+
return this.namespaceTable;
|
|
689
974
|
}
|
|
690
975
|
/**
|
|
691
976
|
* Executes `fn` and, if it throws a `SessionInvalidError`, creates a fresh
|
|
@@ -727,6 +1012,11 @@ var Client = class {
|
|
|
727
1012
|
* Starts a periodic keep-alive timer that reads Server_ServerStatus when no subscription is
|
|
728
1013
|
* active. OPC UA Part 4, Section 5.7.1 requires clients to keep the session alive; when no
|
|
729
1014
|
* subscription Publish loop is running this is the only mechanism that does so.
|
|
1015
|
+
*
|
|
1016
|
+
* The keep-alive read also serves as the **Detect Shutdown** mechanism (Session Client Detect
|
|
1017
|
+
* Shutdown conformance unit): when the returned `ServerStatusDataType.state` equals
|
|
1018
|
+
* `ServerStateEnum.Shutdown` the client schedules a reconnect after
|
|
1019
|
+
* `SHUTDOWN_RECONNECT_DELAY_MS` to let the server finish its shutdown sequence.
|
|
730
1020
|
*/
|
|
731
1021
|
startKeepAlive() {
|
|
732
1022
|
this.keepAliveTimer = setInterval(() => {
|
|
@@ -734,7 +1024,12 @@ var Client = class {
|
|
|
734
1024
|
return;
|
|
735
1025
|
}
|
|
736
1026
|
if (this.attributeService) {
|
|
737
|
-
void this.attributeService.ReadValue([SERVER_STATUS_NODE_ID]).
|
|
1027
|
+
void this.attributeService.ReadValue([SERVER_STATUS_NODE_ID]).then((results) => {
|
|
1028
|
+
const statusData = results[0]?.value;
|
|
1029
|
+
if (statusData?.state === opcjsBase.ServerStateEnum.Shutdown) {
|
|
1030
|
+
this.handleServerShutdownDetected();
|
|
1031
|
+
}
|
|
1032
|
+
}).catch((err) => {
|
|
738
1033
|
this.logger.warn("Keep-alive read failed:", err);
|
|
739
1034
|
});
|
|
740
1035
|
}
|
|
@@ -744,6 +1039,87 @@ var Client = class {
|
|
|
744
1039
|
clearInterval(this.keepAliveTimer);
|
|
745
1040
|
this.keepAliveTimer = void 0;
|
|
746
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Called when a server-shutdown announcement is detected — either via the keep-alive read
|
|
1044
|
+
* returning `ServerStateEnum.Shutdown` or via a subscription `StatusChangeNotification`
|
|
1045
|
+
* with status `BadShutdown` / `BadServerHalted`.
|
|
1046
|
+
*
|
|
1047
|
+
* Reads `Server/ServerStatus/EstimatedReturnTime` (ns=0, i=2992) to decide how long to
|
|
1048
|
+
* wait before reconnecting (Base Info Client Estimated Return Time conformance unit).
|
|
1049
|
+
* Falls back to `configuration.shutdownReconnectDelayMs` when the read fails or the
|
|
1050
|
+
* attributed service is not yet available. Fires `onPermanentShutdown` and suppresses the
|
|
1051
|
+
* reconnect when the server sends `MinDateTime`.
|
|
1052
|
+
*
|
|
1053
|
+
* Only one reconnect attempt is scheduled at a time; a second detection while one is already
|
|
1054
|
+
* pending is silently ignored.
|
|
1055
|
+
*/
|
|
1056
|
+
handleServerShutdownDetected() {
|
|
1057
|
+
if (this.shutdownReconnectPending) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
this.shutdownReconnectPending = true;
|
|
1061
|
+
this.stopKeepAlive();
|
|
1062
|
+
this.logger.warn("Server shutdown detected; reading EstimatedReturnTime...");
|
|
1063
|
+
void this.computeReconnectDelayMs().then((delayMs) => {
|
|
1064
|
+
if (delayMs === null) {
|
|
1065
|
+
this.logger.warn(
|
|
1066
|
+
"Server indicated it will not restart (MinDateTime). Firing onPermanentShutdown."
|
|
1067
|
+
);
|
|
1068
|
+
this.onPermanentShutdown?.();
|
|
1069
|
+
this.shutdownReconnectPending = false;
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
this.logger.warn(`Scheduling reconnect in ${delayMs} ms...`);
|
|
1073
|
+
setTimeout(() => {
|
|
1074
|
+
void this.reconnectAndReactivate().then(() => {
|
|
1075
|
+
this.initServices();
|
|
1076
|
+
this.startKeepAlive();
|
|
1077
|
+
this.logger.info("Reconnected after server shutdown.");
|
|
1078
|
+
}).catch((err) => {
|
|
1079
|
+
this.logger.warn("Reconnect after server shutdown failed:", err);
|
|
1080
|
+
}).finally(() => {
|
|
1081
|
+
this.shutdownReconnectPending = false;
|
|
1082
|
+
});
|
|
1083
|
+
}, delayMs);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Reads `Server/ServerStatus/EstimatedReturnTime` (ns=0, i=2992) and returns the reconnect
|
|
1088
|
+
* delay in milliseconds (Base Info Client Estimated Return Time — OPC UA Part 5, §12.6):
|
|
1089
|
+
*
|
|
1090
|
+
* - Valid future `DateTime` → delay = `estimatedReturnTime − now`, clamped to at least
|
|
1091
|
+
* `MIN_RECONNECT_DELAY_MS`.
|
|
1092
|
+
* - Past `DateTime` (server should already be available) → `MIN_RECONNECT_DELAY_MS`.
|
|
1093
|
+
* - OPC UA `MinDateTime` (server will not restart) → `null`.
|
|
1094
|
+
* - Unreadable / unavailable → falls back to `configuration.shutdownReconnectDelayMs`.
|
|
1095
|
+
*/
|
|
1096
|
+
async computeReconnectDelayMs() {
|
|
1097
|
+
if (!this.attributeService) {
|
|
1098
|
+
return this.configuration.shutdownReconnectDelayMs;
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
const results = await this.attributeService.ReadValue([ESTIMATED_RETURN_TIME_NODE_ID]);
|
|
1102
|
+
const variant = results[0]?.value;
|
|
1103
|
+
const estimatedReturnTime = variant?.value;
|
|
1104
|
+
if (estimatedReturnTime instanceof Date && !isNaN(estimatedReturnTime.getTime())) {
|
|
1105
|
+
if (estimatedReturnTime.getTime() <= OPC_UA_MIN_DATE_TIME_MS) {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
const delayMs = estimatedReturnTime.getTime() - Date.now();
|
|
1109
|
+
if (delayMs > 0) {
|
|
1110
|
+
this.logger.info(
|
|
1111
|
+
`EstimatedReturnTime: ${estimatedReturnTime.toISOString()} (reconnect in ${delayMs} ms).`
|
|
1112
|
+
);
|
|
1113
|
+
return delayMs;
|
|
1114
|
+
}
|
|
1115
|
+
this.logger.info("EstimatedReturnTime is in the past; reconnecting immediately.");
|
|
1116
|
+
return this.configuration.minReconnectDelayMs;
|
|
1117
|
+
}
|
|
1118
|
+
} catch (err) {
|
|
1119
|
+
this.logger.debug("Failed to read EstimatedReturnTime; using configured delay:", err);
|
|
1120
|
+
}
|
|
1121
|
+
return this.configuration.shutdownReconnectDelayMs;
|
|
1122
|
+
}
|
|
747
1123
|
async connect() {
|
|
748
1124
|
const { ws, sc } = await this.openTransportAndChannel();
|
|
749
1125
|
this.secureChannel = sc;
|
|
@@ -908,37 +1284,78 @@ var Client = class {
|
|
|
908
1284
|
this.ws = void 0;
|
|
909
1285
|
this.logger.info("Disconnected.");
|
|
910
1286
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1287
|
+
/**
|
|
1288
|
+
* Reads the Value attribute of one or more Nodes.
|
|
1289
|
+
*
|
|
1290
|
+
* The returned object is a `Promise` that also exposes `requestHandle` — the
|
|
1291
|
+
* OPC UA `requestHandle` assigned to the underlying `ReadRequest`. The handle
|
|
1292
|
+
* is available synchronously (before `await`) so it can be passed to
|
|
1293
|
+
* `cancel()` to abort the in-flight request.
|
|
1294
|
+
*
|
|
1295
|
+
* @example
|
|
1296
|
+
* ```ts
|
|
1297
|
+
* const req = client.read([nodeId])
|
|
1298
|
+
* await client.cancel(req.requestHandle) // abort before response
|
|
1299
|
+
* const results = await req // ReadValueResult[]
|
|
1300
|
+
* ```
|
|
1301
|
+
*/
|
|
1302
|
+
read(ids, options) {
|
|
1303
|
+
const requestHandle = nextRequestHandle();
|
|
1304
|
+
const promise = this.withSessionRefresh(async () => {
|
|
1305
|
+
const result = await this.attributeService?.ReadValue(ids, 0, void 0, options?.returnDiagnostics, requestHandle);
|
|
1306
|
+
return result?.map((r) => new ReadValueResult(r.value, r.statusCode, r.diagnosticInfo)) ?? [];
|
|
915
1307
|
});
|
|
1308
|
+
return Object.assign(promise, { requestHandle });
|
|
916
1309
|
}
|
|
917
1310
|
/**
|
|
918
1311
|
* Method for calling a single method on the server.
|
|
1312
|
+
*
|
|
1313
|
+
* The returned object is a `Promise` that also exposes `requestHandle` — the
|
|
1314
|
+
* OPC UA `requestHandle` assigned to the underlying `CallRequest`. The handle
|
|
1315
|
+
* is available synchronously (before `await`) so it can be passed to
|
|
1316
|
+
* `cancel()` to abort the in-flight request.
|
|
1317
|
+
*
|
|
919
1318
|
* @param objectId - NodeId of the Object that owns the method.
|
|
920
1319
|
* @param methodId - NodeId of the Method to invoke.
|
|
921
1320
|
* @param inputArguments - Input argument Variants (default: empty).
|
|
922
|
-
* @
|
|
1321
|
+
* @param options - Request options (e.g. `returnDiagnostics`).
|
|
1322
|
+
* @returns A promise resolving to the CallMethodResult, with `requestHandle` available synchronously.
|
|
923
1323
|
*/
|
|
924
|
-
|
|
925
|
-
|
|
1324
|
+
callMethod(objectId, methodId, inputArguments = [], options) {
|
|
1325
|
+
const requestHandle = nextRequestHandle();
|
|
1326
|
+
const promise = this.withSessionRefresh(async () => {
|
|
926
1327
|
const request = new opcjsBase.CallMethodRequest();
|
|
927
1328
|
request.objectId = objectId;
|
|
928
1329
|
request.methodId = methodId;
|
|
929
1330
|
request.inputArguments = inputArguments.map((arg) => opcjsBase.Variant.newFrom(arg));
|
|
930
|
-
const responses = await this.methodService.call([request]);
|
|
1331
|
+
const responses = await this.methodService.call([request], options?.returnDiagnostics, requestHandle);
|
|
931
1332
|
const response = responses[0];
|
|
932
|
-
return new CallMethodResult(response.value, response.statusCode);
|
|
1333
|
+
return new CallMethodResult(response.value, response.statusCode, response.diagnosticInfo);
|
|
933
1334
|
});
|
|
1335
|
+
return Object.assign(promise, { requestHandle });
|
|
934
1336
|
}
|
|
935
|
-
|
|
936
|
-
|
|
1337
|
+
/**
|
|
1338
|
+
* Browses the Address Space starting from `nodeId`.
|
|
1339
|
+
*
|
|
1340
|
+
* The returned object is a `Promise` that also exposes `requestHandle` — the
|
|
1341
|
+
* OPC UA `requestHandle` assigned to the initial `BrowseRequest`. The handle
|
|
1342
|
+
* is available synchronously (before `await`) so it can be passed to
|
|
1343
|
+
* `cancel()` to abort the in-flight request.
|
|
1344
|
+
*
|
|
1345
|
+
* @param nodeId - Starting node.
|
|
1346
|
+
* @param recursive - When true, recursively follows HierarchicalReferences.
|
|
1347
|
+
* @param options - Request options (e.g. `returnDiagnostics`).
|
|
1348
|
+
* @returns A promise resolving to the list of referenced nodes, with `requestHandle` available synchronously.
|
|
1349
|
+
*/
|
|
1350
|
+
browse(nodeId, recursive = false, options) {
|
|
1351
|
+
const requestHandle = nextRequestHandle();
|
|
1352
|
+
const promise = this.withSessionRefresh(() => {
|
|
937
1353
|
const visited = /* @__PURE__ */ new Set();
|
|
938
|
-
return this.browseRecursive(nodeId, recursive, visited);
|
|
1354
|
+
return this.browseRecursive(nodeId, recursive, visited, options?.returnDiagnostics ?? 0, requestHandle);
|
|
939
1355
|
});
|
|
1356
|
+
return Object.assign(promise, { requestHandle });
|
|
940
1357
|
}
|
|
941
|
-
async browseRecursive(nodeId, recursive, visited) {
|
|
1358
|
+
async browseRecursive(nodeId, recursive, visited, returnDiagnostics, requestHandle) {
|
|
942
1359
|
const nodeKey = `${nodeId.namespace}:${nodeId.identifier}`;
|
|
943
1360
|
if (visited.has(nodeKey)) {
|
|
944
1361
|
return [];
|
|
@@ -951,12 +1368,12 @@ var Client = class {
|
|
|
951
1368
|
description.includeSubtypes = true;
|
|
952
1369
|
description.nodeClassMask = 0;
|
|
953
1370
|
description.resultMask = opcjsBase.BrowseResultMaskEnum.All;
|
|
954
|
-
const browseResults = await this.browseService.browse([description]);
|
|
1371
|
+
const browseResults = await this.browseService.browse([description], returnDiagnostics, requestHandle);
|
|
955
1372
|
const browseResult = browseResults[0];
|
|
956
1373
|
const allReferences = [...browseResult.references ?? []];
|
|
957
1374
|
let continuationPoint = browseResult.continuationPoint;
|
|
958
1375
|
while (continuationPoint && continuationPoint.byteLength > 0) {
|
|
959
|
-
const nextResults = await this.browseService.browseNext([continuationPoint], false);
|
|
1376
|
+
const nextResults = await this.browseService.browseNext([continuationPoint], false, returnDiagnostics);
|
|
960
1377
|
const nextResult = nextResults[0];
|
|
961
1378
|
allReferences.push(...nextResult.references ?? []);
|
|
962
1379
|
continuationPoint = nextResult.continuationPoint;
|
|
@@ -981,7 +1398,8 @@ var Client = class {
|
|
|
981
1398
|
const childResults = await this.browseRecursive(
|
|
982
1399
|
childNodeId,
|
|
983
1400
|
true,
|
|
984
|
-
visited
|
|
1401
|
+
visited,
|
|
1402
|
+
returnDiagnostics
|
|
985
1403
|
);
|
|
986
1404
|
results.push(...childResults);
|
|
987
1405
|
}
|
|
@@ -991,6 +1409,170 @@ var Client = class {
|
|
|
991
1409
|
async subscribe(ids, callback, options) {
|
|
992
1410
|
this.subscriptionHandler?.subscribe(ids, callback, options);
|
|
993
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Asks the server to cancel a pending service request
|
|
1414
|
+
* (OPC UA Part 4, Section 5.7.5 — Session Client Cancel conformance unit).
|
|
1415
|
+
*
|
|
1416
|
+
* The `requestHandle` uniquely identifies the pending request. It is the value
|
|
1417
|
+
* assigned to `RequestHeader.requestHandle` when the request was initially sent.
|
|
1418
|
+
* Service calls made through this client automatically assign monotonically
|
|
1419
|
+
* increasing handles, so the caller can capture the handle before or after issuing
|
|
1420
|
+
* Each method (`read`, `browse`, `callMethod`) returns a `Promise` with a
|
|
1421
|
+
* `requestHandle` property that is available synchronously. Pass that handle
|
|
1422
|
+
* here to abort the corresponding in-flight request.
|
|
1423
|
+
*
|
|
1424
|
+
* The server makes a best-effort attempt to cancel the matching request. Cancelled
|
|
1425
|
+
* requests complete with status `BadRequestCancelledByClient`. Not all servers
|
|
1426
|
+
* guarantee that a request in flight can be cancelled.
|
|
1427
|
+
*
|
|
1428
|
+
* @param requestHandle - Handle of the pending request to cancel.
|
|
1429
|
+
* @returns The number of pending requests the server actually cancelled.
|
|
1430
|
+
* @throws If no session is active or the server returns a non-Good status.
|
|
1431
|
+
*
|
|
1432
|
+
* @example
|
|
1433
|
+
* ```ts
|
|
1434
|
+
* // Issue a potentially slow operation and immediately cancel it.
|
|
1435
|
+
* const req = client.read([nodeId])
|
|
1436
|
+
* const cancelled = await client.cancel(req.requestHandle)
|
|
1437
|
+
* console.log(`Cancelled ${cancelled} request(s)`)
|
|
1438
|
+
* const results = await req // resolves with BadRequestCancelledByClient
|
|
1439
|
+
* ```
|
|
1440
|
+
*/
|
|
1441
|
+
async cancel(requestHandle) {
|
|
1442
|
+
if (!this.sessionHandler) {
|
|
1443
|
+
throw new Error("Not connected: call connect() before cancel()");
|
|
1444
|
+
}
|
|
1445
|
+
return this.sessionHandler.cancel(requestHandle);
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Switches the active user identity for the current session by calling ActivateSession
|
|
1449
|
+
* with a new identity token (OPC UA Part 4, Section 5.7.3 — Session Client Impersonate
|
|
1450
|
+
* conformance unit).
|
|
1451
|
+
*
|
|
1452
|
+
* The server re-evaluates authorisation under the new identity while keeping all existing
|
|
1453
|
+
* Subscriptions and MonitoredItems intact. The new identity is also stored so that any
|
|
1454
|
+
* subsequent auto-reconnect or session refresh uses it instead of the original identity.
|
|
1455
|
+
*
|
|
1456
|
+
* @param identity - The new user identity to apply to the session.
|
|
1457
|
+
* @throws {Error} When not connected (call `connect()` first).
|
|
1458
|
+
* @throws {Error} When the server rejects the identity (e.g. `BadIdentityTokenRejected`
|
|
1459
|
+
* or `BadUserAccessDenied`).
|
|
1460
|
+
*
|
|
1461
|
+
* @example
|
|
1462
|
+
* ```ts
|
|
1463
|
+
* await client.connect()
|
|
1464
|
+
* // ... work as the original user ...
|
|
1465
|
+
* await client.impersonate(UserIdentity.newWithUserName('admin', 'secret'))
|
|
1466
|
+
* // ... subsequent calls run under the admin identity ...
|
|
1467
|
+
* ```
|
|
1468
|
+
*/
|
|
1469
|
+
async impersonate(identity) {
|
|
1470
|
+
if (!this.session) {
|
|
1471
|
+
throw new Error("Not connected: call connect() before impersonate()");
|
|
1472
|
+
}
|
|
1473
|
+
await this.session.impersonate(identity);
|
|
1474
|
+
this.identity = identity;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Reads the `SelectionListType` metadata for a Variable
|
|
1478
|
+
* (OPC UA Part 5, §7.18 — Base Info Client Selection List conformance unit).
|
|
1479
|
+
*
|
|
1480
|
+
* The client browses the node's `HasProperty` references for `Selections`,
|
|
1481
|
+
* `SelectionDescriptions`, and `RestrictToList`, then reads their values in a
|
|
1482
|
+
* single batch Read request.
|
|
1483
|
+
*
|
|
1484
|
+
* Works transparently for instances of `SelectionListType` (ns=0; i=19726) and
|
|
1485
|
+
* any of its subtypes, because all subtypes inherit the `Selections` mandatory
|
|
1486
|
+
* property.
|
|
1487
|
+
*
|
|
1488
|
+
* @param nodeId - NodeId of the Variable to inspect.
|
|
1489
|
+
* @returns `SelectionList` when the node has a `Selections` property, `null` otherwise.
|
|
1490
|
+
* @throws When not connected or if the server returns a non-Good service status.
|
|
1491
|
+
*
|
|
1492
|
+
* @example
|
|
1493
|
+
* ```ts
|
|
1494
|
+
* const list = await client.getSelectionList(nodeId)
|
|
1495
|
+
* if (list) {
|
|
1496
|
+
* list.selectionDescriptions.forEach((desc, i) =>
|
|
1497
|
+
* console.log(`[${i}] ${desc.text}:`, list.selections[i])
|
|
1498
|
+
* )
|
|
1499
|
+
* }
|
|
1500
|
+
* ```
|
|
1501
|
+
*/
|
|
1502
|
+
getSelectionList(nodeId) {
|
|
1503
|
+
return this.withSessionRefresh(() => this.fetchSelectionList(nodeId));
|
|
1504
|
+
}
|
|
1505
|
+
/**
|
|
1506
|
+
* Internal implementation of `getSelectionList`. Browses the node's
|
|
1507
|
+
* HasProperty references to locate Selections/SelectionDescriptions/RestrictToList,
|
|
1508
|
+
* then batch-reads their values.
|
|
1509
|
+
*/
|
|
1510
|
+
async fetchSelectionList(nodeId) {
|
|
1511
|
+
if (!this.browseService || !this.attributeService) {
|
|
1512
|
+
throw new Error("Not connected: call connect() before getSelectionList()");
|
|
1513
|
+
}
|
|
1514
|
+
const description = new opcjsBase.BrowseDescription();
|
|
1515
|
+
description.nodeId = nodeId;
|
|
1516
|
+
description.browseDirection = opcjsBase.BrowseDirectionEnum.Forward;
|
|
1517
|
+
description.referenceTypeId = HAS_PROPERTY_REF_TYPE_ID;
|
|
1518
|
+
description.includeSubtypes = false;
|
|
1519
|
+
description.nodeClassMask = 0;
|
|
1520
|
+
description.resultMask = opcjsBase.BrowseResultMaskEnum.All;
|
|
1521
|
+
const browseResults = await this.browseService.browse([description]);
|
|
1522
|
+
const refs = browseResults[0]?.references ?? [];
|
|
1523
|
+
let selectionsNodeId;
|
|
1524
|
+
let selectionsDescNodeId;
|
|
1525
|
+
let restrictToListNodeId;
|
|
1526
|
+
for (const ref of refs) {
|
|
1527
|
+
const name = ref.browseName?.name;
|
|
1528
|
+
if (name === "Selections") {
|
|
1529
|
+
selectionsNodeId = ref.nodeId.nodeId;
|
|
1530
|
+
} else if (name === "SelectionDescriptions") {
|
|
1531
|
+
selectionsDescNodeId = ref.nodeId.nodeId;
|
|
1532
|
+
} else if (name === "RestrictToList") {
|
|
1533
|
+
restrictToListNodeId = ref.nodeId.nodeId;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (!selectionsNodeId) {
|
|
1537
|
+
return null;
|
|
1538
|
+
}
|
|
1539
|
+
const nodeIdsToRead = [selectionsNodeId];
|
|
1540
|
+
if (selectionsDescNodeId) nodeIdsToRead.push(selectionsDescNodeId);
|
|
1541
|
+
if (restrictToListNodeId) nodeIdsToRead.push(restrictToListNodeId);
|
|
1542
|
+
const readResults = await this.attributeService.ReadValue(nodeIdsToRead);
|
|
1543
|
+
const selectionsVariant = readResults[0]?.value;
|
|
1544
|
+
const selections = Array.isArray(selectionsVariant?.value) ? selectionsVariant.value : [];
|
|
1545
|
+
let selectionDescriptions = [];
|
|
1546
|
+
let descReadIdx = 1;
|
|
1547
|
+
if (selectionsDescNodeId) {
|
|
1548
|
+
const descVariant = readResults[descReadIdx]?.value;
|
|
1549
|
+
if (Array.isArray(descVariant?.value)) {
|
|
1550
|
+
selectionDescriptions = descVariant.value;
|
|
1551
|
+
}
|
|
1552
|
+
descReadIdx++;
|
|
1553
|
+
}
|
|
1554
|
+
let restrictToList = false;
|
|
1555
|
+
if (restrictToListNodeId) {
|
|
1556
|
+
const rtlVariant = readResults[descReadIdx]?.value;
|
|
1557
|
+
if (typeof rtlVariant?.value === "boolean") {
|
|
1558
|
+
restrictToList = rtlVariant.value;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return { nodeId, selections, selectionDescriptions, restrictToList };
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* The `requestHandle` value that was assigned to the most recently issued
|
|
1565
|
+
* service request in this session.
|
|
1566
|
+
*
|
|
1567
|
+
* @deprecated Prefer accessing `requestHandle` directly on the promise returned
|
|
1568
|
+
* by `read()`, `browse()`, or `callMethod()`, which is available synchronously
|
|
1569
|
+
* before `await` and avoids relying on shared module state.
|
|
1570
|
+
*
|
|
1571
|
+
* Returns `0` before any request has been sent.
|
|
1572
|
+
*/
|
|
1573
|
+
get lastRequestHandle() {
|
|
1574
|
+
return lastAssignedHandle();
|
|
1575
|
+
}
|
|
994
1576
|
};
|
|
995
1577
|
var ConfigurationClient = class _ConfigurationClient extends opcjsBase.Configuration {
|
|
996
1578
|
/**
|
|
@@ -1001,6 +1583,26 @@ var ConfigurationClient = class _ConfigurationClient extends opcjsBase.Configura
|
|
|
1001
1583
|
* @see SecurityConfiguration
|
|
1002
1584
|
*/
|
|
1003
1585
|
securityConfiguration;
|
|
1586
|
+
/**
|
|
1587
|
+
* How long to wait (ms) before attempting a reconnect after a server-shutdown
|
|
1588
|
+
* is detected via `ServerStatus/State = Shutdown` or a subscription
|
|
1589
|
+
* `StatusChangeNotification` with `BadShutdown` / `BadServerHalted`.
|
|
1590
|
+
*
|
|
1591
|
+
* Gives the server process time to exit fully before the client tries to
|
|
1592
|
+
* re-connect. Defaults to 5 000 ms.
|
|
1593
|
+
*/
|
|
1594
|
+
shutdownReconnectDelayMs = 5e3;
|
|
1595
|
+
/**
|
|
1596
|
+
* Minimum reconnect delay in milliseconds used when
|
|
1597
|
+
* `Server/ServerStatus/EstimatedReturnTime` is already in the past (the server
|
|
1598
|
+
* should already be available again).
|
|
1599
|
+
*
|
|
1600
|
+
* Also acts as the lower bound for the ERT-derived delay, ensuring the client
|
|
1601
|
+
* always waits at least this long before retrying.
|
|
1602
|
+
*
|
|
1603
|
+
* Defaults to 1 000 ms.
|
|
1604
|
+
*/
|
|
1605
|
+
minReconnectDelayMs = 1e3;
|
|
1004
1606
|
static getSimple(name, company, loggerFactory) {
|
|
1005
1607
|
if (!loggerFactory) {
|
|
1006
1608
|
loggerFactory = new opcjsBase.LoggerFactory({
|
|
@@ -1090,10 +1692,44 @@ var UserIdentity = class _UserIdentity {
|
|
|
1090
1692
|
}
|
|
1091
1693
|
};
|
|
1092
1694
|
|
|
1695
|
+
// src/requestOptions.ts
|
|
1696
|
+
var ReturnDiagnosticsMask = {
|
|
1697
|
+
/** All service-level diagnostic fields. */
|
|
1698
|
+
ServiceLevel: 31,
|
|
1699
|
+
/** All operation-level diagnostic fields. */
|
|
1700
|
+
OperationLevel: 992,
|
|
1701
|
+
/** All diagnostic fields (service level + operation level). */
|
|
1702
|
+
All: 1023,
|
|
1703
|
+
/** Service-level: index to SymbolicId in the server string table. */
|
|
1704
|
+
ServiceSymbolicId: 1,
|
|
1705
|
+
/** Service-level: index to LocalizedText in the server string table. */
|
|
1706
|
+
ServiceLocalizedText: 2,
|
|
1707
|
+
/** Service-level: additional info string. */
|
|
1708
|
+
ServiceAdditionalInfo: 4,
|
|
1709
|
+
/** Service-level: inner status code. */
|
|
1710
|
+
ServiceInnerStatusCode: 8,
|
|
1711
|
+
/** Service-level: inner diagnostic info. */
|
|
1712
|
+
ServiceInnerDiagnostics: 16,
|
|
1713
|
+
/** Operation-level: index to SymbolicId in the server string table. */
|
|
1714
|
+
OperationSymbolicId: 32,
|
|
1715
|
+
/** Operation-level: index to LocalizedText in the server string table. */
|
|
1716
|
+
OperationLocalizedText: 64,
|
|
1717
|
+
/** Operation-level: additional info string. */
|
|
1718
|
+
OperationAdditionalInfo: 128,
|
|
1719
|
+
/** Operation-level: inner status code. */
|
|
1720
|
+
OperationInnerStatusCode: 256,
|
|
1721
|
+
/** Operation-level: inner diagnostic info. */
|
|
1722
|
+
OperationInnerDiagnostics: 512
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1093
1725
|
exports.BrowseNodeResult = BrowseNodeResult;
|
|
1726
|
+
exports.CERTIFICATE_REQUIRED_STATUS_CODES = CERTIFICATE_REQUIRED_STATUS_CODES;
|
|
1094
1727
|
exports.CallMethodResult = CallMethodResult;
|
|
1728
|
+
exports.CertificateRequiredError = CertificateRequiredError;
|
|
1095
1729
|
exports.Client = Client;
|
|
1096
1730
|
exports.ConfigurationClient = ConfigurationClient;
|
|
1731
|
+
exports.NamespaceTable = NamespaceTable;
|
|
1732
|
+
exports.ReturnDiagnosticsMask = ReturnDiagnosticsMask;
|
|
1097
1733
|
exports.SECURITY_POLICY_NONE_URI = SECURITY_POLICY_NONE_URI;
|
|
1098
1734
|
exports.SessionInvalidError = SessionInvalidError;
|
|
1099
1735
|
exports.UserIdentity = UserIdentity;
|