node-opcua-transport 2.170.0 → 2.173.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.
@@ -8,28 +8,27 @@ import { types } from "node:util";
8
8
  import chalk from "chalk";
9
9
 
10
10
  import { assert } from "node-opcua-assert";
11
- import { BinaryStream } from "node-opcua-binary-stream";
12
- import { readMessageHeader } from "node-opcua-chunkmanager";
13
- import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
11
+ import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
14
12
  import type { ErrorCallback } from "node-opcua-status-code";
15
- import { AcknowledgeMessage } from "./AcknowledgeMessage";
16
- import { HelloMessage } from "./HelloMessage";
17
- import { TCPErrorMessage } from "./TCPErrorMessage";
18
- import { getFakeTransport, type ISocketLike, TCP_transport } from "./tcp_transport";
19
- import { decodeMessage, packTcpMessage, parseEndpointUrl } from "./tools";
20
- import { doTraceHelloAck } from "./utils";
13
+
14
+ import { ClientTransportBase } from "./client_transport_base";
15
+ import type { TransportSettingsOptions } from "./i_client_transport";
16
+ import { getFakeTransport, type ISocketLike } from "./tcp_transport";
17
+ import { parseEndpointUrl } from "./tools";
21
18
 
22
19
  const doDebug = checkDebugFlag(__filename);
23
20
  const debugLog = make_debugLog(__filename);
24
- const warningLog = make_warningLog(__filename);
25
21
  const errorLog = make_errorLog(__filename);
26
22
  const gHostname = os.hostname();
27
23
 
24
+ // Re-exported for source-level backwards compatibility: this type used to live in this file.
25
+ export type { TransportSettingsOptions };
26
+
28
27
  function createClientSocket(endpointUrl: string, timeout: number): ISocketLike {
29
28
  // create a socket based on Url
30
29
  const ep = parseEndpointUrl(endpointUrl);
31
- const port = parseInt(ep.port!, 10);
32
- const hostname = ep.hostname!;
30
+ const port = parseInt(ep.port || "4840", 10);
31
+ const hostname = ep.hostname;
33
32
 
34
33
  let socket: ISocketLike;
35
34
  switch (ep.protocol) {
@@ -53,29 +52,6 @@ function createClientSocket(endpointUrl: string, timeout: number): ISocketLike {
53
52
  }
54
53
  }
55
54
  }
