opcjs-client 0.1.10 → 0.1.14
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 +1387 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +789 -22
- package/dist/index.d.ts +789 -22
- package/dist/index.js +1381 -139
- package/dist/index.js.map +1 -1
- package/package.json +10 -6
package/dist/index.cjs
CHANGED
|
@@ -3,23 +3,87 @@
|
|
|
3
3
|
var opcjsBase = require('opcjs-base');
|
|
4
4
|
|
|
5
5
|
// src/client.ts
|
|
6
|
+
var SessionInvalidError = class extends Error {
|
|
7
|
+
statusCode;
|
|
8
|
+
constructor(statusCode) {
|
|
9
|
+
super(`Session is no longer valid: ${opcjsBase.StatusCodeToString(statusCode)} (0x${statusCode.toString(16).toUpperCase()})`);
|
|
10
|
+
this.name = "SessionInvalidError";
|
|
11
|
+
this.statusCode = statusCode;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
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
|
+
}
|
|
6
28
|
var ServiceBase = class {
|
|
7
29
|
constructor(authToken, secureChannel) {
|
|
8
30
|
this.authToken = authToken;
|
|
9
31
|
this.secureChannel = secureChannel;
|
|
10
32
|
}
|
|
11
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Validates the `serviceResult` value from a response header.
|
|
35
|
+
*
|
|
36
|
+
* Throws `SessionInvalidError` for session-related status codes so callers
|
|
37
|
+
* can detect a dropped session and act accordingly (e.g. reconnect).
|
|
38
|
+
* Throws a generic `Error` for all other non-Good codes.
|
|
39
|
+
*
|
|
40
|
+
* @param result - The `serviceResult` value from the response header.
|
|
41
|
+
* @param context - Short description used in the error message (e.g. "ReadRequest").
|
|
42
|
+
*/
|
|
43
|
+
checkServiceResult(result, context) {
|
|
44
|
+
if (result === void 0 || result === opcjsBase.StatusCode.Good) return;
|
|
45
|
+
if (result === opcjsBase.StatusCode.BadSessionIdInvalid || result === opcjsBase.StatusCode.BadSessionClosed) {
|
|
46
|
+
throw new SessionInvalidError(result);
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`${context} failed: ${opcjsBase.StatusCodeToString(result)} (${opcjsBase.StatusCodeToStringNumber(result)})`);
|
|
49
|
+
}
|
|
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) {
|
|
12
58
|
const requestHeader = new opcjsBase.RequestHeader();
|
|
13
59
|
requestHeader.authenticationToken = this.authToken;
|
|
14
60
|
requestHeader.timestamp = /* @__PURE__ */ new Date();
|
|
15
|
-
requestHeader.requestHandle =
|
|
16
|
-
requestHeader.returnDiagnostics =
|
|
61
|
+
requestHeader.requestHandle = preAllocatedHandle ?? nextRequestHandle();
|
|
62
|
+
requestHeader.returnDiagnostics = returnDiagnostics;
|
|
17
63
|
requestHeader.auditEntryId = "";
|
|
18
64
|
requestHeader.timeoutHint = 6e4;
|
|
19
65
|
requestHeader.additionalHeader = opcjsBase.ExtensionObject.newEmpty();
|
|
20
66
|
return requestHeader;
|
|
21
67
|
}
|
|
22
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
|
+
]);
|
|
23
87
|
|
|
24
88
|
// src/services/sessionService.ts
|
|
25
89
|
var SessionService = class _SessionService extends ServiceBase {
|
|
@@ -27,8 +91,20 @@ var SessionService = class _SessionService extends ServiceBase {
|
|
|
27
91
|
super(authToken, secureChannel);
|
|
28
92
|
this.configuration = configuration;
|
|
29
93
|
}
|
|
30
|
-
|
|
31
|
-
|
|
94
|
+
logger = opcjsBase.getLogger("services.SessionService");
|
|
95
|
+
/**
|
|
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`).
|
|
103
|
+
* @returns The session ID, authentication token, and selected server endpoint.
|
|
104
|
+
* @throws {CertificateRequiredError} when the server demands a client certificate.
|
|
105
|
+
*/
|
|
106
|
+
async createSession(clientCertificate = null) {
|
|
107
|
+
this.logger.debug("Creating session...");
|
|
32
108
|
const clientDescription = new opcjsBase.ApplicationDescription();
|
|
33
109
|
clientDescription.applicationUri = this.configuration.applicationUri;
|
|
34
110
|
clientDescription.productUri = this.configuration.productUri;
|
|
@@ -44,10 +120,10 @@ var SessionService = class _SessionService extends ServiceBase {
|
|
|
44
120
|
request.endpointUrl = this.secureChannel.getEndpointUrl();
|
|
45
121
|
request.sessionName = "";
|
|
46
122
|
request.clientNonce = null;
|
|
47
|
-
request.clientCertificate = null;
|
|
123
|
+
request.clientCertificate = clientCertificate ?? null;
|
|
48
124
|
request.requestedSessionTimeout = 6e4;
|
|
49
125
|
request.maxResponseMessageSize = 0;
|
|
50
|
-
|
|
126
|
+
this.logger.debug("Sending CreateSessionRequest...");
|
|
51
127
|
const response = await this.secureChannel.issueServiceRequest(request);
|
|
52
128
|
if (!response || !(response instanceof opcjsBase.CreateSessionResponse)) {
|
|
53
129
|
throw new Error("Invalid response type for CreateSessionRequest");
|
|
@@ -56,24 +132,41 @@ var SessionService = class _SessionService extends ServiceBase {
|
|
|
56
132
|
if (!castedResponse || !castedResponse.sessionId || !castedResponse.authenticationToken) {
|
|
57
133
|
throw new Error("CreateSessionResponse missing SessionId or AuthenticationToken");
|
|
58
134
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
135
|
+
const serviceResult = castedResponse.responseHeader?.serviceResult;
|
|
136
|
+
if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
|
|
137
|
+
if (CERTIFICATE_REQUIRED_STATUS_CODES.has(serviceResult)) {
|
|
138
|
+
throw new CertificateRequiredError(serviceResult);
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`CreateSessionRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
|
|
141
|
+
}
|
|
142
|
+
const clientConnectionUrl = new URL("opc." + this.secureChannel.getEndpointUrl());
|
|
61
143
|
const securityMode = this.secureChannel.getSecurityMode();
|
|
62
144
|
const securityPolicyUri = this.secureChannel.getSecurityPolicy();
|
|
145
|
+
const normalizeEndpointUrl = (serverEndpointUrl) => {
|
|
146
|
+
const url = new URL(serverEndpointUrl);
|
|
147
|
+
url.hostname = clientConnectionUrl.hostname;
|
|
148
|
+
url.port = clientConnectionUrl.port;
|
|
149
|
+
return url;
|
|
150
|
+
};
|
|
63
151
|
const serverEndpoint = castedResponse?.serverEndpoints?.find((currentEndpoint) => {
|
|
64
|
-
const
|
|
65
|
-
return
|
|
152
|
+
const normalized = normalizeEndpointUrl(currentEndpoint.endpointUrl);
|
|
153
|
+
return normalized.protocol === clientConnectionUrl.protocol && normalized.pathname === clientConnectionUrl.pathname && currentEndpoint.securityMode === securityMode && currentEndpoint.securityPolicyUri === securityPolicyUri;
|
|
66
154
|
});
|
|
67
155
|
if (!serverEndpoint) {
|
|
68
|
-
throw new Error(`Server endpoint ${
|
|
156
|
+
throw new Error(`Server endpoint ${clientConnectionUrl.toString()} not found in CreateSessionResponse`);
|
|
69
157
|
}
|
|
70
|
-
|
|
158
|
+
serverEndpoint.endpointUrl = normalizeEndpointUrl(serverEndpoint.endpointUrl).toString();
|
|
159
|
+
this.logger.debug("Session created with id:", castedResponse.sessionId.identifier);
|
|
71
160
|
return {
|
|
72
161
|
sessionId: castedResponse.sessionId.identifier,
|
|
73
162
|
authToken: castedResponse.authenticationToken,
|
|
74
163
|
endpoint: serverEndpoint
|
|
75
164
|
};
|
|
76
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Activates an existing session using the supplied identity token (OPC UA Part 4, Section 5.7.3).
|
|
168
|
+
* @param identityToken - User identity token (anonymous, username/password, certificate, or issued token).
|
|
169
|
+
*/
|
|
77
170
|
async activateSession(identityToken) {
|
|
78
171
|
const signatureData = new opcjsBase.SignatureData();
|
|
79
172
|
signatureData.algorithm = this.secureChannel.getSecurityPolicy();
|
|
@@ -85,16 +178,65 @@ var SessionService = class _SessionService extends ServiceBase {
|
|
|
85
178
|
request.localeIds = ["en-US"];
|
|
86
179
|
request.userIdentityToken = opcjsBase.ExtensionObject.newBinary(identityToken);
|
|
87
180
|
request.userTokenSignature = signatureData;
|
|
88
|
-
|
|
89
|
-
await this.secureChannel.issueServiceRequest(request);
|
|
90
|
-
|
|
181
|
+
this.logger.debug("Sending ActivateSessionRequest...");
|
|
182
|
+
const activateResponse = await this.secureChannel.issueServiceRequest(request);
|
|
183
|
+
const activateResult = activateResponse?.responseHeader?.serviceResult;
|
|
184
|
+
if (activateResult !== void 0 && activateResult !== opcjsBase.StatusCode.Good) {
|
|
185
|
+
throw new Error(`ActivateSessionRequest failed: ${opcjsBase.StatusCodeToString(activateResult)}`);
|
|
186
|
+
}
|
|
187
|
+
this.logger.debug("Session activated.");
|
|
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
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Closes the current session on the server (OPC UA Part 4, Section 5.7.4).
|
|
218
|
+
* @param deleteSubscriptions - When true the server deletes all Subscriptions
|
|
219
|
+
* associated with this Session, freeing their resources immediately.
|
|
220
|
+
* Pass false to keep Subscriptions alive for transfer to another Session.
|
|
221
|
+
*/
|
|
222
|
+
async closeSession(deleteSubscriptions) {
|
|
223
|
+
this.logger.debug("Sending CloseSessionRequest...");
|
|
224
|
+
const request = new opcjsBase.CloseSessionRequest();
|
|
225
|
+
request.requestHeader = this.createRequestHeader();
|
|
226
|
+
request.deleteSubscriptions = deleteSubscriptions;
|
|
227
|
+
const response = await this.secureChannel.issueServiceRequest(request);
|
|
228
|
+
const result = response?.responseHeader?.serviceResult;
|
|
229
|
+
if (result !== void 0 && result !== opcjsBase.StatusCode.Good) {
|
|
230
|
+
throw new Error(`CloseSession failed: ${opcjsBase.StatusCodeToString(result)}`);
|
|
231
|
+
}
|
|
232
|
+
this.logger.debug("Session closed.");
|
|
91
233
|
}
|
|
92
234
|
recreate(authToken) {
|
|
93
235
|
return new _SessionService(authToken, this.secureChannel, this.configuration);
|
|
94
236
|
}
|
|
95
237
|
};
|
|
96
238
|
|
|
97
|
-
// src/issuerConfiguration.ts
|
|
239
|
+
// src/configuration/issuerConfiguration.ts
|
|
98
240
|
var IssuerConfiguration = class _IssuerConfiguration {
|
|
99
241
|
constructor(resourceId, authorityUrl, authorityProfileUri, tokenEndpoint, authorizationEndpoint, requestTypes, scopes) {
|
|
100
242
|
this.resourceId = resourceId;
|
|
@@ -150,29 +292,159 @@ var Session = class {
|
|
|
150
292
|
getAuthToken() {
|
|
151
293
|
return this.authToken;
|
|
152
294
|
}
|
|
295
|
+
getSessionId() {
|
|
296
|
+
return this.sessionId;
|
|
297
|
+
}
|
|
298
|
+
getEndpoint() {
|
|
299
|
+
return this.endpoint;
|
|
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
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Closes the session on the server (OPC UA Part 4, Section 5.7.4).
|
|
319
|
+
* @param deleteSubscriptions - When true the server deletes all Subscriptions
|
|
320
|
+
* tied to this Session. Defaults to true.
|
|
321
|
+
*/
|
|
322
|
+
async close(deleteSubscriptions = true) {
|
|
323
|
+
await this.sessionServices.closeSession(deleteSubscriptions);
|
|
324
|
+
}
|
|
153
325
|
};
|
|
154
326
|
|
|
155
327
|
// src/sessions/sessionHandler.ts
|
|
156
328
|
var SessionHandler = class {
|
|
329
|
+
constructor(secureChannel, configuration) {
|
|
330
|
+
this.configuration = configuration;
|
|
331
|
+
this.sessionServices = new SessionService(opcjsBase.NodeId.newTwoByte(0), secureChannel, configuration);
|
|
332
|
+
}
|
|
157
333
|
sessionServices;
|
|
334
|
+
logger = opcjsBase.getLogger("sessions.SessionHandler");
|
|
158
335
|
async createNewSession(identity) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
162
360
|
await session.activateSession(identity);
|
|
163
361
|
return session;
|
|
164
362
|
}
|
|
165
|
-
|
|
166
|
-
|
|
363
|
+
/**
|
|
364
|
+
* Attempts to reactivate an existing OPC UA session on the current (new) SecureChannel
|
|
365
|
+
* without calling CreateSession first (OPC UA Part 4, Section 5.7.3).
|
|
366
|
+
*
|
|
367
|
+
* This is the preferred recovery path when the SecureChannel drops but the server-side
|
|
368
|
+
* session has not yet timed out: only a new channel is needed, not a new session.
|
|
369
|
+
*
|
|
370
|
+
* @returns The reactivated Session if ActivateSession succeeded, or `null` if the server
|
|
371
|
+
* rejected the request (e.g. the session had already expired).
|
|
372
|
+
*/
|
|
373
|
+
async tryActivateExistingSession(existingAuthToken, existingSessionId, existingEndpoint, identity) {
|
|
374
|
+
const serviceForExistingSession = this.sessionServices.recreate(existingAuthToken);
|
|
375
|
+
try {
|
|
376
|
+
const session = new Session(existingSessionId, existingAuthToken, existingEndpoint, serviceForExistingSession);
|
|
377
|
+
await session.activateSession(identity);
|
|
378
|
+
return session;
|
|
379
|
+
} catch (err) {
|
|
380
|
+
this.logger.debug("ActivateSession for existing session failed:", err);
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Closes the active session on the server (OPC UA Part 4, Section 5.7.4).
|
|
386
|
+
* @param deleteSubscriptions - Forwarded to CloseSessionRequest. Defaults to true.
|
|
387
|
+
*/
|
|
388
|
+
async closeSession(deleteSubscriptions = true) {
|
|
389
|
+
await this.sessionServices.closeSession(deleteSubscriptions);
|
|
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
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Validates the requested user-identity token type against:
|
|
404
|
+
* 1. The `allowedUserTokenTypes` from the client security configuration — the
|
|
405
|
+
* client has explicitly restricted which token types it will use.
|
|
406
|
+
* 2. The token policies advertised by the server endpoint — verifies that the
|
|
407
|
+
* server actually supports at least one of the allowed types.
|
|
408
|
+
*
|
|
409
|
+
* Throws with a descriptive message if either check fails.
|
|
410
|
+
*/
|
|
411
|
+
validateUserTokenPolicy(identity, endpoint) {
|
|
412
|
+
const allowedTypes = this.configuration.securityConfiguration?.allowedUserTokenTypes;
|
|
413
|
+
if (!allowedTypes) return;
|
|
414
|
+
const requestedType = identity.getTokenType();
|
|
415
|
+
if (!allowedTypes.includes(requestedType)) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`User token type '${opcjsBase.UserTokenTypeEnum[requestedType]}' is not permitted by the client security configuration. Allowed types: ${allowedTypes.map((t) => opcjsBase.UserTokenTypeEnum[t]).join(", ")}.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const serverTypes = endpoint.userIdentityTokens?.map((p) => p.tokenType) ?? [];
|
|
421
|
+
const intersection = allowedTypes.filter((t) => serverTypes.includes(t));
|
|
422
|
+
if (intersection.length === 0) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Server endpoint does not offer any user token type from the allowed list: ${allowedTypes.map((t) => opcjsBase.UserTokenTypeEnum[t]).join(", ")}. Server offers: ${serverTypes.map((t) => opcjsBase.UserTokenTypeEnum[t]).join(", ")}.`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
167
427
|
}
|
|
168
428
|
};
|
|
169
429
|
|
|
430
|
+
// src/securityConfiguration.ts
|
|
431
|
+
var SECURITY_POLICY_NONE_URI = "http://opcfoundation.org/UA/SecurityPolicy#None";
|
|
432
|
+
|
|
170
433
|
// src/services/attributeServiceAttributes.ts
|
|
171
434
|
var AttrIdValue = 13;
|
|
172
435
|
|
|
173
436
|
// src/services/attributeService.ts
|
|
174
437
|
var AttributeService = class extends ServiceBase {
|
|
175
|
-
|
|
438
|
+
logger = opcjsBase.getLogger("services.AttributeService");
|
|
439
|
+
/**
|
|
440
|
+
* Reads the Value attribute of one or more Nodes (OPC UA Part 4, Section 5.10.2).
|
|
441
|
+
* @param nodeIds - NodeIds of the Nodes to read.
|
|
442
|
+
* @param maxAge - Maximum age of the cached value in milliseconds the server may return. 0 = always current value.
|
|
443
|
+
* @param timestampsToReturn - Which timestamps to include in results. Default: Source.
|
|
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.
|
|
446
|
+
*/
|
|
447
|
+
async ReadValue(nodeIds, maxAge = 0, timestampsToReturn = opcjsBase.TimestampsToReturnEnum.Source, returnDiagnostics = 0, requestHandle) {
|
|
176
448
|
const readValueIds = nodeIds.map((ni) => {
|
|
177
449
|
const readValueId = new opcjsBase.ReadValueId();
|
|
178
450
|
readValueId.nodeId = ni;
|
|
@@ -182,19 +454,22 @@ var AttributeService = class extends ServiceBase {
|
|
|
182
454
|
return readValueId;
|
|
183
455
|
});
|
|
184
456
|
const request = new opcjsBase.ReadRequest();
|
|
185
|
-
request.requestHeader = this.createRequestHeader();
|
|
186
|
-
request.maxAge =
|
|
187
|
-
request.timestampsToReturn =
|
|
457
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
|
|
458
|
+
request.maxAge = maxAge;
|
|
459
|
+
request.timestampsToReturn = timestampsToReturn;
|
|
188
460
|
request.nodesToRead = readValueIds;
|
|
189
|
-
|
|
461
|
+
this.logger.debug("Sending ReadRequest...");
|
|
190
462
|
const response = await this.secureChannel.issueServiceRequest(request);
|
|
463
|
+
this.checkServiceResult(response.responseHeader?.serviceResult, "ReadRequest");
|
|
191
464
|
const results = new Array();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
465
|
+
const diagInfos = response.diagnosticInfos ?? [];
|
|
466
|
+
for (let i = 0; i < (response.results ?? []).length; i++) {
|
|
467
|
+
const dataValue = response.results[i];
|
|
468
|
+
results.push({
|
|
469
|
+
statusCode: dataValue.statusCode ?? opcjsBase.StatusCode.Good,
|
|
470
|
+
value: dataValue.value,
|
|
471
|
+
diagnosticInfo: diagInfos[i]
|
|
472
|
+
});
|
|
198
473
|
}
|
|
199
474
|
return results;
|
|
200
475
|
}
|
|
@@ -205,13 +480,14 @@ var AttributeService = class extends ServiceBase {
|
|
|
205
480
|
|
|
206
481
|
// src/readValueResult.ts
|
|
207
482
|
var ReadValueResult = class {
|
|
208
|
-
constructor(value,
|
|
483
|
+
constructor(value, statusCode, diagnosticInfo) {
|
|
209
484
|
this.value = value;
|
|
210
|
-
this.
|
|
485
|
+
this.statusCode = statusCode;
|
|
486
|
+
this.diagnosticInfo = diagnosticInfo;
|
|
211
487
|
}
|
|
212
488
|
};
|
|
213
489
|
|
|
214
|
-
// src/subscriptionHandlerEntry.ts
|
|
490
|
+
// src/subscription/subscriptionHandlerEntry.ts
|
|
215
491
|
var SubscriptionHandlerEntry = class {
|
|
216
492
|
constructor(subscriptionId, handle, id, callback) {
|
|
217
493
|
this.subscriptionId = subscriptionId;
|
|
@@ -221,95 +497,155 @@ var SubscriptionHandlerEntry = class {
|
|
|
221
497
|
}
|
|
222
498
|
};
|
|
223
499
|
|
|
224
|
-
// src/subscriptionHandler.ts
|
|
500
|
+
// src/subscription/subscriptionHandler.ts
|
|
501
|
+
var NODE_ID_DATA_CHANGE_NOTIFICATION = 811;
|
|
502
|
+
var NODE_ID_STATUS_CHANGE_NOTIFICATION = 818;
|
|
225
503
|
var SubscriptionHandler = class {
|
|
226
504
|
constructor(subscriptionService, monitoredItemService) {
|
|
227
505
|
this.subscriptionService = subscriptionService;
|
|
228
506
|
this.monitoredItemService = monitoredItemService;
|
|
229
507
|
}
|
|
508
|
+
logger = opcjsBase.getLogger("SubscriptionHandler");
|
|
230
509
|
entries = new Array();
|
|
231
510
|
nextHandle = 0;
|
|
232
|
-
|
|
511
|
+
isRunning = false;
|
|
512
|
+
/** Guards against multiple concurrent publish loops. */
|
|
513
|
+
publishInFlight = false;
|
|
514
|
+
/**
|
|
515
|
+
* Optional callback invoked when the server announces a shutdown via a
|
|
516
|
+
* `StatusChangeNotification` with status `BadShutdown` or `BadServerHalted`
|
|
517
|
+
* (OPC UA Part 4, §5.13.6.2 — Session Client Detect Shutdown).
|
|
518
|
+
*
|
|
519
|
+
* Assign this before calling `subscribe()`. The client sets it automatically
|
|
520
|
+
* in `initServices()` to trigger a reconnect.
|
|
521
|
+
*/
|
|
522
|
+
onShutdown;
|
|
523
|
+
/** Returns true when at least one subscription is active and the publish loop is running. */
|
|
524
|
+
hasActiveSubscription() {
|
|
525
|
+
return this.isRunning && this.entries.length > 0;
|
|
526
|
+
}
|
|
527
|
+
async subscribe(ids, callback, options) {
|
|
233
528
|
if (this.entries.length > 0) {
|
|
234
529
|
throw new Error("Subscribing more than once is not implemented");
|
|
235
530
|
}
|
|
236
|
-
const subscriptionId = await this.subscriptionService.createSubscription();
|
|
531
|
+
const subscriptionId = await this.subscriptionService.createSubscription(options);
|
|
237
532
|
const items = [];
|
|
238
|
-
for (
|
|
239
|
-
const entry = new SubscriptionHandlerEntry(
|
|
240
|
-
subscriptionId,
|
|
241
|
-
this.nextHandle++,
|
|
242
|
-
id,
|
|
243
|
-
callback
|
|
244
|
-
);
|
|
533
|
+
for (const id of ids) {
|
|
534
|
+
const entry = new SubscriptionHandlerEntry(subscriptionId, this.nextHandle++, id, callback);
|
|
245
535
|
this.entries.push(entry);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
536
|
+
items.push({ id, handle: entry.handle });
|
|
537
|
+
}
|
|
538
|
+
await this.monitoredItemService.createMonitoredItems(subscriptionId, items, options);
|
|
539
|
+
this.isRunning = true;
|
|
540
|
+
void this.publishLoop([]);
|
|
541
|
+
}
|
|
542
|
+
// https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.5
|
|
543
|
+
async publishLoop(pendingAcknowledgements) {
|
|
544
|
+
if (!this.isRunning) return;
|
|
545
|
+
if (this.publishInFlight) return;
|
|
546
|
+
this.publishInFlight = true;
|
|
547
|
+
let response;
|
|
548
|
+
try {
|
|
549
|
+
response = await this.subscriptionService.publish(pendingAcknowledgements);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
this.logger.error(`Publish failed, stopping publish loop: ${err}`);
|
|
552
|
+
this.isRunning = false;
|
|
553
|
+
this.publishInFlight = false;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
this.publishInFlight = false;
|
|
557
|
+
const { subscriptionId, availableSequenceNumbers, moreNotifications, notificationMessage } = response;
|
|
558
|
+
const notificationDatas = notificationMessage?.notificationData ?? [];
|
|
559
|
+
const seqNumber = notificationMessage?.sequenceNumber;
|
|
560
|
+
const nextAcknowledgements = [];
|
|
561
|
+
const isKeepAlive = notificationDatas.length === 0;
|
|
562
|
+
if (!isKeepAlive && seqNumber !== void 0) {
|
|
563
|
+
const isAvailable = !availableSequenceNumbers || availableSequenceNumbers.includes(seqNumber);
|
|
564
|
+
if (isAvailable) {
|
|
565
|
+
const ack = new opcjsBase.SubscriptionAcknowledgement();
|
|
566
|
+
ack.subscriptionId = subscriptionId;
|
|
567
|
+
ack.sequenceNumber = seqNumber;
|
|
568
|
+
nextAcknowledgements.push(ack);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const notificationData of notificationDatas) {
|
|
267
572
|
const decodedData = notificationData.data;
|
|
268
|
-
const
|
|
269
|
-
|
|
573
|
+
const rawTypeId = notificationData.typeId;
|
|
574
|
+
const typeNodeId = rawTypeId instanceof opcjsBase.ExpandedNodeId ? rawTypeId.nodeId : rawTypeId;
|
|
575
|
+
if (typeNodeId.namespace === 0 && typeNodeId.identifier === NODE_ID_DATA_CHANGE_NOTIFICATION) {
|
|
270
576
|
const dataChangeNotification = decodedData;
|
|
271
|
-
for (
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
const entry = this.entries.find((e) => e.handle == clientHandle);
|
|
275
|
-
entry?.callback([{
|
|
276
|
-
id: entry.id,
|
|
277
|
-
value: value.value?.value
|
|
278
|
-
}]);
|
|
577
|
+
for (const item of dataChangeNotification.monitoredItems) {
|
|
578
|
+
const entry = this.entries.find((e) => e.handle === item.clientHandle);
|
|
579
|
+
entry?.callback([{ id: entry.id, value: item.value.value?.value }]);
|
|
279
580
|
}
|
|
581
|
+
} else if (typeNodeId.namespace === 0 && typeNodeId.identifier === NODE_ID_STATUS_CHANGE_NOTIFICATION) {
|
|
582
|
+
const statusChange = decodedData;
|
|
583
|
+
this.logger.warn(
|
|
584
|
+
`Subscription ${subscriptionId} status changed: 0x${statusChange.status?.toString(16).toUpperCase()}`
|
|
585
|
+
);
|
|
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
|
+
}
|
|
592
|
+
return;
|
|
280
593
|
} else {
|
|
281
|
-
|
|
594
|
+
this.logger.warn(
|
|
595
|
+
`Notification data type ${typeNodeId.namespace}:${typeNodeId.identifier} is not supported.`
|
|
596
|
+
);
|
|
282
597
|
}
|
|
283
598
|
}
|
|
284
|
-
|
|
285
|
-
this.
|
|
286
|
-
}
|
|
599
|
+
if (moreNotifications) {
|
|
600
|
+
void this.publishLoop(nextAcknowledgements);
|
|
601
|
+
} else {
|
|
602
|
+
setTimeout(() => void this.publishLoop(nextAcknowledgements), 0);
|
|
603
|
+
}
|
|
287
604
|
}
|
|
288
605
|
};
|
|
289
606
|
var SubscriptionService = class extends ServiceBase {
|
|
607
|
+
logger = opcjsBase.getLogger("services.SubscriptionService");
|
|
608
|
+
/**
|
|
609
|
+
* Creates a new subscription on the server (OPC UA Part 4, Section 5.14.2).
|
|
610
|
+
* @param options - Optional subscription parameters; server revises all values.
|
|
611
|
+
* @returns The server-assigned subscription ID.
|
|
612
|
+
*/
|
|
290
613
|
// https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.2
|
|
291
|
-
async createSubscription() {
|
|
614
|
+
async createSubscription(options) {
|
|
292
615
|
const request = new opcjsBase.CreateSubscriptionRequest();
|
|
293
616
|
request.requestHeader = this.createRequestHeader();
|
|
294
|
-
request.requestedPublishingInterval = 2e3;
|
|
295
|
-
request.requestedLifetimeCount = 36e4;
|
|
296
|
-
request.requestedMaxKeepAliveCount = 6e4;
|
|
297
|
-
request.maxNotificationsPerPublish = 200;
|
|
617
|
+
request.requestedPublishingInterval = options?.requestedPublishingInterval ?? 2e3;
|
|
618
|
+
request.requestedLifetimeCount = options?.requestedLifetimeCount ?? 36e4;
|
|
619
|
+
request.requestedMaxKeepAliveCount = options?.requestedMaxKeepAliveCount ?? 6e4;
|
|
620
|
+
request.maxNotificationsPerPublish = options?.maxNotificationsPerPublish ?? 200;
|
|
298
621
|
request.publishingEnabled = true;
|
|
299
|
-
request.priority = 1;
|
|
300
|
-
|
|
622
|
+
request.priority = options?.priority ?? 1;
|
|
623
|
+
this.logger.debug("Sending CreateSubscriptionRequest...");
|
|
301
624
|
const response = await this.secureChannel.issueServiceRequest(request);
|
|
302
|
-
|
|
625
|
+
const serviceResult = response.responseHeader?.serviceResult;
|
|
626
|
+
if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
|
|
627
|
+
throw new Error(`CreateSubscriptionRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
|
|
628
|
+
}
|
|
629
|
+
this.logger.debug(`Subscription created with id: ${response.subscriptionId}`);
|
|
303
630
|
return response.subscriptionId;
|
|
304
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* Sends a publish request to receive notifications from active subscriptions (OPC UA Part 4, Section 5.14.5).
|
|
634
|
+
* @param acknowledgements - Sequence numbers to acknowledge from previous publish responses.
|
|
635
|
+
* @returns The publish response containing notification data.
|
|
636
|
+
*/
|
|
305
637
|
// https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.5
|
|
306
638
|
async publish(acknowledgements) {
|
|
307
639
|
const request = new opcjsBase.PublishRequest();
|
|
308
640
|
request.requestHeader = this.createRequestHeader();
|
|
309
641
|
request.subscriptionAcknowledgements = acknowledgements;
|
|
310
|
-
|
|
642
|
+
this.logger.debug("Sending PublishRequest...");
|
|
311
643
|
const response = await this.secureChannel.issueServiceRequest(request);
|
|
312
|
-
|
|
644
|
+
const serviceResult = response.responseHeader?.serviceResult;
|
|
645
|
+
if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
|
|
646
|
+
throw new Error(`PublishRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
|
|
647
|
+
}
|
|
648
|
+
this.logger.debug("Received PublishResponse.");
|
|
313
649
|
return response;
|
|
314
650
|
}
|
|
315
651
|
constructor(authToken, secureChannel) {
|
|
@@ -317,7 +653,15 @@ var SubscriptionService = class extends ServiceBase {
|
|
|
317
653
|
}
|
|
318
654
|
};
|
|
319
655
|
var MonitoredItemService = class extends ServiceBase {
|
|
320
|
-
|
|
656
|
+
logger = opcjsBase.getLogger("services.MonitoredItemService");
|
|
657
|
+
/**
|
|
658
|
+
* Creates monitored items within a subscription (OPC UA Part 4, Section 5.13.2).
|
|
659
|
+
* @param subscriptionId - ID of the subscription to add monitored items to.
|
|
660
|
+
* @param ids - Array of NodeIds and client handles identifying the items to monitor.
|
|
661
|
+
* @param options - Monitoring options (samplingInterval, queueSize).
|
|
662
|
+
*/
|
|
663
|
+
async createMonitoredItems(subscriptionId, ids, options = {}) {
|
|
664
|
+
const { samplingInterval = 1e3, queueSize = 100 } = options;
|
|
321
665
|
const items = ids.map((ni) => {
|
|
322
666
|
const readValueId = new opcjsBase.ReadValueId();
|
|
323
667
|
readValueId.nodeId = ni.id;
|
|
@@ -326,9 +670,9 @@ var MonitoredItemService = class extends ServiceBase {
|
|
|
326
670
|
readValueId.dataEncoding = new opcjsBase.QualifiedName(0, "");
|
|
327
671
|
const monitoringParameters = new opcjsBase.MonitoringParameters();
|
|
328
672
|
monitoringParameters.clientHandle = ni.handle;
|
|
329
|
-
monitoringParameters.samplingInterval =
|
|
673
|
+
monitoringParameters.samplingInterval = samplingInterval;
|
|
330
674
|
monitoringParameters.filter = opcjsBase.ExtensionObject.newEmpty();
|
|
331
|
-
monitoringParameters.queueSize =
|
|
675
|
+
monitoringParameters.queueSize = queueSize;
|
|
332
676
|
monitoringParameters.discardOldest = true;
|
|
333
677
|
const monitoredItemCreateRequest = new opcjsBase.MonitoredItemCreateRequest();
|
|
334
678
|
monitoredItemCreateRequest.itemToMonitor = readValueId;
|
|
@@ -341,69 +685,935 @@ var MonitoredItemService = class extends ServiceBase {
|
|
|
341
685
|
request.subscriptionId = subscriptionId;
|
|
342
686
|
request.timestampsToReturn = opcjsBase.TimestampsToReturnEnum.Source;
|
|
343
687
|
request.itemsToCreate = items;
|
|
344
|
-
|
|
345
|
-
await this.secureChannel.issueServiceRequest(request);
|
|
688
|
+
this.logger.debug("Sending CreateMonitoredItemsRequest...");
|
|
689
|
+
const response = await this.secureChannel.issueServiceRequest(request);
|
|
690
|
+
const serviceResult = response.responseHeader?.serviceResult;
|
|
691
|
+
if (serviceResult !== void 0 && serviceResult !== opcjsBase.StatusCode.Good) {
|
|
692
|
+
throw new Error(`CreateMonitoredItemsRequest failed: ${opcjsBase.StatusCodeToString(serviceResult)}`);
|
|
693
|
+
}
|
|
694
|
+
for (const result of response.results ?? []) {
|
|
695
|
+
if (result.statusCode !== void 0 && result.statusCode !== opcjsBase.StatusCode.Good) {
|
|
696
|
+
this.logger.warn(`Failed to create monitored item: ${opcjsBase.StatusCodeToString(result.statusCode)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
constructor(authToken, secureChannel) {
|
|
701
|
+
super(authToken, secureChannel);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
var MethodService = class extends ServiceBase {
|
|
705
|
+
logger = opcjsBase.getLogger("services.MethodService");
|
|
706
|
+
/**
|
|
707
|
+
* Calls one or more methods on the server (OPC UA Part 4, Section 5.11.2).
|
|
708
|
+
* @param methodsToCall - Array of CallMethodRequest describing each method to invoke.
|
|
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.
|
|
712
|
+
*/
|
|
713
|
+
async call(methodsToCall, returnDiagnostics = 0, requestHandle) {
|
|
714
|
+
const request = new opcjsBase.CallRequest();
|
|
715
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
|
|
716
|
+
request.methodsToCall = methodsToCall;
|
|
717
|
+
this.logger.debug("Sending CallRequest...");
|
|
718
|
+
const response = await this.secureChannel.issueServiceRequest(request);
|
|
719
|
+
this.checkServiceResult(response.responseHeader?.serviceResult, "CallRequest");
|
|
720
|
+
const diagInfos = response.diagnosticInfos ?? [];
|
|
721
|
+
return response.results.map((result, i) => ({
|
|
722
|
+
statusCode: result.statusCode ?? opcjsBase.StatusCode.Good,
|
|
723
|
+
value: result.outputArguments.map((arg) => arg.value),
|
|
724
|
+
diagnosticInfo: diagInfos[i]
|
|
725
|
+
}));
|
|
726
|
+
}
|
|
727
|
+
constructor(authToken, secureChannel) {
|
|
728
|
+
super(authToken, secureChannel);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// src/method/callMethodResult.ts
|
|
733
|
+
var CallMethodResult = class {
|
|
734
|
+
constructor(values, statusCode, diagnosticInfo) {
|
|
735
|
+
this.values = values;
|
|
736
|
+
this.statusCode = statusCode;
|
|
737
|
+
this.diagnosticInfo = diagnosticInfo;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
var BrowseService = class extends ServiceBase {
|
|
741
|
+
logger = opcjsBase.getLogger("services.BrowseService");
|
|
742
|
+
/**
|
|
743
|
+
* Browses one or more Nodes and returns their References (OPC UA Part 4, Section 5.9.2).
|
|
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.
|
|
746
|
+
* @returns Array of BrowseResult, one per requested node.
|
|
747
|
+
*/
|
|
748
|
+
async browse(nodesToBrowse, returnDiagnostics = 0, requestHandle) {
|
|
749
|
+
const view = new opcjsBase.ViewDescription();
|
|
750
|
+
view.viewId = opcjsBase.NodeId.newNumeric(0, 0);
|
|
751
|
+
view.timestamp = /* @__PURE__ */ new Date(-116444736e5);
|
|
752
|
+
view.viewVersion = 0;
|
|
753
|
+
const request = new opcjsBase.BrowseRequest();
|
|
754
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics, requestHandle);
|
|
755
|
+
request.view = view;
|
|
756
|
+
request.requestedMaxReferencesPerNode = 0;
|
|
757
|
+
request.nodesToBrowse = nodesToBrowse;
|
|
758
|
+
this.logger.debug("Sending BrowseRequest...");
|
|
759
|
+
const response = await this.secureChannel.issueServiceRequest(request);
|
|
760
|
+
this.checkServiceResult(response.responseHeader?.serviceResult, "BrowseRequest");
|
|
761
|
+
return response.results ?? [];
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Continues a Browse operation using continuation points (OPC UA Part 4, Section 5.9.3).
|
|
765
|
+
* @param continuationPoints - Continuation points returned by a prior Browse or BrowseNext call.
|
|
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.
|
|
768
|
+
* @returns Array of BrowseResult, one per continuation point.
|
|
769
|
+
*/
|
|
770
|
+
async browseNext(continuationPoints, releaseContinuationPoints, returnDiagnostics = 0) {
|
|
771
|
+
const request = new opcjsBase.BrowseNextRequest();
|
|
772
|
+
request.requestHeader = this.createRequestHeader(returnDiagnostics);
|
|
773
|
+
request.releaseContinuationPoints = releaseContinuationPoints;
|
|
774
|
+
request.continuationPoints = continuationPoints;
|
|
775
|
+
this.logger.debug("Sending BrowseNextRequest...");
|
|
776
|
+
const response = await this.secureChannel.issueServiceRequest(request);
|
|
777
|
+
this.checkServiceResult(response.responseHeader?.serviceResult, "BrowseNextRequest");
|
|
778
|
+
return response.results ?? [];
|
|
346
779
|
}
|
|
347
780
|
constructor(authToken, secureChannel) {
|
|
348
781
|
super(authToken, secureChannel);
|
|
349
782
|
}
|
|
350
783
|
};
|
|
351
784
|
|
|
785
|
+
// src/browseNodeResult.ts
|
|
786
|
+
var BrowseNodeResult = class {
|
|
787
|
+
constructor(referenceTypeId, isForward, nodeId, browseName, displayName, nodeClass, typeDefinition) {
|
|
788
|
+
this.referenceTypeId = referenceTypeId;
|
|
789
|
+
this.isForward = isForward;
|
|
790
|
+
this.nodeId = nodeId;
|
|
791
|
+
this.browseName = browseName;
|
|
792
|
+
this.displayName = displayName;
|
|
793
|
+
this.nodeClass = nodeClass;
|
|
794
|
+
this.typeDefinition = typeDefinition;
|
|
795
|
+
}
|
|
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
|
+
};
|
|
850
|
+
|
|
352
851
|
// src/client.ts
|
|
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);
|
|
856
|
+
var KEEP_ALIVE_INTERVAL_MS = 25e3;
|
|
857
|
+
var OPC_UA_MIN_DATE_TIME_MS = -116444736e5;
|
|
353
858
|
var Client = class {
|
|
354
859
|
constructor(endpointUrl, configuration, identity) {
|
|
355
860
|
this.configuration = configuration;
|
|
356
861
|
this.identity = identity;
|
|
357
862
|
this.endpointUrl = endpointUrl;
|
|
863
|
+
opcjsBase.initLoggerProvider(configuration.loggerFactory);
|
|
864
|
+
this.logger = opcjsBase.getLogger("Client");
|
|
358
865
|
}
|
|
359
866
|
endpointUrl;
|
|
360
|
-
|
|
867
|
+
attributeService;
|
|
868
|
+
methodService;
|
|
869
|
+
browseService;
|
|
361
870
|
session;
|
|
362
871
|
subscriptionHandler;
|
|
872
|
+
logger;
|
|
873
|
+
// Stored after connect() so that refreshSession() and disconnect() can use them.
|
|
874
|
+
secureChannel;
|
|
875
|
+
secureChannelFacade;
|
|
876
|
+
ws;
|
|
877
|
+
sessionHandler;
|
|
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;
|
|
363
913
|
getSession() {
|
|
364
914
|
if (!this.session) {
|
|
365
915
|
throw new Error("No session available");
|
|
366
916
|
}
|
|
367
917
|
return this.session;
|
|
368
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* (Re-)initialises all session-scoped services from the current `this.session`.
|
|
921
|
+
* Called both after the initial `connect()` and after a session refresh.
|
|
922
|
+
*/
|
|
923
|
+
initServices() {
|
|
924
|
+
const authToken = this.session.getAuthToken();
|
|
925
|
+
const sc = this.secureChannel;
|
|
926
|
+
this.attributeService = new AttributeService(authToken, sc);
|
|
927
|
+
this.methodService = new MethodService(authToken, sc);
|
|
928
|
+
this.browseService = new BrowseService(authToken, sc);
|
|
929
|
+
this.subscriptionHandler = new SubscriptionHandler(
|
|
930
|
+
new SubscriptionService(authToken, sc),
|
|
931
|
+
new MonitoredItemService(authToken, sc)
|
|
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;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Executes `fn` and, if it throws a `SessionInvalidError`, creates a fresh
|
|
977
|
+
* session and retries the operation exactly once.
|
|
978
|
+
*
|
|
979
|
+
* This covers the reactive case: a service call reveals that the server has
|
|
980
|
+
* already dropped the session (e.g. due to timeout). The new session is
|
|
981
|
+
* established transparently before re-running the original operation.
|
|
982
|
+
*
|
|
983
|
+
* For any other error (e.g. transport-level failures when the SecureChannel
|
|
984
|
+
* drops), this method attempts to reconnect the channel and reactivate the
|
|
985
|
+
* existing session first — falling back to a brand-new session only when
|
|
986
|
+
* reactivation fails — before retrying the operation once.
|
|
987
|
+
*/
|
|
988
|
+
async withSessionRefresh(fn) {
|
|
989
|
+
try {
|
|
990
|
+
return await fn();
|
|
991
|
+
} catch (err) {
|
|
992
|
+
if (err instanceof SessionInvalidError) {
|
|
993
|
+
this.logger.info(`Session invalid (${err.statusCode.toString(16)}), refreshing session...`);
|
|
994
|
+
this.session = await this.sessionHandler.createNewSession(this.identity);
|
|
995
|
+
this.initServices();
|
|
996
|
+
this.logger.info("Session refreshed, retrying operation.");
|
|
997
|
+
return await fn();
|
|
998
|
+
}
|
|
999
|
+
this.logger.info("Service call failed, attempting channel reconnect and session reactivation...");
|
|
1000
|
+
try {
|
|
1001
|
+
await this.reconnectAndReactivate();
|
|
1002
|
+
this.initServices();
|
|
1003
|
+
this.logger.info("Reconnected successfully, retrying operation.");
|
|
1004
|
+
} catch (reconnectErr) {
|
|
1005
|
+
this.logger.warn("Channel reconnect failed:", reconnectErr);
|
|
1006
|
+
throw err;
|
|
1007
|
+
}
|
|
1008
|
+
return await fn();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Starts a periodic keep-alive timer that reads Server_ServerStatus when no subscription is
|
|
1013
|
+
* active. OPC UA Part 4, Section 5.7.1 requires clients to keep the session alive; when no
|
|
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.
|
|
1020
|
+
*/
|
|
1021
|
+
startKeepAlive() {
|
|
1022
|
+
this.keepAliveTimer = setInterval(() => {
|
|
1023
|
+
if (this.subscriptionHandler?.hasActiveSubscription()) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (this.attributeService) {
|
|
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) => {
|
|
1033
|
+
this.logger.warn("Keep-alive read failed:", err);
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}, KEEP_ALIVE_INTERVAL_MS);
|
|
1037
|
+
}
|
|
1038
|
+
stopKeepAlive() {
|
|
1039
|
+
clearInterval(this.keepAliveTimer);
|
|
1040
|
+
this.keepAliveTimer = void 0;
|
|
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
|
+
}
|
|
369
1123
|
async connect() {
|
|
370
|
-
const
|
|
1124
|
+
const { ws, sc } = await this.openTransportAndChannel();
|
|
1125
|
+
this.secureChannel = sc;
|
|
1126
|
+
this.secureChannelFacade = sc;
|
|
1127
|
+
this.ws = ws;
|
|
1128
|
+
this.logger.debug("Creating session...");
|
|
1129
|
+
this.sessionHandler = new SessionHandler(sc, this.configuration);
|
|
1130
|
+
this.session = await this.sessionHandler.createNewSession(this.identity);
|
|
1131
|
+
this.logger.debug("Session created.");
|
|
1132
|
+
this.logger.debug("Initializing services...");
|
|
1133
|
+
this.initServices();
|
|
1134
|
+
this.startKeepAlive();
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Builds the full WebSocket → TCP → SecureChannel pipeline and returns the
|
|
1138
|
+
* two objects needed to drive it: the raw WebSocket facade (for teardown)
|
|
1139
|
+
* and the SecureChannelFacade (for service requests/session management).
|
|
1140
|
+
*
|
|
1141
|
+
* Extracted from `connect()` so it can be reused by `reconnectAndReactivate()`.
|
|
1142
|
+
*/
|
|
1143
|
+
async openTransportAndChannel() {
|
|
1144
|
+
const wsOptions = { endpoint: this.endpointUrl };
|
|
1145
|
+
const ws = new opcjsBase.WebSocketFascade(wsOptions);
|
|
1146
|
+
const webSocketReadableStream = new opcjsBase.WebSocketReadableStream(ws, 1e3);
|
|
1147
|
+
const webSocketWritableStream = new opcjsBase.WebSocketWritableStream(ws);
|
|
1148
|
+
const scContext = new opcjsBase.SecureChannelContext(this.endpointUrl);
|
|
1149
|
+
const tcpMessageInjector = new opcjsBase.TcpMessageInjector();
|
|
1150
|
+
const tcpConnectionHandler = new opcjsBase.TcpConnectionHandler(tcpMessageInjector, scContext);
|
|
1151
|
+
const tcpMessageDecoupler = new opcjsBase.TcpMessageDecoupler(tcpConnectionHandler.onTcpMessage.bind(tcpConnectionHandler));
|
|
1152
|
+
const scMessageEncoder = new opcjsBase.SecureChannelMessageEncoder(scContext);
|
|
1153
|
+
const scTypeDecoder = new opcjsBase.SecureChannelTypeDecoder(
|
|
1154
|
+
this.configuration.decoder
|
|
1155
|
+
);
|
|
1156
|
+
const scMessageDecoder = new opcjsBase.SecureChannelMessageDecoder(scContext);
|
|
1157
|
+
const scTypeEncoder = new opcjsBase.SecureChannelTypeEncoder(
|
|
1158
|
+
this.configuration.encoder
|
|
1159
|
+
);
|
|
1160
|
+
const scChunkWriter = new opcjsBase.SecureChannelChunkWriter(scContext);
|
|
1161
|
+
const scChunkReader = new opcjsBase.SecureChannelChunkReader(scContext);
|
|
1162
|
+
webSocketReadableStream.pipeTo(tcpMessageDecoupler.writable);
|
|
1163
|
+
tcpMessageDecoupler.readable.pipeTo(scMessageDecoder.writable);
|
|
1164
|
+
scMessageDecoder.readable.pipeTo(scChunkReader.writable);
|
|
1165
|
+
scChunkReader.readable.pipeTo(scTypeDecoder.writable);
|
|
1166
|
+
scTypeEncoder.readable.pipeTo(scChunkWriter.writable);
|
|
1167
|
+
scChunkWriter.readable.pipeTo(scMessageEncoder.writable);
|
|
1168
|
+
scMessageEncoder.readable.pipeTo(tcpMessageInjector.writable);
|
|
1169
|
+
tcpMessageInjector.readable.pipeTo(webSocketWritableStream);
|
|
1170
|
+
const sc = new opcjsBase.SecureChannelFacade(scContext, scTypeDecoder, scTypeEncoder);
|
|
371
1171
|
let connected = false;
|
|
372
1172
|
while (!connected) {
|
|
373
|
-
|
|
374
|
-
|
|
1173
|
+
this.logger.debug(`Connecting to OPC UA server at ${this.endpointUrl}...`);
|
|
1174
|
+
await ws.connect();
|
|
1175
|
+
this.logger.debug("WebSocket connection established, now establishing TCP connection...");
|
|
1176
|
+
connected = await tcpConnectionHandler.connect(this.endpointUrl);
|
|
375
1177
|
if (!connected) {
|
|
376
|
-
|
|
1178
|
+
this.logger.info("Connection failed, retrying in 2 seconds...");
|
|
377
1179
|
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
378
1180
|
}
|
|
379
1181
|
}
|
|
380
|
-
|
|
381
|
-
this.
|
|
382
|
-
await
|
|
383
|
-
|
|
384
|
-
this.
|
|
385
|
-
|
|
386
|
-
new SubscriptionService(this.session.getAuthToken(), this.channel),
|
|
387
|
-
new MonitoredItemService(this.session.getAuthToken(), this.channel)
|
|
388
|
-
);
|
|
1182
|
+
this.logger.info("Connected to OPC UA server.");
|
|
1183
|
+
this.logger.debug("Opening secure channel...");
|
|
1184
|
+
await sc.openSecureChannel();
|
|
1185
|
+
this.logger.debug("Secure channel established.");
|
|
1186
|
+
this.enforceChannelSecurityConfig(sc);
|
|
1187
|
+
return { ws, sc };
|
|
389
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Validates the negotiated channel's security policy and mode against the
|
|
1191
|
+
* client's `SecurityConfiguration` (OPC UA Part 2, Security Administration).
|
|
1192
|
+
*
|
|
1193
|
+
* Throws if:
|
|
1194
|
+
* - `allowSecurityPolicyNone` is `false` and the channel uses SecurityPolicy None.
|
|
1195
|
+
* - `messageSecurityMode` is set and does not match the channel's actual mode.
|
|
1196
|
+
*/
|
|
1197
|
+
enforceChannelSecurityConfig(sc) {
|
|
1198
|
+
const config = this.configuration.securityConfiguration;
|
|
1199
|
+
if (!config) return;
|
|
1200
|
+
const negotiatedPolicy = sc.getSecurityPolicy();
|
|
1201
|
+
const negotiatedMode = sc.getSecurityMode();
|
|
1202
|
+
if (config.allowSecurityPolicyNone === false && negotiatedPolicy === SECURITY_POLICY_NONE_URI) {
|
|
1203
|
+
throw new Error(
|
|
1204
|
+
"Connection refused: SecurityPolicy None is disabled by the client security configuration. Only SecurityPolicy None is currently supported by this client implementation."
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
if (config.messageSecurityMode !== void 0 && config.messageSecurityMode !== negotiatedMode) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
`Connection refused: negotiated MessageSecurityMode ${negotiatedMode} does not match the required mode ${config.messageSecurityMode} from the security configuration.`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Tears down the current (dead) channel and establishes a fresh one, then
|
|
1215
|
+
* attempts to recover the existing OPC UA session via ActivateSession before
|
|
1216
|
+
* falling back to a full CreateSession + ActivateSession.
|
|
1217
|
+
*
|
|
1218
|
+
* OPC UA Part 4, Section 5.7.1 / Session Client Auto Reconnect conformance unit:
|
|
1219
|
+
* When the SecureChannel drops but the server-side session has not yet timed
|
|
1220
|
+
* out, the client SHOULD reuse the existing session by calling ActivateSession
|
|
1221
|
+
* on the new channel. Only if that fails should the client create a new session.
|
|
1222
|
+
*/
|
|
1223
|
+
async reconnectAndReactivate() {
|
|
1224
|
+
this.logger.info("Tearing down dead channel before reconnect...");
|
|
1225
|
+
try {
|
|
1226
|
+
this.secureChannelFacade?.close();
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
this.ws?.close();
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
this.secureChannelFacade = void 0;
|
|
1234
|
+
this.secureChannel = void 0;
|
|
1235
|
+
this.ws = void 0;
|
|
1236
|
+
const { ws, sc } = await this.openTransportAndChannel();
|
|
1237
|
+
this.secureChannel = sc;
|
|
1238
|
+
this.secureChannelFacade = sc;
|
|
1239
|
+
this.ws = ws;
|
|
1240
|
+
this.sessionHandler = new SessionHandler(sc, this.configuration);
|
|
1241
|
+
if (this.session) {
|
|
1242
|
+
const reactivated = await this.sessionHandler.tryActivateExistingSession(
|
|
1243
|
+
this.session.getAuthToken(),
|
|
1244
|
+
this.session.getSessionId(),
|
|
1245
|
+
this.session.getEndpoint(),
|
|
1246
|
+
this.identity
|
|
1247
|
+
);
|
|
1248
|
+
if (reactivated !== null) {
|
|
1249
|
+
this.session = reactivated;
|
|
1250
|
+
this.logger.info("Existing session successfully reactivated on new channel.");
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
this.logger.info("ActivateSession for existing session failed; creating a fresh session...");
|
|
1254
|
+
}
|
|
1255
|
+
this.session = await this.sessionHandler.createNewSession(this.identity);
|
|
1256
|
+
this.logger.info("Fresh session established on new channel.");
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Gracefully disconnects from the OPC UA server.
|
|
1260
|
+
*
|
|
1261
|
+
* Sequence per OPC UA Part 4, Section 5.7.4:
|
|
1262
|
+
* 1. CloseSession (deleteSubscriptions=true) so the server frees all resources.
|
|
1263
|
+
* 2. Close the SecureChannel, which cancels the pending token-renewal timer.
|
|
1264
|
+
* 3. Close the WebSocket transport.
|
|
1265
|
+
*
|
|
1266
|
+
* CloseSession errors are swallowed so transport teardown always completes even
|
|
1267
|
+
* when the session has already expired on the server side.
|
|
1268
|
+
*/
|
|
390
1269
|
async disconnect() {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
1270
|
+
this.logger.info("Disconnecting from OPC UA server...");
|
|
1271
|
+
this.stopKeepAlive();
|
|
1272
|
+
if (this.session && this.sessionHandler) {
|
|
1273
|
+
try {
|
|
1274
|
+
await this.sessionHandler.closeSession(true);
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
this.logger.warn("CloseSession failed (continuing teardown):", err);
|
|
1277
|
+
}
|
|
1278
|
+
this.session = void 0;
|
|
1279
|
+
}
|
|
1280
|
+
this.secureChannelFacade?.close();
|
|
1281
|
+
this.secureChannelFacade = void 0;
|
|
1282
|
+
this.secureChannel = void 0;
|
|
1283
|
+
this.ws?.close();
|
|
1284
|
+
this.ws = void 0;
|
|
1285
|
+
this.logger.info("Disconnected.");
|
|
1286
|
+
}
|
|
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)) ?? [];
|
|
1307
|
+
});
|
|
1308
|
+
return Object.assign(promise, { requestHandle });
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
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
|
+
*
|
|
1318
|
+
* @param objectId - NodeId of the Object that owns the method.
|
|
1319
|
+
* @param methodId - NodeId of the Method to invoke.
|
|
1320
|
+
* @param inputArguments - Input argument Variants (default: empty).
|
|
1321
|
+
* @param options - Request options (e.g. `returnDiagnostics`).
|
|
1322
|
+
* @returns A promise resolving to the CallMethodResult, with `requestHandle` available synchronously.
|
|
1323
|
+
*/
|
|
1324
|
+
callMethod(objectId, methodId, inputArguments = [], options) {
|
|
1325
|
+
const requestHandle = nextRequestHandle();
|
|
1326
|
+
const promise = this.withSessionRefresh(async () => {
|
|
1327
|
+
const request = new opcjsBase.CallMethodRequest();
|
|
1328
|
+
request.objectId = objectId;
|
|
1329
|
+
request.methodId = methodId;
|
|
1330
|
+
request.inputArguments = inputArguments.map((arg) => opcjsBase.Variant.newFrom(arg));
|
|
1331
|
+
const responses = await this.methodService.call([request], options?.returnDiagnostics, requestHandle);
|
|
1332
|
+
const response = responses[0];
|
|
1333
|
+
return new CallMethodResult(response.value, response.statusCode, response.diagnosticInfo);
|
|
1334
|
+
});
|
|
1335
|
+
return Object.assign(promise, { requestHandle });
|
|
1336
|
+
}
|
|
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(() => {
|
|
1353
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1354
|
+
return this.browseRecursive(nodeId, recursive, visited, options?.returnDiagnostics ?? 0, requestHandle);
|
|
1355
|
+
});
|
|
1356
|
+
return Object.assign(promise, { requestHandle });
|
|
1357
|
+
}
|
|
1358
|
+
async browseRecursive(nodeId, recursive, visited, returnDiagnostics, requestHandle) {
|
|
1359
|
+
const nodeKey = `${nodeId.namespace}:${nodeId.identifier}`;
|
|
1360
|
+
if (visited.has(nodeKey)) {
|
|
1361
|
+
return [];
|
|
1362
|
+
}
|
|
1363
|
+
visited.add(nodeKey);
|
|
1364
|
+
const description = new opcjsBase.BrowseDescription();
|
|
1365
|
+
description.nodeId = nodeId;
|
|
1366
|
+
description.browseDirection = opcjsBase.BrowseDirectionEnum.Forward;
|
|
1367
|
+
description.referenceTypeId = opcjsBase.NodeId.newNumeric(0, 33);
|
|
1368
|
+
description.includeSubtypes = true;
|
|
1369
|
+
description.nodeClassMask = 0;
|
|
1370
|
+
description.resultMask = opcjsBase.BrowseResultMaskEnum.All;
|
|
1371
|
+
const browseResults = await this.browseService.browse([description], returnDiagnostics, requestHandle);
|
|
1372
|
+
const browseResult = browseResults[0];
|
|
1373
|
+
const allReferences = [...browseResult.references ?? []];
|
|
1374
|
+
let continuationPoint = browseResult.continuationPoint;
|
|
1375
|
+
while (continuationPoint && continuationPoint.byteLength > 0) {
|
|
1376
|
+
const nextResults = await this.browseService.browseNext([continuationPoint], false, returnDiagnostics);
|
|
1377
|
+
const nextResult = nextResults[0];
|
|
1378
|
+
allReferences.push(...nextResult.references ?? []);
|
|
1379
|
+
continuationPoint = nextResult.continuationPoint;
|
|
1380
|
+
}
|
|
1381
|
+
const results = allReferences.map(
|
|
1382
|
+
(ref) => new BrowseNodeResult(
|
|
1383
|
+
ref.referenceTypeId,
|
|
1384
|
+
ref.isForward,
|
|
1385
|
+
ref.nodeId,
|
|
1386
|
+
ref.browseName,
|
|
1387
|
+
ref.displayName,
|
|
1388
|
+
ref.nodeClass,
|
|
1389
|
+
ref.typeDefinition
|
|
1390
|
+
)
|
|
1391
|
+
);
|
|
1392
|
+
if (recursive) {
|
|
1393
|
+
for (const ref of allReferences) {
|
|
1394
|
+
const childNodeId = opcjsBase.NodeId.newNumeric(
|
|
1395
|
+
ref.nodeId.nodeId.namespace,
|
|
1396
|
+
ref.nodeId.nodeId.identifier
|
|
1397
|
+
);
|
|
1398
|
+
const childResults = await this.browseRecursive(
|
|
1399
|
+
childNodeId,
|
|
1400
|
+
true,
|
|
1401
|
+
visited,
|
|
1402
|
+
returnDiagnostics
|
|
1403
|
+
);
|
|
1404
|
+
results.push(...childResults);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return results;
|
|
1408
|
+
}
|
|
1409
|
+
async subscribe(ids, callback, options) {
|
|
1410
|
+
this.subscriptionHandler?.subscribe(ids, callback, options);
|
|
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()");
|
|
394
1472
|
}
|
|
1473
|
+
await this.session.impersonate(identity);
|
|
1474
|
+
this.identity = identity;
|
|
395
1475
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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));
|
|
400
1504
|
}
|
|
401
|
-
|
|
402
|
-
|
|
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();
|
|
403
1575
|
}
|
|
404
1576
|
};
|
|
405
1577
|
var ConfigurationClient = class _ConfigurationClient extends opcjsBase.Configuration {
|
|
406
|
-
|
|
1578
|
+
/**
|
|
1579
|
+
* Optional security restrictions applied during `Client.connect()`.
|
|
1580
|
+
* When not set, permissive defaults are used (SecurityPolicy None allowed,
|
|
1581
|
+
* all user-token types accepted).
|
|
1582
|
+
*
|
|
1583
|
+
* @see SecurityConfiguration
|
|
1584
|
+
*/
|
|
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;
|
|
1606
|
+
static getSimple(name, company, loggerFactory) {
|
|
1607
|
+
if (!loggerFactory) {
|
|
1608
|
+
loggerFactory = new opcjsBase.LoggerFactory({
|
|
1609
|
+
defaultLevel: "DEBUG",
|
|
1610
|
+
//todo: use enum
|
|
1611
|
+
categoryLevels: {
|
|
1612
|
+
"transport.*": "TRACE",
|
|
1613
|
+
"secureChannel.*": "TRACE"
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
407
1617
|
const applicationUri = `urn:${company}:${name}`;
|
|
408
1618
|
const productUri = `urn:${company}:${name}:product`;
|
|
409
1619
|
const encoder = new opcjsBase.Encoder();
|
|
@@ -417,24 +1627,26 @@ var ConfigurationClient = class _ConfigurationClient extends opcjsBase.Configura
|
|
|
417
1627
|
});
|
|
418
1628
|
opcjsBase.registerTypeDecoders(decoder);
|
|
419
1629
|
opcjsBase.registerBinaryDecoders(decoder);
|
|
420
|
-
return new _ConfigurationClient(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
static newId(namespace, identifier) {
|
|
431
|
-
return new _Id(new opcjsBase.NodeId(namespace, identifier));
|
|
432
|
-
}
|
|
433
|
-
toNodeId() {
|
|
434
|
-
return this.nodeId;
|
|
1630
|
+
return new _ConfigurationClient(
|
|
1631
|
+
name,
|
|
1632
|
+
applicationUri,
|
|
1633
|
+
name,
|
|
1634
|
+
productUri,
|
|
1635
|
+
encoder,
|
|
1636
|
+
decoder,
|
|
1637
|
+
loggerFactory
|
|
1638
|
+
);
|
|
435
1639
|
}
|
|
436
|
-
|
|
437
|
-
|
|
1640
|
+
constructor(applicationName, applicationUri, productName, productUri, encoder, decoder, loggerFactory) {
|
|
1641
|
+
super(
|
|
1642
|
+
applicationName,
|
|
1643
|
+
applicationUri,
|
|
1644
|
+
productName,
|
|
1645
|
+
productUri,
|
|
1646
|
+
encoder,
|
|
1647
|
+
decoder,
|
|
1648
|
+
loggerFactory
|
|
1649
|
+
);
|
|
438
1650
|
}
|
|
439
1651
|
};
|
|
440
1652
|
var UserIdentity = class _UserIdentity {
|
|
@@ -480,9 +1692,46 @@ var UserIdentity = class _UserIdentity {
|
|
|
480
1692
|
}
|
|
481
1693
|
};
|
|
482
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
|
+
|
|
1725
|
+
exports.BrowseNodeResult = BrowseNodeResult;
|
|
1726
|
+
exports.CERTIFICATE_REQUIRED_STATUS_CODES = CERTIFICATE_REQUIRED_STATUS_CODES;
|
|
1727
|
+
exports.CallMethodResult = CallMethodResult;
|
|
1728
|
+
exports.CertificateRequiredError = CertificateRequiredError;
|
|
483
1729
|
exports.Client = Client;
|
|
484
1730
|
exports.ConfigurationClient = ConfigurationClient;
|
|
485
|
-
exports.
|
|
1731
|
+
exports.NamespaceTable = NamespaceTable;
|
|
1732
|
+
exports.ReturnDiagnosticsMask = ReturnDiagnosticsMask;
|
|
1733
|
+
exports.SECURITY_POLICY_NONE_URI = SECURITY_POLICY_NONE_URI;
|
|
1734
|
+
exports.SessionInvalidError = SessionInvalidError;
|
|
486
1735
|
exports.UserIdentity = UserIdentity;
|
|
487
1736
|
//# sourceMappingURL=index.cjs.map
|
|
488
1737
|
//# sourceMappingURL=index.cjs.map
|