@seriousme/opifex 2.1.0 → 2.3.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.
Files changed (147) hide show
  1. package/README.md +7 -6
  2. package/bin/mqtt.ts +1 -1
  3. package/client/client.ts +58 -7
  4. package/client/context.ts +2 -2
  5. package/client/deps.ts +3 -3
  6. package/client/mod.ts +2 -2
  7. package/connNext/connNext.ts +44 -0
  8. package/deno/README.md +1 -1
  9. package/deno/denoConnToNext.ts +104 -0
  10. package/deno/mod.ts +7 -0
  11. package/deno/tcpClient.ts +8 -24
  12. package/deno/tcpServer.test.ts +14 -103
  13. package/deno/tcpServer.ts +14 -6
  14. package/deno/tlsServer.test.ts +14 -58
  15. package/deno/tlsServer.ts +14 -6
  16. package/deno/wsServer.test.ts +15 -61
  17. package/deno/wsServer.ts +77 -35
  18. package/dev_utils/asyncStreamPair.ts +114 -0
  19. package/dev_utils/asyncStreamToNext.ts +171 -0
  20. package/dev_utils/connNextPair.test.ts +179 -0
  21. package/dev_utils/mockConn.ts +115 -29
  22. package/dev_utils/mockServer.ts +3 -4
  23. package/dev_utils/mod.ts +1 -2
  24. package/dev_utils/testClient.ts +64 -0
  25. package/dev_utils/webStreamToNext.ts +187 -0
  26. package/dist/bin/mqtt.js +1 -1
  27. package/dist/bin/mqtt.js.map +1 -1
  28. package/dist/client/client.d.ts +21 -3
  29. package/dist/client/client.d.ts.map +1 -1
  30. package/dist/client/client.js +43 -5
  31. package/dist/client/client.js.map +1 -1
  32. package/dist/client/context.d.ts +2 -2
  33. package/dist/client/context.d.ts.map +1 -1
  34. package/dist/client/deps.d.ts +1 -1
  35. package/dist/client/deps.d.ts.map +1 -1
  36. package/dist/client/mod.d.ts +1 -1
  37. package/dist/client/mod.d.ts.map +1 -1
  38. package/dist/connNext/connNext.d.ts +43 -0
  39. package/dist/connNext/connNext.d.ts.map +1 -0
  40. package/dist/connNext/connNext.js +2 -0
  41. package/dist/connNext/connNext.js.map +1 -0
  42. package/dist/examples/authHandler.test.js +2 -2
  43. package/dist/examples/authHandler.test.js.map +1 -1
  44. package/dist/examples/mqtt+mqtts.test.d.ts +2 -0
  45. package/dist/examples/mqtt+mqtts.test.d.ts.map +1 -0
  46. package/dist/examples/mqtt+mqtts.test.js +104 -0
  47. package/dist/examples/mqtt+mqtts.test.js.map +1 -0
  48. package/dist/examples/mqtt.test.js +2 -2
  49. package/dist/examples/mqtt.test.js.map +1 -1
  50. package/dist/examples/mqtts.test.js +2 -2
  51. package/dist/examples/mqtts.test.js.map +1 -1
  52. package/dist/mqttConn/mqttConn.d.ts +5 -6
  53. package/dist/mqttConn/mqttConn.d.ts.map +1 -1
  54. package/dist/mqttConn/mqttConn.js +2 -3
  55. package/dist/mqttConn/mqttConn.js.map +1 -1
  56. package/dist/mqttPacket/protocolLevels.d.ts +2 -0
  57. package/dist/mqttPacket/protocolLevels.d.ts.map +1 -1
  58. package/dist/node/nodeSocketToNext.d.ts +20 -0
  59. package/dist/node/nodeSocketToNext.d.ts.map +1 -0
  60. package/dist/node/nodeSocketToNext.js +161 -0
  61. package/dist/node/nodeSocketToNext.js.map +1 -0
  62. package/dist/node/tcpClient.d.ts +4 -6
  63. package/dist/node/tcpClient.d.ts.map +1 -1
  64. package/dist/node/tcpClient.js +6 -21
  65. package/dist/node/tcpClient.js.map +1 -1
  66. package/dist/node/tcpServer.d.ts +8 -4
  67. package/dist/node/tcpServer.d.ts.map +1 -1
  68. package/dist/node/tcpServer.js +14 -6
  69. package/dist/node/tcpServer.js.map +1 -1
  70. package/dist/node/tlsServer.d.ts +7 -3
  71. package/dist/node/tlsServer.d.ts.map +1 -1
  72. package/dist/node/tlsServer.js +13 -5
  73. package/dist/node/tlsServer.js.map +1 -1
  74. package/dist/server/context.d.ts +5 -5
  75. package/dist/server/context.d.ts.map +1 -1
  76. package/dist/server/context.js +4 -1
  77. package/dist/server/context.js.map +1 -1
  78. package/dist/server/deps.d.ts +1 -1
  79. package/dist/server/deps.d.ts.map +1 -1
  80. package/dist/server/mod.d.ts +1 -1
  81. package/dist/server/mod.d.ts.map +1 -1
  82. package/dist/server/server.d.ts +3 -3
  83. package/dist/server/server.d.ts.map +1 -1
  84. package/dist/server/server.js +3 -3
  85. package/dist/utils/utils.d.ts +1 -2
  86. package/dist/utils/utils.d.ts.map +1 -1
  87. package/dist/utils/utils.js +3 -2
  88. package/dist/utils/utils.js.map +1 -1
  89. package/dist/web/webSocketToNext.d.ts +3 -0
  90. package/dist/web/webSocketToNext.d.ts.map +1 -0
  91. package/dist/web/webSocketToNext.js +160 -0
  92. package/dist/web/webSocketToNext.js.map +1 -0
  93. package/dist/web/wsClient.d.ts +8 -3
  94. package/dist/web/wsClient.d.ts.map +1 -1
  95. package/dist/web/wsClient.js +20 -6
  96. package/dist/web/wsClient.js.map +1 -1
  97. package/dist/web/wsClient.test.js +15 -0
  98. package/dist/web/wsClient.test.js.map +1 -1
  99. package/examples/README.md +21 -0
  100. package/examples/authHandler.test.ts +2 -2
  101. package/examples/mqtt+mqtts.test.ts +122 -0
  102. package/examples/mqtt.test.ts +2 -2
  103. package/examples/mqtts.test.ts +2 -2
  104. package/examples/webSocket.test.ts +10 -64
  105. package/examples/webSocketServer.ts +98 -0
  106. package/mqttConn/mqttConn.test.ts +6 -6
  107. package/mqttConn/mqttConn.ts +8 -9
  108. package/mqttPacket/protocolLevels.ts +2 -0
  109. package/node/README.md +1 -1
  110. package/node/mod.ts +7 -0
  111. package/node/{socket-node-stream.ts → nodeSocketToNext.ts} +78 -65
  112. package/node/tcpClient.ts +13 -27
  113. package/node/tcpServer.test.ts +12 -58
  114. package/node/tcpServer.ts +18 -8
  115. package/node/tlsServer.test.ts +11 -58
  116. package/node/tlsServer.ts +17 -7
  117. package/package.json +8 -2
  118. package/server/context.ts +9 -6
  119. package/server/deps.ts +1 -1
  120. package/server/mod.ts +1 -1
  121. package/server/server.ts +6 -6
  122. package/utils/utils.ts +3 -2
  123. package/web/webSocketToNext.ts +183 -0
  124. package/web/wsClient.test.ts +22 -0
  125. package/web/wsClient.ts +22 -9
  126. package/dist/node/wrapNodeSocket.d.ts +0 -12
  127. package/dist/node/wrapNodeSocket.d.ts.map +0 -1
  128. package/dist/node/wrapNodeSocket.js +0 -36
  129. package/dist/node/wrapNodeSocket.js.map +0 -1
  130. package/dist/socket/socket.d.ts +0 -40
  131. package/dist/socket/socket.d.ts.map +0 -1
  132. package/dist/socket/socket.js +0 -94
  133. package/dist/socket/socket.js.map +0 -1
  134. package/dist/web/webSocketStream.d.ts +0 -29
  135. package/dist/web/webSocketStream.d.ts.map +0 -1
  136. package/dist/web/webSocketStream.js +0 -69
  137. package/dist/web/webSocketStream.js.map +0 -1
  138. package/dist/web/wrapWebSocket.d.ts +0 -12
  139. package/dist/web/wrapWebSocket.d.ts.map +0 -1
  140. package/dist/web/wrapWebSocket.js +0 -41
  141. package/dist/web/wrapWebSocket.js.map +0 -1
  142. package/node/socket-node-stream.test.ts +0 -322
  143. package/node/wrapNodeSocket.ts +0 -44
  144. package/socket/socket.test.ts +0 -91
  145. package/socket/socket.ts +0 -124
  146. package/web/webSocketStream.ts +0 -97
  147. package/web/wrapWebSocket.ts +0 -52
