opcjs-client 0.1.40-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 +672 -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 +670 -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
|
|
|
@@ -385,6 +511,15 @@ var SubscriptionHandler = class {
|
|
|
385
511
|
isRunning = false;
|
|
386
512
|
/** Guards against multiple concurrent publish loops. */
|
|
387
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;
|
|
388
523
|
/** Returns true when at least one subscription is active and the publish loop is running. */
|
|
389
524
|
hasActiveSubscription() {
|
|
390
525
|
return this.isRunning && this.entries.length > 0;
|
|
@@ -449,6 +584,11 @@ var SubscriptionHandler = class {
|
|
|
449
584
|
`Subscription ${subscriptionId} status changed: 0x${statusChange.status?.toString(16).toUpperCase()}`
|
|
450
585
|
);
|
|
451
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
|
+
}
|
|
452
592
|
return;
|
|
453
593
|
} else {
|
|
454
594
|
this.logger.warn(
|
|
@@ -566,18 +706,22 @@ var MethodService = class extends ServiceBase {
|
|
|
566
706
|
/**
|
|
567
707
|
* Calls one or more methods on the server (OPC UA Part 4, Section 5.11.2).
|
|
568
708
|
* @param methodsToCall - Array of CallMethodRequest describing each method to invoke.
|
|
569
|
-
* @
|
|
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.
|
|
570
712
|
*/
|
|
571
|
-
async call(methodsToCall) {
|
|
713
|
+
async call(methodsToCall, returnDiagnostics = 0, requestHandle) {
|
|
572
714
|
const request = new opcjsBase.CallRequest();
|
|
573
|
-
request.requestHeader = this.createRequestHeader();
|
|
715
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
|
|
574
716
|
request.methodsToCall = methodsToCall;
|
|
575
717
|
this.logger.debug("Sending CallRequest...");
|
|
576
718
|
const response = await this.secureChannel.issueServiceRequest(request);
|
|
577
719
|
this.checkServiceResult(response.responseHeader?.serviceResult, "CallRequest");
|
|
578
|
-
|
|
720
|
+
const diagInfos = response.diagnosticInfos ?? [];
|
|
721
|
+
return response.results.map((result, i) => ({
|
|
579
722
|
statusCode: result.statusCode ?? opcjsBase.StatusCode.Good,
|
|
580
|
-
value: result.outputArguments.map((arg) => arg.value)
|
|
723
|
+
value: result.outputArguments.map((arg) => arg.value),
|
|
724
|
+
diagnosticInfo: diagInfos[i]
|
|
581
725
|
}));
|
|
582
726
|
}
|
|
583
727
|
constructor(authToken, secureChannel) {
|
|
@@ -587,9 +731,10 @@ var MethodService = class extends ServiceBase {
|
|
|
587
731
|
|
|
588
732
|
// src/method/callMethodResult.ts
|
|
589
733
|
var CallMethodResult = class {
|
|
590
|
-
constructor(values, statusCode) {
|
|
734
|
+
constructor(values, statusCode, diagnosticInfo) {
|
|
591
735
|
this.values = values;
|
|
592
736
|
this.statusCode = statusCode;
|
|
737
|
+
this.diagnosticInfo = diagnosticInfo;
|
|
593
738
|
}
|
|
594
739
|
};
|
|
595
740
|
var BrowseService = class extends ServiceBase {
|
|
@@ -597,15 +742,16 @@ var BrowseService = class extends ServiceBase {
|
|
|
597
742
|
/**
|
|
598
743
|
* Browses one or more Nodes and returns their References (OPC UA Part 4, Section 5.9.2).
|
|
599
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.
|
|
600
746
|
* @returns Array of BrowseResult, one per requested node.
|
|
601
747
|
*/
|
|
602
|
-
async browse(nodesToBrowse) {
|
|
748
|
+
async browse(nodesToBrowse, returnDiagnostics = 0, requestHandle) {
|
|
603
749
|
const view = new opcjsBase.ViewDescription();
|
|
604
750
|
view.viewId = opcjsBase.NodeId.newNumeric(0, 0);
|
|
605
751
|
view.timestamp = /* @__PURE__ */ new Date(-116444736e5);
|
|
606
752
|
view.viewVersion = 0;
|
|
607
753
|
const request = new opcjsBase.BrowseRequest();
|
|
608
|
-
request.requestHeader = this.createRequestHeader();
|
|
754
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
|
|
609
755
|
request.view = view;
|
|
610
756
|
request.requestedMaxReferencesPerNode = 0;
|
|
611
757
|
request.nodesToBrowse = nodesToBrowse;
|
|
@@ -618,11 +764,12 @@ var BrowseService = class extends ServiceBase {
|
|
|
618
764
|
* Continues a Browse operation using continuation points (OPC UA Part 4, Section 5.9.3).
|
|
619
765
|
* @param continuationPoints - Continuation points returned by a prior Browse or BrowseNext call.
|
|
620
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.
|
|
621
768
|
* @returns Array of BrowseResult, one per continuation point.
|
|
622
769
|
*/
|
|
623
|
-
async browseNext(continuationPoints, releaseContinuationPoints) {
|
|
770
|
+
async browseNext(continuationPoints, releaseContinuationPoints, returnDiagnostics = 0) {
|
|
624
771
|
const request = new opcjsBase.BrowseNextRequest();
|
|
625
|
-
request.requestHeader = this.createRequestHeader();
|
|
772
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics);
|
|
626
773
|
request.releaseContinuationPoints = releaseContinuationPoints;
|
|
627
774
|
request.continuationPoints = continuationPoints;
|
|
628
775
|
this.logger.debug("Sending BrowseNextRequest...");
|
|
@@ -647,10 +794,67 @@ var BrowseNodeResult = class {
|
|
|
647
794
|
this.typeDefinition = typeDefinition;
|
|
648
795
|
}
|
|
649
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
|
+
};
|
|
650
850
|
|
|
651
851
|
// src/client.ts
|
|
652
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);
|
|
653
856
|
var KEEP_ALIVE_INTERVAL_MS = 25e3;
|
|
857
|
+
var OPC_UA_MIN_DATE_TIME_MS = -116444736e5;
|
|
654
858
|
var Client = class {
|
|
655
859
|
constructor(endpointUrl, configuration, identity) {
|
|
656
860
|
this.configuration = configuration;
|
|
@@ -672,6 +876,40 @@ var Client = class {
|
|
|
672
876
|
ws;
|
|
673
877
|
sessionHandler;
|
|
674
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;
|
|
675
913
|
getSession() {
|
|
676
914
|
if (!this.session) {
|
|
677
915
|
throw new Error("No session available");
|
|
@@ -692,6 +930,47 @@ var Client = class {
|
|
|
692
930
|
new SubscriptionService(authToken, sc),
|
|
693
931
|
new MonitoredItemService(authToken, sc)
|
|
694
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;
|
|
695
974
|
}
|
|
696
975
|
/**
|
|
697
976
|
* Executes `fn` and, if it throws a `SessionInvalidError`, creates a fresh
|
|
@@ -733,6 +1012,11 @@ var Client = class {
|
|
|
733
1012
|
* Starts a periodic keep-alive timer that reads Server_ServerStatus when no subscription is
|
|
734
1013
|
* active. OPC UA Part 4, Section 5.7.1 requires clients to keep the session alive; when no
|
|
735
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.
|
|
736
1020
|
*/
|
|
737
1021
|
startKeepAlive() {
|
|
738
1022
|
this.keepAliveTimer = setInterval(() => {
|
|
@@ -740,7 +1024,12 @@ var Client = class {
|
|
|
740
1024
|
return;
|
|
741
1025
|
}
|
|
742
1026
|
if (this.attributeService) {
|
|
743
|
-
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) => {
|
|
744
1033
|
this.logger.warn("Keep-alive read failed:", err);
|
|
745
1034
|
});
|
|
746
1035
|
}
|
|
@@ -750,6 +1039,87 @@ var Client = class {
|
|
|
750
1039
|
clearInterval(this.keepAliveTimer);
|
|
751
1040
|
this.keepAliveTimer = void 0;
|
|
752
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
|
+
}
|
|
753
1123
|
async connect() {
|
|
754
1124
|
const { ws, sc } = await this.openTransportAndChannel();
|
|
755
1125
|
this.secureChannel = sc;
|
|
@@ -914,37 +1284,78 @@ var Client = class {
|
|
|
914
1284
|
this.ws = void 0;
|
|
915
1285
|
this.logger.info("Disconnected.");
|
|
916
1286
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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)) ?? [];
|
|
921
1307
|
});
|
|
1308
|
+
return Object.assign(promise, { requestHandle });
|
|
922
1309
|
}
|
|
923
1310
|
/**
|
|
924
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
|
+
*
|
|
925
1318
|
* @param objectId - NodeId of the Object that owns the method.
|
|
926
1319
|
* @param methodId - NodeId of the Method to invoke.
|
|
927
1320
|
* @param inputArguments - Input argument Variants (default: empty).
|
|
928
|
-
* @
|
|
1321
|
+
* @param options - Request options (e.g. `returnDiagnostics`).
|
|
1322
|
+
* @returns A promise resolving to the CallMethodResult, with `requestHandle` available synchronously.
|
|
929
1323
|
*/
|
|
930
|
-
|
|
931
|
-
|
|
1324
|
+
callMethod(objectId, methodId, inputArguments = [], options) {
|
|
1325
|
+
const requestHandle = nextRequestHandle();
|
|
1326
|
+
const promise = this.withSessionRefresh(async () => {
|
|
932
1327
|
const request = new opcjsBase.CallMethodRequest();
|
|
933
1328
|
request.objectId = objectId;
|
|
934
1329
|
request.methodId = methodId;
|
|
935
1330
|
request.inputArguments = inputArguments.map((arg) => opcjsBase.Variant.newFrom(arg));
|
|
936
|
-
const responses = await this.methodService.call([request]);
|
|
1331
|
+
const responses = await this.methodService.call([request], options?.returnDiagnostics, requestHandle);
|
|
937
1332
|
const response = responses[0];
|
|
938
|
-
return new CallMethodResult(response.value, response.statusCode);
|
|
1333
|
+
return new CallMethodResult(response.value, response.statusCode, response.diagnosticInfo);
|
|
939
1334
|
});
|
|
1335
|
+
return Object.assign(promise, { requestHandle });
|
|
940
1336
|
}
|
|
941
|
-
|
|
942
|
-
|
|
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(() => {
|
|
943
1353
|
const visited = /* @__PURE__ */ new Set();
|
|
944
|
-
return this.browseRecursive(nodeId, recursive, visited);
|
|
1354
|
+
return this.browseRecursive(nodeId, recursive, visited, options?.returnDiagnostics ?? 0, requestHandle);
|
|
945
1355
|
});
|
|
1356
|
+
return Object.assign(promise, { requestHandle });
|
|
946
1357
|
}
|
|
947
|
-
async browseRecursive(nodeId, recursive, visited) {
|
|
1358
|
+
async browseRecursive(nodeId, recursive, visited, returnDiagnostics, requestHandle) {
|
|
948
1359
|
const nodeKey = `${nodeId.namespace}:${nodeId.identifier}`;
|
|
949
1360
|
if (visited.has(nodeKey)) {
|
|
950
1361
|
return [];
|
|
@@ -957,12 +1368,12 @@ var Client = class {
|
|
|
957
1368
|
description.includeSubtypes = true;
|
|
958
1369
|
description.nodeClassMask = 0;
|
|
959
1370
|
description.resultMask = opcjsBase.BrowseResultMaskEnum.All;
|
|
960
|
-
const browseResults = await this.browseService.browse([description]);
|
|
1371
|
+
const browseResults = await this.browseService.browse([description], returnDiagnostics, requestHandle);
|
|
961
1372
|
const browseResult = browseResults[0];
|
|
962
1373
|
const allReferences = [...browseResult.references ?? []];
|
|
963
1374
|
let continuationPoint = browseResult.continuationPoint;
|
|
964
1375
|
while (continuationPoint && continuationPoint.byteLength > 0) {
|
|
965
|
-
const nextResults = await this.browseService.browseNext([continuationPoint], false);
|
|
1376
|
+
const nextResults = await this.browseService.browseNext([continuationPoint], false, returnDiagnostics);
|
|
966
1377
|
const nextResult = nextResults[0];
|
|
967
1378
|
allReferences.push(...nextResult.references ?? []);
|
|
968
1379
|
continuationPoint = nextResult.continuationPoint;
|
|
@@ -987,7 +1398,8 @@ var Client = class {
|
|
|
987
1398
|
const childResults = await this.browseRecursive(
|
|
988
1399
|
childNodeId,
|
|
989
1400
|
true,
|
|
990
|
-
visited
|
|
1401
|
+
visited,
|
|
1402
|
+
returnDiagnostics
|
|
991
1403
|
);
|
|
992
1404
|
results.push(...childResults);
|
|
993
1405
|
}
|
|
@@ -997,6 +1409,170 @@ var Client = class {
|
|
|
997
1409
|
async subscribe(ids, callback, options) {
|
|
998
1410
|
this.subscriptionHandler?.subscribe(ids, callback, options);
|
|
999
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
|
+
}
|
|
1000
1576
|
};
|
|
1001
1577
|
var ConfigurationClient = class _ConfigurationClient extends opcjsBase.Configuration {
|
|
1002
1578
|
/**
|
|
@@ -1007,6 +1583,26 @@ var ConfigurationClient = class _ConfigurationClient extends opcjsBase.Configura
|
|
|
1007
1583
|
* @see SecurityConfiguration
|
|
1008
1584
|
*/
|
|
1009
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;
|
|
1010
1606
|
static getSimple(name, company, loggerFactory) {
|
|
1011
1607
|
if (!loggerFactory) {
|
|
1012
1608
|
loggerFactory = new opcjsBase.LoggerFactory({
|
|
@@ -1096,10 +1692,44 @@ var UserIdentity = class _UserIdentity {
|
|
|
1096
1692
|
}
|
|
1097
1693
|
};
|
|
1098
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
|
+
|
|
1099
1725
|
exports.BrowseNodeResult = BrowseNodeResult;
|
|
1726
|
+
exports.CERTIFICATE_REQUIRED_STATUS_CODES = CERTIFICATE_REQUIRED_STATUS_CODES;
|
|
1100
1727
|
exports.CallMethodResult = CallMethodResult;
|
|
1728
|
+
exports.CertificateRequiredError = CertificateRequiredError;
|
|
1101
1729
|
exports.Client = Client;
|
|
1102
1730
|
exports.ConfigurationClient = ConfigurationClient;
|
|
1731
|
+
exports.NamespaceTable = NamespaceTable;
|
|
1732
|
+
exports.ReturnDiagnosticsMask = ReturnDiagnosticsMask;
|
|
1103
1733
|
exports.SECURITY_POLICY_NONE_URI = SECURITY_POLICY_NONE_URI;
|
|
1104
1734
|
exports.SessionInvalidError = SessionInvalidError;
|
|
1105
1735
|
exports.UserIdentity = UserIdentity;
|