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