node-opcua-client-browser 2.177.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/uacp.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * @module node-opcua-client-browser
4
+ * @internal
5
+ *
6
+ * UACP primitives usable in the browser.
7
+ *
8
+ * These are thin re-exports of the byte-level encoders/decoders from
9
+ * `node-opcua-transport`. Those modules are pure-JS (only `node:events` is
10
+ * pulled in, which Vite polyfills automatically) and are therefore safe to
11
+ * bundle for the browser. Consolidating them here gives us a single point of
12
+ * audit if a future release of `node-opcua-transport` introduces a Node-only
13
+ * import.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.readRawMessageHeader = exports.packTcpMessage = exports.TCPErrorMessage = exports.HelloMessage = exports.AcknowledgeMessage = void 0;
17
+ var node_opcua_transport_1 = require("node-opcua-transport");
18
+ Object.defineProperty(exports, "AcknowledgeMessage", { enumerable: true, get: function () { return node_opcua_transport_1.AcknowledgeMessage; } });
19
+ Object.defineProperty(exports, "HelloMessage", { enumerable: true, get: function () { return node_opcua_transport_1.HelloMessage; } });
20
+ Object.defineProperty(exports, "TCPErrorMessage", { enumerable: true, get: function () { return node_opcua_transport_1.TCPErrorMessage; } });
21
+ Object.defineProperty(exports, "packTcpMessage", { enumerable: true, get: function () { return node_opcua_transport_1.packTcpMessage; } });
22
+ Object.defineProperty(exports, "readRawMessageHeader", { enumerable: true, get: function () { return node_opcua_transport_1.readRawMessageHeader; } });
23
+ //# sourceMappingURL=uacp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uacp.js","sourceRoot":"","sources":["../source/uacp.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AAEH,6DAO8B;AAN1B,0HAAA,kBAAkB,OAAA;AAClB,oHAAA,YAAY,OAAA;AACZ,uHAAA,eAAe,OAAA;AACf,sHAAA,cAAc,OAAA;AACd,4HAAA,oBAAoB,OAAA"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @module node-opcua-client-browser
3
+ * @internal
4
+ *
5
+ * WebSocket → {@link ISocketLike} adapter.
6
+ *
7
+ * {@link TCP_transport} in `node-opcua-transport` expects an `ISocketLike` that
8
+ * emits `data`, `close`, `end`, `error`, `timeout` events and provides
9
+ * `write`, `end`, `destroy`, `setKeepAlive`, `setNoDelay`, `setTimeout`.
10
+ *
11
+ * This adapter turns a browser `WebSocket` (or the `ws` package used during
12
+ * unit tests) into that shape so the full HEL/ACK + chunk-assembly machinery
13
+ * in `TCP_transport` can be reused without change.
14
+ *
15
+ * Framing per OPC UA Part 6 §7.5: each outgoing UACP chunk is sent as exactly
16
+ * one binary WebSocket message. Incoming binary messages are concatenated via
17
+ * the `TCP_transport` packet-assembler, so even if a peer (e.g. `websockify`)
18
+ * splits a chunk across multiple frames, the assembler re-combines them.
19
+ * Text frames are ignored with a warning.
20
+ */
21
+ import { EventEmitter } from "events";
22
+ import type { ISocketLike } from "node-opcua-transport";
23
+ /**
24
+ * Minimal structural type that matches both the browser `WebSocket` global and
25
+ * the `ws` package's Node implementation.
26
+ */
27
+ export interface WebSocketLike {
28
+ readonly readyState: number;
29
+ binaryType: string;
30
+ send(data: ArrayBuffer | ArrayBufferView): void;
31
+ close(code?: number, reason?: string): void;
32
+ onopen: ((this: WebSocketLike, ev: unknown) => void) | null;
33
+ onclose: ((this: WebSocketLike, ev: {
34
+ code?: number;
35
+ reason?: string;
36
+ }) => void) | null;
37
+ onerror: ((this: WebSocketLike, ev: unknown) => void) | null;
38
+ onmessage: ((this: WebSocketLike, ev: {
39
+ data: unknown;
40
+ }) => void) | null;
41
+ }
42
+ /**
43
+ * Wrap a {@link WebSocketLike} as an {@link ISocketLike}. The returned socket
44
+ * forwards `data` events once the WS is open, and cleans up the WS listeners
45
+ * on `destroy` or `end`.
46
+ *
47
+ * The WebSocket must already be instantiated (i.e. `new WebSocket(url, protocols)`).
48
+ * The caller drives this by the time `_install_socket` is called; we just bind.
49
+ */
50
+ export declare class WsSocketAdapter extends EventEmitter implements ISocketLike {
51
+ private readonly ws;
52
+ remoteAddress?: string;
53
+ remotePort?: number;
54
+ destroyed: boolean;
55
+ private _timeoutHandle;
56
+ private _timeoutMs;
57
+ private _timeoutCb;
58
+ constructor(ws: WebSocketLike, url?: string);
59
+ write(data: string | Buffer, callback?: (err?: Error | null) => undefined | undefined): void;
60
+ end(): void;
61
+ destroy(_err?: Error): void;
62
+ setKeepAlive(_enable?: boolean, _initialDelay?: number): this;
63
+ setNoDelay(_noDelay?: boolean): this;
64
+ setTimeout(timeout: number, callback?: () => void): this;
65
+ private _resetTimeout;
66
+ private _clearTimeout;
67
+ }
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ /**
3
+ * @module node-opcua-client-browser
4
+ * @internal
5
+ *
6
+ * WebSocket → {@link ISocketLike} adapter.
7
+ *
8
+ * {@link TCP_transport} in `node-opcua-transport` expects an `ISocketLike` that
9
+ * emits `data`, `close`, `end`, `error`, `timeout` events and provides
10
+ * `write`, `end`, `destroy`, `setKeepAlive`, `setNoDelay`, `setTimeout`.
11
+ *
12
+ * This adapter turns a browser `WebSocket` (or the `ws` package used during
13
+ * unit tests) into that shape so the full HEL/ACK + chunk-assembly machinery
14
+ * in `TCP_transport` can be reused without change.
15
+ *
16
+ * Framing per OPC UA Part 6 §7.5: each outgoing UACP chunk is sent as exactly
17
+ * one binary WebSocket message. Incoming binary messages are concatenated via
18
+ * the `TCP_transport` packet-assembler, so even if a peer (e.g. `websockify`)
19
+ * splits a chunk across multiple frames, the assembler re-combines them.
20
+ * Text frames are ignored with a warning.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.WsSocketAdapter = void 0;
24
+ const events_1 = require("events");
25
+ // WebSocket readyState constants (avoid importing the DOM lib to stay Node-side compatible)
26
+ const WS_OPEN = 1;
27
+ const WS_CLOSING = 2;
28
+ const WS_CLOSED = 3;
29
+ function toBuffer(data) {
30
+ if (!data)
31
+ return null;
32
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(data))
33
+ return data;
34
+ if (data instanceof ArrayBuffer)
35
+ return Buffer.from(new Uint8Array(data));
36
+ if (ArrayBuffer.isView(data)) {
37
+ const view = data;
38
+ return Buffer.from(view.buffer, view.byteOffset, view.byteLength);
39
+ }
40
+ // Blob path: the transport's HEL/ACK uses `binaryType = "arraybuffer"`,
41
+ // so this should never hit. Fail loud if it does.
42
+ return null;
43
+ }
44
+ /**
45
+ * Wrap a {@link WebSocketLike} as an {@link ISocketLike}. The returned socket
46
+ * forwards `data` events once the WS is open, and cleans up the WS listeners
47
+ * on `destroy` or `end`.
48
+ *
49
+ * The WebSocket must already be instantiated (i.e. `new WebSocket(url, protocols)`).
50
+ * The caller drives this by the time `_install_socket` is called; we just bind.
51
+ */
52
+ class WsSocketAdapter extends events_1.EventEmitter {
53
+ ws;
54
+ remoteAddress;
55
+ remotePort;
56
+ destroyed = false;
57
+ _timeoutHandle = null;
58
+ _timeoutMs = 0;
59
+ _timeoutCb = null;
60
+ constructor(ws, url) {
61
+ super();
62
+ this.ws = ws;
63
+ if (url) {
64
+ try {
65
+ const u = new URL(url);
66
+ this.remoteAddress = u.hostname;
67
+ this.remotePort = u.port ? Number(u.port) : undefined;
68
+ }
69
+ catch {
70
+ /* ignore – URL parse already happened in connect() */
71
+ }
72
+ }
73
+ ws.binaryType = "arraybuffer";
74
+ const emitError = (err) => {
75
+ this.emit("error", err);
76
+ };
77
+ ws.onopen = () => {
78
+ this.emit("connect");
79
+ };
80
+ ws.onmessage = (ev) => {
81
+ // Reset inactivity timer if any
82
+ this._resetTimeout();
83
+ const buf = toBuffer(ev.data);
84
+ if (!buf) {
85
+ // Text frame or unknown type: ignore with a warning, per spec
86
+ // eslint-disable-next-line no-console
87
+ console.warn("[ClientWS_transport] ignoring non-binary WebSocket frame");
88
+ return;
89
+ }
90
+ this.emit("data", buf);
91
+ };
92
+ ws.onerror = () => {
93
+ // Browser WebSocket errors carry no useful detail. Translate into a
94
+ // generic Error so downstream log messages still make sense.
95
+ emitError(new Error("WebSocket error"));
96
+ };
97
+ ws.onclose = (ev) => {
98
+ this._clearTimeout();
99
+ // `hadError` is best-effort: anything other than 1000 (normal) is
100
+ // treated as an error path.
101
+ const hadError = !!ev && typeof ev.code === "number" && ev.code !== 1000;
102
+ this.emit("end");
103
+ this.emit("close", hadError);
104
+ };
105
+ }
106
+ write(data, callback) {
107
+ if (this.destroyed || this.ws.readyState >= WS_CLOSING) {
108
+ callback?.(new Error("WebSocket is not open"));
109
+ return;
110
+ }
111
+ try {
112
+ if (typeof data === "string") {
113
+ // UACP chunks are always binary; if we somehow got a string,
114
+ // encode to UTF-8.
115
+ this.ws.send(new TextEncoder().encode(data));
116
+ }
117
+ else {
118
+ // Send as ArrayBufferView so the WS layer keeps it as one binary frame.
119
+ const u8 = data instanceof Uint8Array ? data : new Uint8Array(data);
120
+ // `send()` accepts ArrayBuffer or ArrayBufferView; use the view so the
121
+ // underlying buffer isn't copied unnecessarily.
122
+ this.ws.send(u8);
123
+ }
124
+ callback?.();
125
+ }
126
+ catch (err) {
127
+ callback?.(err);
128
+ }
129
+ }
130
+ end() {
131
+ this._clearTimeout();
132
+ if (this.ws.readyState === WS_OPEN) {
133
+ try {
134
+ this.ws.close(1000, "normal closure");
135
+ }
136
+ catch {
137
+ /* swallow */
138
+ }
139
+ }
140
+ }
141
+ destroy(_err) {
142
+ this.destroyed = true;
143
+ this._clearTimeout();
144
+ if (this.ws.readyState < WS_CLOSED) {
145
+ try {
146
+ this.ws.close(1001, "going away");
147
+ }
148
+ catch {
149
+ /* swallow */
150
+ }
151
+ }
152
+ // Detach handlers so no late events fire into a destroyed transport.
153
+ try {
154
+ this.ws.onopen = null;
155
+ this.ws.onclose = null;
156
+ this.ws.onerror = null;
157
+ this.ws.onmessage = null;
158
+ }
159
+ catch {
160
+ /* swallow */
161
+ }
162
+ }
163
+ // These three are no-ops for WebSocket – keep-alive and nodelay are handled
164
+ // by the browser / underlying TCP stack; we expose them so TCP_transport's
165
+ // setup sequence runs unchanged.
166
+ setKeepAlive(_enable, _initialDelay) {
167
+ return this;
168
+ }
169
+ setNoDelay(_noDelay) {
170
+ return this;
171
+ }
172
+ setTimeout(timeout, callback) {
173
+ this._timeoutMs = timeout;
174
+ this._timeoutCb = callback ?? null;
175
+ this._resetTimeout();
176
+ return this;
177
+ }
178
+ _resetTimeout() {
179
+ this._clearTimeout();
180
+ if (this._timeoutMs > 0 && this._timeoutCb) {
181
+ const cb = this._timeoutCb;
182
+ this._timeoutHandle = setTimeout(() => {
183
+ this.emit("timeout");
184
+ cb();
185
+ }, this._timeoutMs);
186
+ }
187
+ }
188
+ _clearTimeout() {
189
+ if (this._timeoutHandle !== null) {
190
+ clearTimeout(this._timeoutHandle);
191
+ this._timeoutHandle = null;
192
+ }
193
+ }
194
+ }
195
+ exports.WsSocketAdapter = WsSocketAdapter;
196
+ //# sourceMappingURL=ws_socket_adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws_socket_adapter.js","sourceRoot":"","sources":["../source/ws_socket_adapter.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "node-opcua-client-browser",
3
+ "version": "2.177.0",
4
+ "description": "OPC UA SDK - browser WebSocket client (opc.ws / opc.wss)",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "browser": "./dist-esm/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "browser": "./dist-esm/index.js",
12
+ "import": "./dist-esm/index.js",
13
+ "require": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "build": "tsc -b && tsc -p tsconfig.esm.json",
20
+ "build:test-page": "node build-test-page.mjs",
21
+ "lint": "eslint source test",
22
+ "test:unit": "mocha",
23
+ "test:e2e": "playwright test",
24
+ "test": "pnpm test:unit && pnpm test:e2e",
25
+ "test:check": "tsc --noEmit -p test/tsconfig.json",
26
+ "clean": "npx rimraf -g node_modules dist dist-esm test/page/dist *.tsbuildinfo test-results playwright-report"
27
+ },
28
+ "dependencies": {
29
+ "node-opcua-assert": "2.175.6",
30
+ "node-opcua-client": "2.177.0",
31
+ "node-opcua-common": "2.177.0",
32
+ "node-opcua-crypto": "5.5.0",
33
+ "node-opcua-debug": "2.175.6",
34
+ "node-opcua-status-code": "2.176.0",
35
+ "node-opcua-transport": "2.177.0"
36
+ },
37
+ "devDependencies": {
38
+ "@playwright/test": "^1.62.1",
39
+ "@types/ws": "^8.18.1",
40
+ "buffer": "^6.0.3",
41
+ "esbuild": "^0.28.2",
42
+ "events": "^3.3.0",
43
+ "node-opcua-leak-detector": "2.175.6",
44
+ "process": "^0.11.10",
45
+ "util": "^0.12.5",
46
+ "ws": "^8.21.3"
47
+ },
48
+ "author": "Sterfive SAS",
49
+ "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git://github.com/node-opcua/node-opcua.git"
53
+ },
54
+ "keywords": [
55
+ "OPCUA",
56
+ "opcua",
57
+ "browser",
58
+ "websocket",
59
+ "wsopc-ua",
60
+ "opc.ws",
61
+ "opc.wss"
62
+ ],
63
+ "homepage": "http://node-opcua.github.io/",
64
+ "files": [
65
+ "dist",
66
+ "dist-esm",
67
+ "source"
68
+ ],
69
+ "gitHead": "064029878247adf2fa5d14a8124e1930431e2428"
70
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @module node-opcua-client-browser
3
+ *
4
+ * `ClientWS_transport` — OPC UA WebSocket client transport (Part 6 §7.5).
5
+ *
6
+ * Subclasses `ClientTCP_transport` and reuses its HEL/ACK + chunk-reassembly
7
+ * machinery unchanged. The only swap is the socket-creation step: instead of
8
+ * opening a TCP connection, we open a `WebSocket` and hand a
9
+ * {@link WsSocketAdapter} to `TCP_transport._install_socket`.
10
+ *
11
+ * OPC UA WebSocket framing:
12
+ * - One UACP chunk per outgoing binary WebSocket frame (handled naturally —
13
+ * each `write(chunk)` call invokes `ws.send(chunk)` once).
14
+ * - Incoming bytes are fed into `TCP_transport`'s packet-assembler, which
15
+ * re-combines split chunks.
16
+ * - Subprotocol `opcua+uacp` is requested on the handshake. If the server
17
+ * echoes it back we record that; if not (common with a `websockify`
18
+ * bridge) we continue without failing — browsers still accept the
19
+ * connection provided the server's bytes are valid UACP.
20
+ */
21
+
22
+ /* global WebSocket */
23
+
24
+ import { assert } from "node-opcua-assert";
25
+ import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
26
+ import type { ErrorCallback } from "node-opcua-status-code";
27
+ import {
28
+ ClientTransportBase,
29
+ type IClientTransport,
30
+ type IClientTransportFactory,
31
+ type ISocketLike,
32
+ type TransportSettingsOptions
33
+ } from "node-opcua-transport";
34
+
35
+ import { type WebSocketLike, WsSocketAdapter } from "./ws_socket_adapter";
36
+
37
+ const debugLog = make_debugLog("ClientWS_transport");
38
+ const warningLog = make_warningLog("ClientWS_transport");
39
+ const doDebug = checkDebugFlag("ClientWS_transport");
40
+
41
+ /** WebSocket subprotocol name defined by OPC UA Part 6 §7.5 */
42
+ export const OPCUA_UACP_SUBPROTOCOL = "opcua+uacp";
43
+
44
+ /**
45
+ * A tiny WebSocket-constructor type. In the browser this is `globalThis.WebSocket`;
46
+ * in Node-side unit tests this is the `ws` package's `default` export. Either works.
47
+ */
48
+ export type WebSocketConstructor = new (url: string, protocols?: string | string[]) => WebSocketLike;
49
+
50
+ /** Options specific to the browser WebSocket transport. */
51
+ export interface ClientWSTransportOptions extends TransportSettingsOptions {
52
+ /**
53
+ * The WebSocket constructor to use. Defaults to `globalThis.WebSocket`.
54
+ * Override this from Node-side unit tests to inject the `ws` package.
55
+ */
56
+ webSocketCtor?: WebSocketConstructor;
57
+
58
+ /**
59
+ * When `true`, `connect()` fails if the peer does not echo back the
60
+ * `opcua+uacp` subprotocol on the WebSocket opening handshake. Defaults to
61
+ * `false` because `websockify` bridges do not negotiate subprotocols; the
62
+ * OPC UA bytes that follow will fail the HEL/ACK step anyway if the peer
63
+ * isn't actually speaking UACP.
64
+ */
65
+ strictSubprotocol?: boolean;
66
+ }
67
+
68
+ /**
69
+ * Parsed endpoint URL, normalised for the browser `WebSocket` constructor.
70
+ */
71
+ export interface ParsedWsEndpoint {
72
+ /** The URL to pass to `new WebSocket(url, …)` — always starts with `ws://` or `wss://`. */
73
+ wsUrl: string;
74
+ /** `true` if the original URL used `opc.wss://` or `wss://`. */
75
+ secure: boolean;
76
+ }
77
+
78
+ /**
79
+ * Parse and normalise an OPC UA WebSocket endpoint URL. Accepts all four
80
+ * forms: `opc.ws://`, `opc.wss://`, `ws://`, `wss://`. Anything else throws.
81
+ *
82
+ * @throws {Error} if the scheme is not one of the four accepted forms.
83
+ */
84
+ export function parseWsEndpointUrl(endpointUrl: string): ParsedWsEndpoint {
85
+ const m = /^(opc\.ws|opc\.wss|ws|wss):\/\/(.+)$/i.exec(endpointUrl);
86
+ if (!m) {
87
+ throw new Error(
88
+ `[node-opcua-client-browser] unsupported endpoint URL scheme for WebSocket transport: ${endpointUrl} ` +
89
+ `(expected one of opc.ws://, opc.wss://, ws://, wss://)`
90
+ );
91
+ }
92
+ const scheme = m[1].toLowerCase();
93
+ const secure = scheme === "opc.wss" || scheme === "wss";
94
+ const wsUrl = `${secure ? "wss" : "ws"}://${m[2]}`;
95
+ return { wsUrl, secure };
96
+ }
97
+
98
+ /**
99
+ * WebSocket-backed client transport. A sibling of `ClientTCP_transport`: both extend
100
+ * {@link ClientTransportBase} and reuse its UACP HEL/ACK machinery; the only
101
+ * difference is the socket flavour (`WebSocket` here, `net.Socket` in TCP).
102
+ */
103
+ export class ClientWS_transport extends ClientTransportBase {
104
+ private readonly _webSocketCtor: WebSocketConstructor;
105
+ private readonly _strictSubprotocol: boolean;
106
+
107
+ constructor(options?: ClientWSTransportOptions) {
108
+ super(options);
109
+ const resolvedCtor =
110
+ options?.webSocketCtor ??
111
+ ((typeof globalThis !== "undefined" ? (globalThis as { WebSocket?: WebSocketConstructor }).WebSocket : undefined) as
112
+ | WebSocketConstructor
113
+ | undefined);
114
+ if (!resolvedCtor) {
115
+ throw new Error(
116
+ "[ClientWS_transport] no WebSocket constructor available: pass `webSocketCtor` explicitly or run in a browser / Node 22+"
117
+ );
118
+ }
119
+ this._webSocketCtor = resolvedCtor;
120
+ this._strictSubprotocol = !!options?.strictSubprotocol;
121
+ }
122
+
123
+ /**
124
+ * Opens a WebSocket to `endpointUrl`, adapts it to `ISocketLike`, and
125
+ * drives the inherited HEL/ACK transaction from `ClientTransportBase`.
126
+ */
127
+ public override connect(endpointUrl: string, callback: ErrorCallback): void {
128
+ this.endpointUrl = endpointUrl;
129
+
130
+ /* c8 ignore next */
131
+ doDebug && debugLog(`ClientWS_transport#connect(endpointUrl = ${endpointUrl})`);
132
+
133
+ let parsed: ParsedWsEndpoint;
134
+ try {
135
+ parsed = parseWsEndpointUrl(endpointUrl);
136
+ } catch (err) {
137
+ callback(err as Error);
138
+ return;
139
+ }
140
+
141
+ let ws: WebSocketLike;
142
+ try {
143
+ ws = new this._webSocketCtor(parsed.wsUrl, OPCUA_UACP_SUBPROTOCOL);
144
+ } catch (err) {
145
+ /* c8 ignore next */
146
+ doDebug && debugLog("WebSocket construction failed", (err as Error).message);
147
+ callback(err as Error);
148
+ return;
149
+ }
150
+
151
+ const socket: ISocketLike = new WsSocketAdapter(ws, parsed.wsUrl);
152
+
153
+ const onEarlyError = (err: Error) => {
154
+ callback(err);
155
+ };
156
+ socket.once("error", onEarlyError);
157
+
158
+ socket.once("connect", () => {
159
+ socket.removeListener("error", onEarlyError);
160
+
161
+ // Subprotocol negotiation check. Browser WS exposes `protocol` on the
162
+ // instance; `ws` package exposes it too. When the peer doesn't
163
+ // negotiate (e.g. a websockify bridge), `protocol` is an empty
164
+ // string. Only fail in strict mode.
165
+ const negotiated: string | undefined = (ws as unknown as { protocol?: string }).protocol;
166
+ if (negotiated && negotiated !== OPCUA_UACP_SUBPROTOCOL) {
167
+ const msg = `unexpected WebSocket subprotocol negotiated: "${negotiated}" (expected "${OPCUA_UACP_SUBPROTOCOL}")`;
168
+ if (this._strictSubprotocol) {
169
+ callback(new Error(`[ClientWS_transport] ${msg}`));
170
+ socket.destroy();
171
+ return;
172
+ }
173
+ warningLog(msg);
174
+ } else if (!negotiated) {
175
+ /* c8 ignore next */
176
+ doDebug &&
177
+ debugLog(
178
+ `peer did not echo the "${OPCUA_UACP_SUBPROTOCOL}" subprotocol; continuing ` +
179
+ `(set strictSubprotocol=true to reject this)`
180
+ );
181
+ }
182
+
183
+ // Install the WebSocket-backed socket adapter into the inherited TCP_transport machinery.
184
+ assert(!this._socket, "transport should not have a socket yet");
185
+ this._install_socket(socket);
186
+
187
+ this._perform_HEL_ACK_transaction((err) => {
188
+ if (!err) {
189
+ /* c8 ignore next */
190
+ if (!this._socket) {
191
+ return callback(new Error("Abandoned"));
192
+ }
193
+ this._install_post_connect_error_handler(endpointUrl);
194
+ this.emit("connect");
195
+ } else {
196
+ debugLog("_perform_HEL_ACK_transaction has failed with err=", err.message);
197
+ }
198
+ callback(err);
199
+ });
200
+ });
201
+ }
202
+ public override dispose(): void {
203
+ /* c8 ignore next */
204
+ doDebug && debugLog(" ClientWS_transport disposed");
205
+
206
+ super.dispose();
207
+ }
208
+ }
209
+
210
+ /**
211
+ * A factory that produces {@link ClientWS_transport} instances. Pass to
212
+ * `ClientSecureChannelLayerOptions.transportFactory` to route all channel
213
+ * traffic through a WebSocket.
214
+ *
215
+ * @example
216
+ * const ws = browserWsTransportFactory.create({});
217
+ * // or, on Node:
218
+ * import WebSocket from "ws";
219
+ * const factory: IClientTransportFactory = {
220
+ * create(s) { return new ClientWS_transport({ ...s, webSocketCtor: WebSocket as any }); }
221
+ * };
222
+ */
223
+ export const browserWsTransportFactory: IClientTransportFactory = {
224
+ create(settings?: TransportSettingsOptions): IClientTransport {
225
+ return new ClientWS_transport(settings as ClientWSTransportOptions | undefined);
226
+ }
227
+ };