56
- export interface ClientTCP_transport {
57
- on(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
58
- on(eventName: "close", eventHandler: (err: Error | null) => void): this;
59
- on(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
60
- on(eventName: "connect", eventHandler: () => void): this;
61
-
62
- once(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
63
- once(eventName: "close", eventHandler: (err: Error | null) => void): this;
64
- once(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
65
- once(eventName: "connect", eventHandler: () => void): this;
66
-
67
- emit(eventName: "chunk", messageChunk: Buffer): boolean;
68
- emit(eventName: "close", err?: Error | null): boolean;
69
- emit(eventName: "connection_break", err?: Error | null): boolean;
70
- emit(eventName: "connect"): boolean;
71
- }
72
-
73
- export interface TransportSettingsOptions {
74
- maxChunkCount?: number;
75
- maxMessageSize?: number;
76
- receiveBufferSize?: number;
77
- sendBufferSize?: number;
78
- }
79
55
 
80
56
  /**
81
57
  * a ClientTCP_transport connects to a remote server socket and
@@ -112,50 +88,7 @@ export interface TransportSettingsOptions {
112
88
  *
113
89
  *
114
90
  */
115
- // biome-ignore lint/suspicious/noUnsafeDeclarationMerging: typed EventEmitter events - extending parent's event map in subclass hierarchy
116
- export class ClientTCP_transport extends TCP_transport {
117
- public static defaultMaxChunk = 0; // 0 - no limits
118
- public static defaultMaxMessageSize = 0; // 0 - no limits
119
- public static defaultReceiveBufferSize = 1024 * 64 * 10;
120
- public static defaultSendBufferSize = 1024 * 64 * 10; // 8192 min,
121
-
122
- public endpointUrl: string;
123
- public serverUri: string;
124
- public numberOfRetry: number;
125
- public parameters?: AcknowledgeMessage;
126
-
127
- private _counter: number;
128
- private _helloSettings: {
129
- maxChunkCount: number;
130
- maxMessageSize: number;
131
- receiveBufferSize: number;
132
- sendBufferSize: number;
133
- };
134
- constructor(transportSettings?: TransportSettingsOptions) {
135
- super();
136
- this.endpointUrl = "";
137
- this.serverUri = "";
138
- this._counter = 0;
139
- this.numberOfRetry = 0;
140
-
141
- // initially before HEL/ACK
142
- this.maxChunkCount = 1;
143
- this.maxMessageSize = 4 * 1024;
144
- this.receiveBufferSize = 4 * 1024;
145
-
146
- transportSettings = transportSettings || {};
147
- this._helloSettings = {
148
- maxChunkCount: transportSettings.maxChunkCount || ClientTCP_transport.defaultMaxChunk,
149
- maxMessageSize: transportSettings.maxMessageSize || ClientTCP_transport.defaultMaxMessageSize,
150
- receiveBufferSize: transportSettings.receiveBufferSize || ClientTCP_transport.defaultReceiveBufferSize,
151
- sendBufferSize: transportSettings.sendBufferSize || ClientTCP_transport.defaultSendBufferSize
152
- };
153
- }
154
-
155
- public getTransportSettings(): TransportSettingsOptions {
156
- return this._helloSettings;
157
- }
158
-
91
+ export class ClientTCP_transport extends ClientTransportBase {
159
92
  public dispose(): void {
160
93
  /* c8 ignore next */
161
94
  doDebug && debugLog(" ClientTCP_transport disposed");
@@ -170,7 +103,8 @@ export class ClientTCP_transport extends TCP_transport {
170
103
  doDebug && debugLog(chalk.cyan(`ClientTCP_transport#connect(endpointUrl = ${endpointUrl})`));
171
104
  let socket: ISocketLike | null = null;
172
105
  try {
173
- const _ep = parseEndpointUrl(endpointUrl);
106
+ // validate the URL upfront so a parse error is reported synchronously
107
+ parseEndpointUrl(endpointUrl);
174
108
  socket = createClientSocket(endpointUrl, this.timeout);
175
109
 
176
110
  socket.setTimeout(this.timeout >> 1, () => {
@@ -184,34 +118,6 @@ export class ClientTCP_transport extends TCP_transport {
184
118
  return;
185
119
  }
186
120
 
187
- /**
188
- *
189
- */
190
- const _on_socket_error_after_connection = (err: Error) => {
191
- /* c8 ignore next */
192
- doDebug && debugLog(" _on_socket_error_after_connection ClientTCP_transport Socket Error", err.message);
193
-
194
- // EPIPE : EPIPE (Broken pipe): A write on a pipe, socket, or FIFO for which there is no process to read the
195
- // data. Commonly encountered at the net and http layers, indicative that the remote side of the stream
196
- // being written to has been closed.
197
-
198
- // ECONNRESET (Connection reset by peer): A connection was forcibly closed by a peer. This normally results
199
- // from a loss of the connection on the remote socket due to a timeout or reboot. Commonly encountered
200
- // via the http and net module
201
-
202
- // socket termination could happen:
203
- // * when the socket times out (lost of connection, network outage, etc...)
204
- // * or, when the server abruptly disconnects the socket ( in case of invalid communication for instance)
205
- if (err.message.match(/ECONNRESET|EPIPE|premature socket termination/)) {
206
- /**
207
- * @event connection_break
208
- *
209
- */
210
- doDebug && debugLog("connection_break after reconnection", endpointUrl);
211
- this.emit("connection_break", err);
212
- }
213
- };
214
-
215
121
  const _on_socket_connect = () => {
216
122
  /* c8 ignore next */
217
123
  doDebug && debugLog("entering _on_socket_connect");
@@ -223,13 +129,12 @@ export class ClientTCP_transport extends TCP_transport {
223
129
  if (!this._socket) {
224
130
  return callback(new Error("Abandoned"));
225
131
  }
226
- // install error handler to detect connection break
227
- this._socket.on("error", _on_socket_error_after_connection);
132
+ // install the post-connect "connection break" detector inherited from ClientTransportBase
133
+ this._install_post_connect_error_handler(endpointUrl);
228
134
  /**
229
135
  * notify the observers that the transport is connected (the socket is connected and the the HEL/ACK
230
136
  * transaction has been done)
231
137
  * @event connect
232
- *
233
138
  */
234
139
  this.emit("connect");
235
140
  } else {
@@ -269,110 +174,4 @@ export class ClientTCP_transport extends TCP_transport {
269
174
  this._socket?.once("end", _on_socket_end_for_connect);
270
175
  this._socket?.once("connect", _on_socket_connect);
271
176
  }
272
-
273
- private _handle_ACK_response(messageChunk: Buffer, callback: ErrorCallback) {
274
- const _stream = new BinaryStream(messageChunk);
275
- const messageHeader = readMessageHeader(_stream);
276
- let err: Error | null = null;
277
- /* c8 ignore next */
278
- if (messageHeader.isFinal !== "F") {
279
- err = new Error(" invalid ACK message");
280
- return callback(err);
281
- }
282
-
283
- let responseClass: typeof AcknowledgeMessage | typeof TCPErrorMessage;
284
- let response: AcknowledgeMessage | TCPErrorMessage;
285
-
286
- if (messageHeader.msgType === "ERR") {
287
- responseClass = TCPErrorMessage;
288
- _stream.rewind();
289
- response = decodeMessage(_stream, responseClass) as TCPErrorMessage;
290
-
291
- err = new Error(`ACK: ERR received ${response.statusCode.toString()} : ${response.reason}`);
292
- (err as any).statusCode = response.statusCode;
293
- // c8 ignore next
294
- doTraceHelloAck && warningLog("receiving ERR instead of Ack", response.toString());
295
-
296
- callback(err);
297
- } else {
298
- responseClass = AcknowledgeMessage;
299
- _stream.rewind();
300
- response = decodeMessage(_stream, responseClass) as AcknowledgeMessage;
301
-
302
- this.parameters = response;
303
- this.setLimits(response);
304
-
305
- // c8 ignore next
306
- doTraceHelloAck && warningLog("receiving Ack\n", response.toString());
307
-
308
- callback();
309
- }
310
- }
311
-
312
- private _send_HELLO_request() {
313
- /* c8 ignore next */
314
- doDebug && debugLog("entering _send_HELLO_request");
315
-
316
- assert(this._socket);
317
- assert(Number.isFinite(this.protocolVersion));
318
- assert(this.endpointUrl.length > 0, " expecting a valid endpoint url");
319
-
320
- const { maxChunkCount, maxMessageSize, receiveBufferSize, sendBufferSize } = this._helloSettings;
321
-
322
- // Write a message to the socket as soon as the client is connected,
323
- // the server will receive it as message from the client
324
- const helloMessage = new HelloMessage({
325
- endpointUrl: this.endpointUrl,
326
- protocolVersion: this.protocolVersion,
327
- maxChunkCount,
328
- maxMessageSize,
329
- receiveBufferSize,
330
- sendBufferSize
331
- });
332
- // c8 ignore next
333
- doTraceHelloAck && warningLog(`sending Hello\n ${helloMessage.toString()} `);
334
-
335
- const messageChunk = packTcpMessage("HEL", helloMessage);
336
- this._write_chunk(messageChunk);
337
- }
338
-
339
- private _on_ACK_response(externalCallback: ErrorCallback, err: Error | null, data?: Buffer) {
340
- /* c8 ignore next */
341
- doDebug && debugLog("entering _on_ACK_response");
342
-
343
- assert(typeof externalCallback === "function");
344
- assert(this._counter === 0, "Ack response should only be received once !");
345
- this._counter += 1;
346
-
347
- if (err || !data) {
348
- if (this._socket) {
349
- const s = this._socket;
350
- this._socket = null;
351
- s.destroy();
352
- }
353
- externalCallback(err || new Error("no data"));
354
- } else {
355
- this._handle_ACK_response(data, externalCallback);
356
- }
357
- }
358
-
359
- private _perform_HEL_ACK_transaction(callback: ErrorCallback) {
360
- /* c8 ignore next */
361
- if (!this._socket) {
362
- return callback(new Error("No socket available to perform HEL/ACK transaction"));
363
- }
364
- assert(this._socket, "expecting a valid socket to send a message");
365
- assert(typeof callback === "function");
366
- this._counter = 0;
367
- /* c8 ignore next */
368
- doDebug && debugLog("entering _perform_HEL_ACK_transaction");
369
-
370
- this._install_one_time_message_receiver((err: Error | null, data?: Buffer) => {
371
- /* c8 ignore next */
372
- doDebug && debugLog("before _on_ACK_response ", err ? err.message : "");
373
-
374
- this._on_ACK_response(callback, err, data);
375
- });
376
- this._send_HELLO_request();
377
- }
378
177
  }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @module node-opcua-transport
3
+ *
4
+ * Transport-agnostic base class for client-side OPC UA transports.
5
+ *
6
+ * Owns the UACP HEL/ACK handshake, the negotiated transport settings, and the
7
+ * post-connect connection-break detector. Concrete subclasses (`ClientTCP_transport`,
8
+ * `ClientWS_transport`, ...) implement only the socket-creation step in `connect()`.
9
+ *
10
+ * Browser-safe: does not import `node:net`, `node:os`, `node:util`, or any other
11
+ * Node-only built-in beyond what `TCP_transport` already inherits from `node:events`.
12
+ */
13
+
14
+ import { assert } from "node-opcua-assert";
15
+ import { BinaryStream } from "node-opcua-binary-stream";
16
+ import { readMessageHeader } from "node-opcua-chunkmanager";
17
+ import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
18
+ import type { ErrorCallback } from "node-opcua-status-code";
19
+
20
+ import { AcknowledgeMessage } from "./AcknowledgeMessage";
21
+ import { HelloMessage } from "./HelloMessage";
22
+ import type { TransportSettingsOptions } from "./i_client_transport";
23
+ import { TCPErrorMessage } from "./TCPErrorMessage";
24
+ import { TCP_transport } from "./tcp_transport";
25
+ import { decodeMessage, packTcpMessage } from "./tools";
26
+ import { doTraceHelloAck } from "./utils";
27
+
28
+ // Use a string category instead of `__filename` so the module loads in
29
+ // browsers without a Node-style filename global.
30
+ const doDebug = checkDebugFlag("ClientTransportBase");
31
+ const debugLog = make_debugLog("ClientTransportBase");
32
+ const warningLog = make_warningLog("ClientTransportBase");
33
+
34
+ export interface ClientTransportBase {
35
+ on(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
36
+ on(eventName: "close", eventHandler: (err: Error | null) => void): this;
37
+ on(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
38
+ on(eventName: "connect", eventHandler: () => void): this;
39
+
40
+ once(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
41
+ once(eventName: "close", eventHandler: (err: Error | null) => void): this;
42
+ once(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
43
+ once(eventName: "connect", eventHandler: () => void): this;
44
+
45
+ emit(eventName: "chunk", messageChunk: Buffer): boolean;
46
+ emit(eventName: "close", err?: Error | null): boolean;
47
+ emit(eventName: "connection_break", err?: Error | null): boolean;
48
+ emit(eventName: "connect"): boolean;
49
+ }
50
+
51
+ // biome-ignore lint/suspicious/noUnsafeDeclarationMerging: companion to the interface above
52
+ export abstract class ClientTransportBase extends TCP_transport {
53
+ public static defaultMaxChunk = 0; // 0 - no limits
54
+ public static defaultMaxMessageSize = 0; // 0 - no limits
55
+ public static defaultReceiveBufferSize = 1024 * 64 * 10;
56
+ public static defaultSendBufferSize = 1024 * 64 * 10; // 8192 min,
57
+
58
+ public endpointUrl: string;
59
+ public serverUri: string;
60
+ public numberOfRetry: number;
61
+ public parameters?: AcknowledgeMessage;
62
+
63
+ private _counter: number;
64
+ private _helloSettings: {
65
+ maxChunkCount: number;
66
+ maxMessageSize: number;
67
+ receiveBufferSize: number;
68
+ sendBufferSize: number;
69
+ };
70
+
71
+ constructor(transportSettings?: TransportSettingsOptions) {
72
+ super();
73
+ this.endpointUrl = "";
74
+ this.serverUri = "";
75
+ this._counter = 0;
76
+ this.numberOfRetry = 0;
77
+
78
+ // initially before HEL/ACK
79
+ this.maxChunkCount = 1;
80
+ this.maxMessageSize = 4 * 1024;
81
+ this.receiveBufferSize = 4 * 1024;
82
+
83
+ transportSettings = transportSettings || {};
84
+ this._helloSettings = {
85
+ maxChunkCount: transportSettings.maxChunkCount || ClientTransportBase.defaultMaxChunk,
86
+ maxMessageSize: transportSettings.maxMessageSize || ClientTransportBase.defaultMaxMessageSize,
87
+ receiveBufferSize: transportSettings.receiveBufferSize || ClientTransportBase.defaultReceiveBufferSize,
88
+ sendBufferSize: transportSettings.sendBufferSize || ClientTransportBase.defaultSendBufferSize
89
+ };
90
+ }
91
+
92
+ public getTransportSettings(): TransportSettingsOptions {
93
+ return this._helloSettings;
94
+ }
95
+
96
+ public dispose(): void {
97
+ /* c8 ignore next */
98
+ doDebug && debugLog(" ClientTransportBase disposed");
99
+
100
+ super.dispose();
101
+ }
102
+
103
+ /**
104
+ * Connect to `endpointUrl` and perform the UACP HEL/ACK handshake.
105
+ * Concrete subclasses are responsible for opening the underlying socket
106
+ * (TCP, WebSocket, ...) and then driving the inherited HEL/ACK machinery.
107
+ */
108
+ public abstract connect(endpointUrl: string, callback: ErrorCallback): void;
109
+
110
+ /**
111
+ * Install the post-connect "connection break" detector. Subclasses call this
112
+ * once the underlying socket is open and the HEL/ACK transaction has succeeded.
113
+ *
114
+ * Detects ECONNRESET / EPIPE / premature socket termination on the live socket
115
+ * and re-emits them as `connection_break` so reconnection logic upstream can
116
+ * react.
117
+ */
118
+ protected _install_post_connect_error_handler(endpointUrl: string): void {
119
+ if (!this._socket) return;
120
+ this._socket.on("error", (err: Error) => {
121
+ // EPIPE : a write on a pipe/socket/FIFO with no reader.
122
+ // ECONNRESET : connection forcibly closed by the peer (timeout, reboot, ...).
123
+ // "premature socket termination" : abrupt close mid-message.
124
+ if (err.message.match(/ECONNRESET|EPIPE|premature socket termination/)) {
125
+ /* c8 ignore next */
126
+ doDebug && debugLog("connection_break after reconnection", endpointUrl);
127
+ this.emit("connection_break", err);
128
+ }
129
+ });
130
+ }
131
+
132
+ protected _perform_HEL_ACK_transaction(callback: ErrorCallback): void {
133
+ /* c8 ignore next */
134
+ if (!this._socket) {
135
+ callback(new Error("No socket available to perform HEL/ACK transaction"));
136
+ return;
137
+ }
138
+ assert(this._socket, "expecting a valid socket to send a message");
139
+ assert(typeof callback === "function");
140
+ this._counter = 0;
141
+ /* c8 ignore next */
142
+ doDebug && debugLog("entering _perform_HEL_ACK_transaction");
143
+
144
+ this._install_one_time_message_receiver((err: Error | null, data?: Buffer) => {
145
+ /* c8 ignore next */
146
+ doDebug && debugLog("before _on_ACK_response ", err ? err.message : "");
147
+
148
+ this._on_ACK_response(callback, err, data);
149
+ });
150
+ this._send_HELLO_request();
151
+ }
152
+
153
+ private _send_HELLO_request(): void {
154
+ /* c8 ignore next */
155
+ doDebug && debugLog("entering _send_HELLO_request");
156
+
157
+ assert(this._socket);
158
+ assert(Number.isFinite(this.protocolVersion));
159
+ assert(this.endpointUrl.length > 0, " expecting a valid endpoint url");
160
+
161
+ const { maxChunkCount, maxMessageSize, receiveBufferSize, sendBufferSize } = this._helloSettings;
162
+
163
+ const helloMessage = new HelloMessage({
164
+ endpointUrl: this.endpointUrl,
165
+ protocolVersion: this.protocolVersion,
166
+ maxChunkCount,
167
+ maxMessageSize,
168
+ receiveBufferSize,
169
+ sendBufferSize
170
+ });
171
+ // c8 ignore next
172
+ doTraceHelloAck && warningLog(`sending Hello\n ${helloMessage.toString()} `);
173
+
174
+ const messageChunk = packTcpMessage("HEL", helloMessage);
175
+ this._write_chunk(messageChunk);
176
+ }
177
+
178
+ private _on_ACK_response(externalCallback: ErrorCallback, err: Error | null, data?: Buffer): void {
179
+ /* c8 ignore next */
180
+ doDebug && debugLog("entering _on_ACK_response");
181
+
182
+ assert(typeof externalCallback === "function");
183
+ assert(this._counter === 0, "Ack response should only be received once !");
184
+ this._counter += 1;
185
+
186
+ if (err || !data) {
187
+ if (this._socket) {
188
+ const s = this._socket;
189
+ this._socket = null;
190
+ s.destroy();
191
+ }
192
+ externalCallback(err || new Error("no data"));
193
+ } else {
194
+ this._handle_ACK_response(data, externalCallback);
195
+ }
196
+ }
197
+
198
+ private _handle_ACK_response(messageChunk: Buffer, callback: ErrorCallback): void {
199
+ const _stream = new BinaryStream(messageChunk);
200
+ const messageHeader = readMessageHeader(_stream);
201
+ let err: Error | null = null;
202
+ /* c8 ignore next */
203
+ if (messageHeader.isFinal !== "F") {
204
+ err = new Error(" invalid ACK message");
205
+ callback(err);
206
+ return;
207
+ }
208
+
209
+ let responseClass: typeof AcknowledgeMessage | typeof TCPErrorMessage;
210
+ let response: AcknowledgeMessage | TCPErrorMessage;
211
+
212
+ if (messageHeader.msgType === "ERR") {
213
+ responseClass = TCPErrorMessage;
214
+ _stream.rewind();
215
+ response = decodeMessage(_stream, responseClass) as TCPErrorMessage;
216
+
217
+ err = new Error(`ACK: ERR received ${response.statusCode.toString()} : ${response.reason}`);
218
+ // biome-ignore lint/suspicious/noExplicitAny: legacy diagnostic field tacked onto Error
219
+ (err as any).statusCode = response.statusCode;
220
+ // c8 ignore next
221
+ doTraceHelloAck && warningLog("receiving ERR instead of Ack", response.toString());
222
+
223
+ callback(err);
224
+ } else {
225
+ responseClass = AcknowledgeMessage;
226
+ _stream.rewind();
227
+ response = decodeMessage(_stream, responseClass) as AcknowledgeMessage;
228
+
229
+ this.parameters = response;
230
+ this.setLimits(response);
231
+
232
+ // c8 ignore next
233
+ doTraceHelloAck && warningLog("receiving Ack\n", response.toString());
234
+
235
+ callback();
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module node-opcua-transport
3
+ */
4
+
5
+ import { ClientTCP_transport, type TransportSettingsOptions } from "./client_tcp_transport";
6
+ import type { IClientTransport, IClientTransportFactory } from "./i_client_transport";
7
+
8
+ /**
9
+ * The default client-transport factory, which returns a {@link ClientTCP_transport}.
10
+ *
11
+ * This is the implicit factory used by {@link ClientSecureChannelLayer} when no
12
+ * `transportFactory` option is provided, preserving the historical (Node-only)
13
+ * behavior byte-for-byte.
14
+ */
15
+ export const defaultClientTransportFactory: IClientTransportFactory = {
16
+ create(settings?: TransportSettingsOptions): IClientTransport {
17
+ return new ClientTCP_transport(settings) as IClientTransport;
18
+ }
19
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @module node-opcua-transport
3
+ */
4
+
5
+ import type { AcknowledgeMessage } from "./AcknowledgeMessage";
6
+
7
+ /**
8
+ * Options used to construct a client transport. Passed through {@link IClientTransportFactory.create}
9
+ * and applied to the UACP HEL message during the handshake.
10
+ */
11
+ export interface TransportSettingsOptions {
12
+ maxChunkCount?: number;
13
+ maxMessageSize?: number;
14
+ receiveBufferSize?: number;
15
+ sendBufferSize?: number;
16
+ }
17
+
18
+ /**
19
+ * The minimal surface that {@link ClientSecureChannelLayer} (and anything else acting as
20
+ * a secure-channel client) uses from a transport. {@link ClientTCP_transport} already
21
+ * satisfies this interface; browser transports (e.g. a WebSocket-based one) must also
22
+ * satisfy it to be pluggable via {@link IClientTransportFactory}.
23
+ *
24
+ * This interface is intentionally the smallest superset of what the existing
25
+ * `ClientTCP_transport` exposes and that the secure-channel layer actually consumes,
26
+ * so adding new transports does not require replicating Node-specific machinery.
27
+ */
28
+ export interface IClientTransport {
29
+ // ──────────────────────────────────────────────────────────────
30
+ // mutable configuration / runtime state
31
+ // ──────────────────────────────────────────────────────────────
32
+
33
+ /** diagnostic name, useful in debug logs */
34
+ readonly name: string;
35
+
36
+ /** OPC UA UACP protocol version advertised in HEL */
37
+ protocolVersion: number;
38
+
39
+ /** overall timeout applied to the underlying socket / connection lifecycle */
40
+ timeout: number;
41
+
42
+ /** number of times the owning channel has retried. Advisory; bumped by callers. */
43
+ numberOfRetry: number;
44
+
45
+ /** endpoint URL the transport was connected to (set by `connect`) */
46
+ endpointUrl: string;
47
+
48
+ /** URI reported by the local application to the peer */
49
+ serverUri: string;
50
+
51
+ // ──────────────────────────────────────────────────────────────
52
+ // HEL/ACK negotiated values (populated after successful connect)
53
+ // ──────────────────────────────────────────────────────────────
54
+
55
+ readonly parameters?: AcknowledgeMessage;
56
+
57
+ readonly receiveBufferSize: number;
58
+ readonly sendBufferSize: number;
59
+ readonly maxChunkCount: number;
60
+ readonly maxMessageSize: number;
61
+
62
+ // ──────────────────────────────────────────────────────────────
63
+ // diagnostics
64
+ // ──────────────────────────────────────────────────────────────
65
+
66
+ readonly bytesRead: number;
67
+ readonly bytesWritten: number;
68
+ readonly chunkReadCount: number;
69
+ readonly chunkWrittenCount: number;
70
+
71
+ // ──────────────────────────────────────────────────────────────
72
+ // lifecycle
73
+ // ──────────────────────────────────────────────────────────────
74
+
75
+ /** connect to `endpointUrl` and perform the UACP HEL/ACK handshake */
76
+ connect(endpointUrl: string, callback: (err?: Error | null) => void): void;
77
+
78
+ /** gracefully disconnect; invokes `callback` when the underlying connection is closed */
79
+ disconnect(callback: (err?: Error | null) => void): void;
80
+
81
+ /** forcibly release resources (close the connection if still open) */
82
+ dispose(): void;
83
+
84
+ /** write a single UACP chunk to the transport */
85
+ write(chunk: Buffer, callback?: (err?: Error | null) => undefined): void;
86
+
87
+ /** emit an ERR back to the peer and destroy the underlying connection */
88
+ prematureTerminate(err: Error, statusCode: import("node-opcua-status-code").StatusCode): void;
89
+
90
+ /** simulate a connection break (used by reconnection logic in tests) */
91
+ forceConnectionBreak(): void;
92
+
93
+ /** `true` when the underlying connection is open and usable */
94
+ isValid(): boolean;
95
+
96
+ /** `true` when `disconnect()` has started or the connection is gone */
97
+ isDisconnecting(): boolean;
98
+
99
+ /** return the effective transport settings (`maxChunkCount` etc.) */
100
+ getTransportSettings(): TransportSettingsOptions;
101
+
102
+ // ──────────────────────────────────────────────────────────────
103
+ // typed events (mirrors ClientTCP_transport's declaration merge)
104
+ // ──────────────────────────────────────────────────────────────
105
+
106
+ on(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
107
+ on(eventName: "close", eventHandler: (err: Error | null) => void): this;
108
+ on(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
109
+ on(eventName: "connect", eventHandler: () => void): this;
110
+
111
+ once(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
112
+ once(eventName: "close", eventHandler: (err: Error | null) => void): this;
113
+ once(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
114
+ once(eventName: "connect", eventHandler: () => void): this;
115
+
116
+ removeListener(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
117
+ removeListener(eventName: "close", eventHandler: (err: Error | null) => void): this;
118
+ removeListener(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
119
+ removeListener(eventName: "connect", eventHandler: () => void): this;
120
+ }
121
+
122
+ /**
123
+ * A factory that produces an {@link IClientTransport}. Injected into
124
+ * {@link ClientSecureChannelLayerOptions.transportFactory} to swap the default Node TCP
125
+ * transport for an alternative (for example, a browser WebSocket transport or a tracing
126
+ * proxy wrapped around the default).
127
+ */
128
+ export interface IClientTransportFactory {
129
+ /**
130
+ * Create a new transport. Called once per secure-channel open; the factory must not
131
+ * return the same instance twice.
132
+ */
133
+ create(settings?: TransportSettingsOptions): IClientTransport;
134
+ }