@@ -30,7 +30,7 @@ test("Test pubSub using TCP client and server using memoryPersistence", async fu
30
30
 
31
31
  const client = new TcpClient();
32
32
  await client.connect(params);
33
- logger.info("Client connected to server at", client.url);
33
+ logger.info("Client connected to server at", client.url.toString());
34
34
 
35
35
  const publishSet: { topic: string; qos: QoS }[] = [
36
36
  { topic: "t0@q0", qos: 0 },
@@ -85,5 +85,5 @@ test("Test pubSub using TCP client and server using memoryPersistence", async fu
85
85
  }
86
86
 
87
87
  logger.info("Stop server");
88
- server.stop();
88
+ await server.stop();
89
89
  });
@@ -41,7 +41,7 @@ test("Test pubSub using TLS client and server and sqlitePersistence", async func
41
41
 
42
42
  const client = new TcpClient();
43
43
  await client.connect(params);
44
- logger.info("Client connected to server at", client.url);
44
+ logger.info("Client connected to server at", client.url.toString());
45
45
 
46
46
  const publishSet: { topic: string; qos: QoS }[] = [
47
47
  { topic: "t0@q0", qos: 0 },
@@ -96,5 +96,5 @@ test("Test pubSub using TLS client and server and sqlitePersistence", async func
96
96
  }
97
97
 
98
98
  logger.info("Stop server");
99
- server.stop();
99
+ await server.stop();
100
100
  });
@@ -1,8 +1,7 @@
1
1
  import { test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { delay, logger, LogLevel } from "@seriousme/opifex/utils";
4
-
5
- import type { PublishPacket, QoS } from "@seriousme/opifex/mqttPacket";
3
+ import { logger, LogLevel } from "@seriousme/opifex/utils";
4
+ import { runTest } from "./public/testRunner.js";
6
5
 
7
6
  logger.level(LogLevel.info);
8
7
 
@@ -27,68 +26,15 @@ test("Deno: Test pubSub using WebSocket client and server and memoryPersistence"
27
26
  ? "127.0.0.1"
28
27
  : server.address;
29
28
 
30
- const params = {
31
- url: new URL(`ws://${hostname}:${server.port}`),
32
- numberOfRetries: 0,
33
- };
34
-
35
- const client = new WsClient();
36
- await client.connect(params);
37
- logger.info("Client connected to server at", client.url);
38
-
39
- const publishSet: { topic: string; qos: QoS }[] = [
40
- { topic: "t0@q0", qos: 0 },
41
- { topic: "t1@q0", qos: 0 },
42
- { topic: "t2@q0", qos: 0 },
43
- { topic: "t0@q1", qos: 1 },
44
- { topic: "t1@q1", qos: 1 },
45
- { topic: "t2@q1", qos: 1 },
46
- { topic: "t0@q2", qos: 2 },
47
- { topic: "t1@q2", qos: 2 },
48
- { topic: "t2@q2", qos: 2 },
49
- ];
50
-
51
- const subscriptions = publishSet.map((item) => ({
52
- topicFilter: item.topic,
53
- qos: item.qos,
54
- }));
55
- await client.subscribe({
56
- subscriptions,
29
+ await runTest({
30
+ WsClient,
31
+ protocol: "ws",
32
+ hostname,
33
+ port: server.port,
34
+ logger: logger.verbose,
35
+ assert,
57
36
  });
58
-
59
- // the IIFE ensures message reception runs in parallel
60
- logger.info("Start receiving");
61
- const received: PublishPacket[] = [];
62
- (async function () {
63
- for await (const item of client.messages()) {
64
- logger.verbose("Receiving:", item.topic, "--", item.qos);
65
- received.push(item);
66
- }
67
- })();
68
- // end of IIFE
69
- for (const item of publishSet) {
70
- logger.verbose("Publishing:", item.topic, "--", item.qos);
71
- await client.publish({
72
- topic: item.topic,
73
- qos: item.qos,
74
- payload: new Uint8Array([0x01]),
75
- });
76
- }
77
-
78
- await delay(100);
79
- logger.info("Disconnect client");
80
- await client.disconnect();
81
-
82
- logger.info("Check completeness");
83
- for (const item of publishSet) {
84
- const found = received.find((f) =>
85
- f.topic == item.topic && f.qos === item.qos
86
- );
87
- logger.verbose("Found:", item.topic, "--", item.qos);
88
- assert(found, `${item.topic} -- ${item.qos}`);
89
- }
90
-
91
37
  logger.info("Stop server");
92
- server.stop();
38
+ await server.stop();
93
39
  }
94
40
  });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * a simple websocket server to verify browser compatibility
3
+ */
4
+ import { extname, join, resolve } from "node:path";
5
+ import { logger } from "@seriousme/opifex/utils";
6
+ import { MqttServer } from "@seriousme/opifex/server";
7
+ import { mqttOverWS } from "@seriousme/opifex/wsServer";
8
+ import { LogLevel } from "@seriousme/opifex/utils";
9
+ import type { RequestHandler } from "@seriousme/opifex/wsServer";
10
+
11
+ logger.level(LogLevel.info);
12
+
13
+ // Nodejs does not natively support WebSockets so we only continue running Deno
14
+ if (typeof Deno === "undefined") {
15
+ logger.error("Native WebSocket server only supported on Deno");
16
+ process.exit(1);
17
+ }
18
+ const currentDir = import.meta.dirname || "./";
19
+ const PUBLIC_DIR = resolve(join(currentDir, "/public"));
20
+ const listenOptions = { port: 8080 };
21
+ const mqttOptions = {};
22
+ const mqttServer = new MqttServer(mqttOptions);
23
+
24
+ const mimeTypes = new Map<string, string>([
25
+ [".html", "text/html; charset=utf-8"],
26
+ [".css", "text/css; charset=utf-8"],
27
+ [".js", "text/javascript; charset=utf-8"],
28
+ ]);
29
+
30
+ function w3cLogger(handler: RequestHandler): RequestHandler {
31
+ return async (req, info) => {
32
+ const res = await handler(req, info);
33
+ const hostname = res.headers.get("upgrade")
34
+ ? "unknown"
35
+ : info.remoteAddr.hostname;
36
+ logger.info(W3CLogLine(req, res, hostname));
37
+ return res;
38
+ };
39
+ }
40
+
41
+ function W3CLogLine(req: Request, res: Response, hostname: string): string {
42
+ const now = new Date();
43
+
44
+ // format date to UTC (YYYY-MM-DD en HH:MM:SS)
45
+ const date = now.toISOString().split("T")[0];
46
+ const time = now.toISOString().split("T")[1]?.substring(0, 8);
47
+
48
+ const url = new URL(req.url);
49
+ const clientIp = hostname ?? "127.0.0.1";
50
+ const method = req.method;
51
+ const uriStem = url.pathname;
52
+ const uriQuery = url.search ? url.search : "-";
53
+ const status = res.status;
54
+ const bytesSent = res.headers.get("content-length") ?? "-";
55
+
56
+ return `${date} ${time} ${clientIp} ${method} ${uriStem} ${uriQuery} ${status} ${bytesSent}`;
57
+ }
58
+
59
+ const requestHandler: RequestHandler = async (req) => {
60
+ const url = new URL(req.url);
61
+
62
+ const pathname = url.pathname.endsWith("/")
63
+ ? url.pathname + "index.html"
64
+ : url.pathname;
65
+
66
+ const ext = extname(pathname);
67
+ const mimeType = mimeTypes.get(ext) ?? "application/octet-stream";
68
+
69
+ const targetPath = resolve(join(PUBLIC_DIR, pathname));
70
+ logger.debug({ targetPath });
71
+
72
+ try {
73
+ if (!targetPath.startsWith(PUBLIC_DIR)) {
74
+ // someone is trying to escape the public folder
75
+ return new Response("403 Forbidden", { status: 403 });
76
+ }
77
+
78
+ const file = await Deno.readFile(targetPath);
79
+ return new Response(file, {
80
+ status: 200,
81
+ headers: {
82
+ "content-type": mimeType,
83
+ },
84
+ });
85
+ } catch (err) {
86
+ if (err instanceof Deno.errors.NotFound) {
87
+ return new Response("404 Not Found", { status: 404 });
88
+ }
89
+ return new Response("500 Internal Server Error", { status: 500 });
90
+ }
91
+ };
92
+
93
+ const server = Deno.serve(
94
+ listenOptions,
95
+ w3cLogger(mqttOverWS(mqttServer, "/mqtt", requestHandler)),
96
+ );
97
+
98
+ await server.finished;
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
- import { createMockSockConn } from "../dev_utils/mod.ts";
3
+ import { createMockConn } from "../dev_utils/mod.ts";
4
4
  import { encode, MQTTLevel, PacketType } from "../mqttPacket/mod.ts";
5
5
  import { MqttConn, MqttConnError } from "./mqttConn.ts";
6
6
  import type { AnyPacket, CodecOpts } from "../mqttPacket/mod.ts";
@@ -50,7 +50,7 @@ test("MqttConn should act as asyncIterator", async () => {
50
50
  const publish = encode(publishPacket, codecOptsV4);
51
51
  const disconnect = encode(disconnectPacket, codecOptsV4);
52
52
 
53
- const conn = createMockSockConn(
53
+ const conn = createMockConn(
54
54
  [connect, publish, disconnect],
55
55
  new Uint8Array(),
56
56
  );
@@ -68,7 +68,7 @@ test("MqttConn should act as asyncIterator", async () => {
68
68
  });
69
69
 
70
70
  test("MqttConn should close on malformed length", async () => {
71
- const conn = createMockSockConn([new Uint8Array([1, 175])], new Uint8Array());
71
+ const conn = createMockConn([new Uint8Array([1, 175])], new Uint8Array());
72
72
  const mqttConn = new MqttConn({ conn });
73
73
 
74
74
  const packets: AnyPacket[] = [];
@@ -86,7 +86,7 @@ test("MqttConn should close on failed packets", async () => {
86
86
  const publish = encode(publishPacket, codecOptsV4);
87
87
  const brokenPublish = publish.slice(0, 7);
88
88
 
89
- const conn = createMockSockConn([connect, brokenPublish], new Uint8Array());
89
+ const conn = createMockConn([connect, brokenPublish], new Uint8Array());
90
90
  const mqttConn = new MqttConn({ conn });
91
91
 
92
92
  const packets: AnyPacket[] = [];
@@ -103,7 +103,7 @@ test("MqttConn should close on failed packets", async () => {
103
103
  test("MqttConn should close on packets too large", async () => {
104
104
  const connect = encode(connectPacket, codecOptsV4);
105
105
 
106
- const conn = createMockSockConn([connect], new Uint8Array());
106
+ const conn = createMockConn([connect], new Uint8Array());
107
107
  const mqttConn = new MqttConn({ conn, maxIncomingPacketSize: 20 });
108
108
 
109
109
  const packets: AnyPacket[] = [];
@@ -119,7 +119,7 @@ test("MqttConn should close on packets too large", async () => {
119
119
  test("MqttConn should be writable", async () => {
120
120
  const connect = encode(connectPacket, codecOptsV4);
121
121
  const writer = new Uint8Array(24);
122
- const conn = createMockSockConn([connect], writer);
122
+ const conn = createMockConn([connect], writer);
123
123
  const mqttConn = new MqttConn({ conn });
124
124
  await mqttConn.send(connectPacket);
125
125
  assert.deepStrictEqual(writer, connect);
@@ -16,8 +16,7 @@ import {
16
16
  } from "../mqttPacket/mod.ts";
17
17
 
18
18
  import { assert } from "../utils/mod.ts";
19
- import type { SockConn } from "../socket/socket.ts";
20
- import { Conn } from "../socket/socket.ts";
19
+ import type { ConnNext } from "../connNext/connNext.ts";
21
20
 
22
21
  /**
23
22
  * Common MQTT connection error messages
@@ -45,7 +44,7 @@ export type SendResult =
45
44
  * @returns Single byte as number
46
45
  * @throws Error if EOF reached unexpectedly
47
46
  */
48
- async function readByte(conn: Conn): Promise<number> {
47
+ async function readByte(conn: ConnNext): Promise<number> {
49
48
  const buff = await conn.read(1);
50
49
  assert(buff !== null, MqttConnError.UnexpectedEof);
51
50
  assert(buff !== undefined, MqttConnError.UnexpectedEof);
@@ -59,7 +58,7 @@ async function readByte(conn: Conn): Promise<number> {
59
58
  * @param buf Buffer to read into
60
59
  * @throws Error if EOF reached unexpectedly
61
60
  */
62
- async function readFull(conn: Conn, length: number): Promise<Uint8Array> {
61
+ async function readFull(conn: ConnNext, length: number): Promise<Uint8Array> {
63
62
  const buff = await conn.read(length);
64
63
  assert(buff !== null, MqttConnError.UnexpectedEof);
65
64
  assert(buff.byteLength === length, MqttConnError.UnexpectedEof);
@@ -74,7 +73,7 @@ async function readFull(conn: Conn, length: number): Promise<Uint8Array> {
74
73
  * @throws Error if packet is invalid or too large
75
74
  */
76
75
  export async function readPacket(
77
- conn: Conn,
76
+ conn: ConnNext,
78
77
  codecOpts: CodecOpts,
79
78
  maxConnectPacketSize: number,
80
79
  ): Promise<AnyPacket> {
@@ -113,7 +112,7 @@ export async function readPacket(
113
112
  */
114
113
  export class MqttConn {
115
114
  /** Underlying connection */
116
- readonly conn: Conn;
115
+ readonly conn: ConnNext;
117
116
  /** Maximum Connect packet size */
118
117
  readonly maxConnectPacketSize: number;
119
118
  /** codecOpts */
@@ -134,10 +133,10 @@ export class MqttConn {
134
133
  */
135
134
  constructor(
136
135
  options:
137
- & { conn: SockConn; maxConnectPacketSize?: number }
136
+ & { conn: ConnNext; maxConnectPacketSize?: number }
138
137
  & Partial<CodecOpts>,
139
138
  ) {
140
- this.conn = new Conn(options.conn);
139
+ this.conn = options.conn;
141
140
  this.maxConnectPacketSize = options.maxConnectPacketSize ??
142
141
  DEFAULT_MAX_PACKETSIZE;
143
142
  this.codecOpts = {
@@ -154,7 +153,7 @@ export class MqttConn {
154
153
  return this._reason;
155
154
  }
156
155
 
157
- /** Get remoteAdress */
156
+ /** Get remoteAddress */
158
157
  get remoteAddress(): string {
159
158
  if (this.conn.remoteAddr?.transport === "tcp") {
160
159
  return this.conn.remoteAddr.hostname;
@@ -14,5 +14,7 @@ export const MQTTLevel = {
14
14
  v5: 5,
15
15
  } as const;
16
16
 
17
+ /** the MQTT protocol level: 3, 4, 5 or undefined */
17
18
  export type ProtocolLevel = (typeof MQTTLevel)[keyof typeof MQTTLevel];
19
+ /** all MQTT protocol levels except 5 */
18
20
  export type ProtocolLevelNoV5 = Exclude<ProtocolLevel, 5>;
package/node/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Running Opifex server/client on NodeJS
2
2
 
3
- These instructions specifify how to run [Opifex](README.md) on NodeJS. To run
3
+ These instructions specifify how to run [Opifex](../README.md) on NodeJS. To run
4
4
  Opifex on Deno see the [instructions](../deno/README.md)
5
5
 
6
6
  ## Playing around
package/node/mod.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Opifex default exports for NodeJS
3
+ * @module
4
+ */
5
+
6
+ export * from "../client/mod.ts";
7
+ export * from "../server/mod.ts";
@@ -1,59 +1,42 @@
1
- import type { Duplex } from "node:stream";
1
+ import type { Socket } from "node:net";
2
+ import type { ConnAddr, ConnNext } from "../connNext/connNext.ts"; // Custom types
2
3
 
3
- export type SockAddr = NetAddr | UnixAddr | VsockAddr;
4
- export interface NetAddr {
5
- transport: "tcp" | "udp";
6
- hostname: string;
7
- port: number;
8
- }
9
- export interface UnixAddr {
10
- transport: "unix" | "unixpacket";
11
- path: string;
12
- }
13
- export interface VsockAddr {
14
- transport: "vsock";
15
- cid: number;
16
- port: number;
17
- }
18
-
19
- export type SockConn = {
20
- stream: Duplex;
21
- close: () => void;
22
- localAddr?: SockAddr;
23
- remoteAddr?: SockAddr;
24
- };
25
-
26
- export class Conn {
27
- private stream: Duplex;
4
+ /** wrapper class */
5
+ export class NodeSocketConnNextWrapper {
6
+ private endPoint: Socket;
28
7
  private closed: boolean;
29
- private closer: () => void;
30
- public remoteAddr: SockAddr | undefined;
31
8
  private leftover: Uint8Array = new Uint8Array(0);
32
9
 
33
10
  // Store bound event listeners so we can cleanly remove them later
34
- private onStreamClose = () => {
11
+ private onClose = () => {
35
12
  this.closed = true;
36
13
  };
37
- private onStreamError = () => {
14
+ private onError = () => {
38
15
  this.closed = true;
39
16
  };
40
17
 
41
- constructor(sockConn: SockConn) {
42
- this.stream = sockConn.stream;
18
+ /** Create a new ConnNext object from a Webstream */
19
+ constructor(endPoint: Socket) {
20
+ this.endPoint = endPoint;
43
21
  this.closed = false;
44
- this.closer = sockConn.close;
45
- this.remoteAddr = sockConn.remoteAddr;
46
-
47
- // Attach listeners to update internal state on stream termination
48
- this.stream.on("close", this.onStreamClose);
49
- this.stream.on("error", this.onStreamError);
22
+ this.endPoint;
50
23
  }
51
24
 
52
- async read(length: number): Promise<Uint8Array | null> {
53
- if (this.closed || length < 0) {
25
+ async read(length?: number): Promise<Uint8Array | null> {
26
+ if (this.closed || (length ?? 0) < 0) {
54
27
  return null;
55
28
  }
56
29
 
30
+ // If no specific byte length is requested, read whatever is available
31
+ if (length === undefined) {
32
+ const chunk = this.endPoint.read();
33
+ return chunk
34
+ ? Promise.resolve(
35
+ new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
36
+ )
37
+ : Promise.resolve(null);
38
+ }
39
+
57
40
  if (length === 0) {
58
41
  return new Uint8Array(0);
59
42
  }
@@ -71,28 +54,28 @@ export class Conn {
71
54
 
72
55
  // 2. Loop until the requested number of bytes is accumulated
73
56
  while (bytesAccumulated < length) {
74
- if (this.closed || !this.stream.readable) {
57
+ if (this.closed || !this.endPoint.readable) {
75
58
  break;
76
59
  }
77
60
 
78
- const chunk = this.stream.read();
61
+ const chunk = this.endPoint.read();
79
62
  if (chunk === null) {
80
63
  await new Promise<void>((resolve) => {
81
64
  const cleanup = () => {
82
- this.stream.off("readable", onEvent);
83
- this.stream.off("end", onEvent);
84
- this.stream.off("error", onEvent);
85
- this.stream.off("close", onEvent);
65
+ this.endPoint.off("readable", onEvent);
66
+ this.endPoint.off("end", onEvent);
67
+ this.endPoint.off("error", onEvent);
68
+ this.endPoint.off("close", onEvent);
86
69
  };
87
70
  const onEvent = () => {
88
71
  cleanup();
89
72
  resolve();
90
73
  };
91
74
 
92
- this.stream.on("readable", onEvent);
93
- this.stream.on("end", onEvent);
94
- this.stream.on("error", onEvent);
95
- this.stream.on("close", onEvent);
75
+ this.endPoint.on("readable", onEvent);
76
+ this.endPoint.on("end", onEvent);
77
+ this.endPoint.on("error", onEvent);
78
+ this.endPoint.on("close", onEvent);
96
79
  });
97
80
 
98
81
  continue;
@@ -119,14 +102,14 @@ export class Conn {
119
102
  return result;
120
103
  }
121
104
 
122
- write(data: Uint8Array): Promise<number> {
123
- if (this.closed || !this.stream.writable) {
105
+ write(data: Uint8Array): Promise<void> {
106
+ if (this.closed || !this.endPoint.writable) {
124
107
  return Promise.reject(new Error("Stream is closed or not writable"));
125
108
  }
126
109
 
127
110
  return new Promise((resolve, reject) => {
128
111
  // 1. Fire the write command and catch any immediate/async errors via the callback
129
- const canWriteMore = this.stream.write(data, (err) => {
112
+ const canWriteMore = this.endPoint.write(data, (err) => {
130
113
  if (err) {
131
114
  reject(err);
132
115
  }
@@ -135,11 +118,11 @@ export class Conn {
135
118
  // 2. Determine when to resolve based on the return value of write()
136
119
  if (canWriteMore) {
137
120
  // If the buffer is not full, we can resolve immediately
138
- resolve(data.length);
121
+ resolve();
139
122
  } else {
140
123
  // If backpressure is triggered, we must wait for the 'drain' event
141
- this.stream.once("drain", () => {
142
- resolve(data.length);
124
+ this.endPoint.once("drain", () => {
125
+ resolve();
143
126
  });
144
127
  }
145
128
  });
@@ -150,25 +133,55 @@ export class Conn {
150
133
  this.closed = true;
151
134
 
152
135
  // Clean up permanent event listeners to prevent memory leaks
153
- this.stream.off("close", this.onStreamClose);
154
- this.stream.off("error", this.onStreamError);
136
+ this.endPoint.off("close", this.onClose);
137
+ this.endPoint.off("error", this.onError);
155
138
 
156
139
  // Safely destroy the stream first, then trigger the closer callback
157
140
  try {
158
- this.stream.destroy();
141
+ this.endPoint.destroy();
159
142
  // deno-coverage-ignore
160
143
  } catch (_) {
161
144
  // Suppress stream destruction errors
162
145
  // deno-coverage-ignore
163
146
  }
147
+ }
148
+ }
164
149
 
165
- try {
166
- this.closer();
167
- // deno-coverage-ignore
168
- } catch (_) {
169
- // Prevent closer failures from crashing the thread
170
- // deno-coverage-ignore
150
+ async *[Symbol.asyncIterator]() {
151
+ while (true) {
152
+ const chunk = await this.read();
153
+ if (chunk === null) {
154
+ break;
171
155
  }
156
+ yield chunk;
157
+ }
158
+ }
159
+
160
+ // Convert addresses to custom ConnAddr format
161
+ get localAddr(): ConnAddr | undefined {
162
+ if (this.endPoint.localAddress) {
163
+ return {
164
+ transport: "tcp",
165
+ hostname: this.endPoint.localAddress,
166
+ port: this.endPoint.localPort ?? 0,
167
+ };
168
+ }
169
+ return undefined;
170
+ }
171
+
172
+ get remoteAddr(): ConnAddr | undefined {
173
+ if (this.endPoint.remoteAddress) {
174
+ return {
175
+ transport: "tcp",
176
+ hostname: this.endPoint.remoteAddress,
177
+ port: this.endPoint.remotePort ?? 0,
178
+ };
172
179
  }
180
+ return undefined;
173
181
  }
174
182
  }
183
+
184
+ export function nodeSocketToNext(socket: Socket): ConnNext {
185
+ const connNext = new NodeSocketConnNextWrapper(socket);
186
+ return connNext;
187
+ }
package/node/tcpClient.ts CHANGED
@@ -7,9 +7,9 @@ import { Client } from "../client/client.ts";
7
7
  import { logger } from "../client/deps.ts";
8
8
  import { readFile } from "node:fs/promises";
9
9
  import { createConnection } from "node:net";
10
- import * as tls from "node:tls";
11
- import type { SockConn } from "../socket/socket.ts";
12
- import { wrapNodeSocket } from "./wrapNodeSocket.ts";
10
+ import { connect } from "node:tls";
11
+ import type { ConnNext } from "../client/mod.ts";
12
+ import { nodeSocketToNext } from "./nodeSocketToNext.ts";
13
13
 
14
14
  /**
15
15
  * Fetches data from a file and returns it as a string
@@ -44,14 +44,17 @@ export class TcpClient extends Client {
44
44
  * @param hostname the name of the host to connect to
45
45
  * @param port the port number
46
46
  */
47
- protected connectMQTT(hostname: string, port = 1883): Promise<SockConn> {
47
+ protected override connectMQTT(
48
+ hostname: string,
49
+ port = 1883,
50
+ ): Promise<ConnNext> {
48
51
  logger.debug({ hostname, port });
49
52
  return new Promise((resolve, reject) => {
50
53
  const socket = createConnection(
51
54
  { port, host: hostname },
52
55
  () => {
53
56
  logger.debug("Connected to server");
54
- resolve(wrapNodeSocket(socket));
57
+ resolve(nodeSocketToNext(socket));
55
58
  },
56
59
  );
57
60
  socket.once("error", (err) => {
@@ -68,15 +71,15 @@ export class TcpClient extends Client {
68
71
  * @param caCerts the CA certificates to trust
69
72
  * @param cert the clients certificate
70
73
  * @param key the clients private key
71
- * @returns a SockConn object
74
+ * @returns a ConnNext object
72
75
  */
73
- protected connectMQTTS(
76
+ protected override connectMQTTS(
74
77
  hostname: string,
75
78
  port = 8883,
76
79
  ca?: string[],
77
80
  cert?: string,
78
81
  key?: string,
79
- ): Promise<SockConn> {
82
+ ): Promise<ConnNext> {
80
83
  const opts = {
81
84
  host: hostname,
82
85
  port,
@@ -87,9 +90,9 @@ export class TcpClient extends Client {
87
90
  };
88
91
  logger.debug({ hostname, port, ca, cert });
89
92
  return new Promise((resolve, reject) => {
90
- const socket = tls.connect(opts, () => {
93
+ const socket = connect(opts, () => {
91
94
  logger.debug("Connected to server");
92
- resolve(wrapNodeSocket(socket));
95
+ resolve(nodeSocketToNext(socket));
93
96
  });
94
97
  socket.once("error", (err) => {
95
98
  logger.debug("Connection failed", err);
@@ -97,21 +100,4 @@ export class TcpClient extends Client {
97
100
  });
98
101
  });
99
102
  }
100
-
101
- /** createConn is used by the base client class */
102
- protected override createConn(): Promise<SockConn> {
103
- const { protocol, hostname, port: portStr } = this.connectUrl;
104
- const port = Number(portStr);
105
- const caCerts = this.caCerts;
106
- const cert = this.cert;
107
- const key = this.key;
108
-
109
- if (protocol === "mqtts:") {
110
- return this.connectMQTTS(hostname, port, caCerts, cert, key);
111
- }
112
- if (protocol === "mqtt:") {
113
- return this.connectMQTT(hostname, port);
114
- }
115
- throw new Error(`Unsupported protocol: ${protocol}`);
116
- }
117
103
  }