@seriousme/opifex 2.2.0 → 2.4.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 (154) hide show
  1. package/README.md +7 -6
  2. package/bin/mqtt.ts +1 -1
  3. package/client/client.ts +62 -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/baseConnNext.ts +123 -0
  8. package/connNext/connNext.ts +44 -0
  9. package/deno/README.md +1 -1
  10. package/deno/denoConnToNext.ts +104 -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 +82 -32
  18. package/dev_utils/WebStreamEndpoint.ts +5 -0
  19. package/dev_utils/asyncStreamEndpoint.ts +5 -0
  20. package/dev_utils/asyncStreamPair.ts +109 -0
  21. package/dev_utils/asyncStreamPairToNext.ts +45 -0
  22. package/dev_utils/asyncStreamToNext.ts +64 -0
  23. package/dev_utils/connNextPair.test.ts +179 -0
  24. package/dev_utils/mockConn.ts +59 -29
  25. package/dev_utils/mockServer.ts +3 -4
  26. package/dev_utils/mod.ts +1 -2
  27. package/dev_utils/testClient.ts +64 -0
  28. package/dev_utils/webStreamPair.ts +3 -7
  29. package/dev_utils/webStreamPairToNext.ts +48 -0
  30. package/dev_utils/webStreamToNext.ts +54 -0
  31. package/dist/bin/mqtt.js +1 -1
  32. package/dist/bin/mqtt.js.map +1 -1
  33. package/dist/client/client.d.ts +24 -3
  34. package/dist/client/client.d.ts.map +1 -1
  35. package/dist/client/client.js +45 -5
  36. package/dist/client/client.js.map +1 -1
  37. package/dist/client/context.d.ts +2 -2
  38. package/dist/client/context.d.ts.map +1 -1
  39. package/dist/client/deps.d.ts +1 -1
  40. package/dist/client/deps.d.ts.map +1 -1
  41. package/dist/client/mod.d.ts +1 -1
  42. package/dist/client/mod.d.ts.map +1 -1
  43. package/dist/connNext/baseConnNext.d.ts +20 -0
  44. package/dist/connNext/baseConnNext.d.ts.map +1 -0
  45. package/dist/connNext/baseConnNext.js +95 -0
  46. package/dist/connNext/baseConnNext.js.map +1 -0
  47. package/dist/connNext/connNext.d.ts +43 -0
  48. package/dist/connNext/connNext.d.ts.map +1 -0
  49. package/dist/connNext/connNext.js +2 -0
  50. package/dist/connNext/connNext.js.map +1 -0
  51. package/dist/examples/authHandler.test.js +2 -2
  52. package/dist/examples/authHandler.test.js.map +1 -1
  53. package/dist/examples/mqtt+mqtts.test.d.ts +2 -0
  54. package/dist/examples/mqtt+mqtts.test.d.ts.map +1 -0
  55. package/dist/examples/mqtt+mqtts.test.js +104 -0
  56. package/dist/examples/mqtt+mqtts.test.js.map +1 -0
  57. package/dist/examples/mqtt.test.js +2 -2
  58. package/dist/examples/mqtt.test.js.map +1 -1
  59. package/dist/examples/mqtts.test.js +2 -2
  60. package/dist/examples/mqtts.test.js.map +1 -1
  61. package/dist/mqttConn/mqttConn.d.ts +5 -6
  62. package/dist/mqttConn/mqttConn.d.ts.map +1 -1
  63. package/dist/mqttConn/mqttConn.js +2 -3
  64. package/dist/mqttConn/mqttConn.js.map +1 -1
  65. package/dist/node/nodeSocketToNext.d.ts +17 -0
  66. package/dist/node/nodeSocketToNext.d.ts.map +1 -0
  67. package/dist/node/nodeSocketToNext.js +105 -0
  68. package/dist/node/nodeSocketToNext.js.map +1 -0
  69. package/dist/node/tcpClient.d.ts +4 -6
  70. package/dist/node/tcpClient.d.ts.map +1 -1
  71. package/dist/node/tcpClient.js +6 -21
  72. package/dist/node/tcpClient.js.map +1 -1
  73. package/dist/node/tcpServer.d.ts +8 -4
  74. package/dist/node/tcpServer.d.ts.map +1 -1
  75. package/dist/node/tcpServer.js +14 -6
  76. package/dist/node/tcpServer.js.map +1 -1
  77. package/dist/node/tlsServer.d.ts +7 -3
  78. package/dist/node/tlsServer.d.ts.map +1 -1
  79. package/dist/node/tlsServer.js +13 -5
  80. package/dist/node/tlsServer.js.map +1 -1
  81. package/dist/server/context.d.ts +5 -5
  82. package/dist/server/context.d.ts.map +1 -1
  83. package/dist/server/context.js +4 -1
  84. package/dist/server/context.js.map +1 -1
  85. package/dist/server/deps.d.ts +1 -1
  86. package/dist/server/deps.d.ts.map +1 -1
  87. package/dist/server/mod.d.ts +1 -1
  88. package/dist/server/mod.d.ts.map +1 -1
  89. package/dist/server/server.d.ts +3 -3
  90. package/dist/server/server.d.ts.map +1 -1
  91. package/dist/server/server.js +3 -3
  92. package/dist/utils/utils.d.ts +1 -2
  93. package/dist/utils/utils.d.ts.map +1 -1
  94. package/dist/utils/utils.js +3 -2
  95. package/dist/utils/utils.js.map +1 -1
  96. package/dist/web/webSocketToNext.d.ts +20 -0
  97. package/dist/web/webSocketToNext.d.ts.map +1 -0
  98. package/dist/web/webSocketToNext.js +125 -0
  99. package/dist/web/webSocketToNext.js.map +1 -0
  100. package/dist/web/wsClient.d.ts +3 -3
  101. package/dist/web/wsClient.d.ts.map +1 -1
  102. package/dist/web/wsClient.js +22 -7
  103. package/dist/web/wsClient.js.map +1 -1
  104. package/dist/web/wsClient.test.js +15 -0
  105. package/dist/web/wsClient.test.js.map +1 -1
  106. package/examples/README.md +21 -0
  107. package/examples/authHandler.test.ts +2 -2
  108. package/examples/mqtt+mqtts.test.ts +122 -0
  109. package/examples/mqtt.test.ts +2 -2
  110. package/examples/mqtts.test.ts +2 -2
  111. package/examples/webSocket.test.ts +10 -64
  112. package/examples/webSocketServer.ts +98 -0
  113. package/mqttConn/mqttConn.test.ts +6 -6
  114. package/mqttConn/mqttConn.ts +8 -9
  115. package/node/README.md +1 -1
  116. package/node/nodeSocketToNext.ts +124 -0
  117. package/node/tcpClient.ts +13 -27
  118. package/node/tcpServer.test.ts +12 -58
  119. package/node/tcpServer.ts +18 -8
  120. package/node/tlsServer.test.ts +11 -58
  121. package/node/tlsServer.ts +17 -7
  122. package/package.json +4 -3
  123. package/server/context.ts +9 -6
  124. package/server/deps.ts +1 -1
  125. package/server/mod.ts +1 -1
  126. package/server/server.ts +6 -6
  127. package/utils/utils.ts +3 -2
  128. package/web/webSocketToNext.ts +146 -0
  129. package/web/wsClient.test.ts +22 -0
  130. package/web/wsClient.ts +29 -9
  131. package/dev_utils/webStreamPair.test.ts +0 -171
  132. package/dist/node/wrapNodeSocket.d.ts +0 -12
  133. package/dist/node/wrapNodeSocket.d.ts.map +0 -1
  134. package/dist/node/wrapNodeSocket.js +0 -36
  135. package/dist/node/wrapNodeSocket.js.map +0 -1
  136. package/dist/socket/socket.d.ts +0 -58
  137. package/dist/socket/socket.d.ts.map +0 -1
  138. package/dist/socket/socket.js +0 -99
  139. package/dist/socket/socket.js.map +0 -1
  140. package/dist/web/webSocketStream.d.ts +0 -29
  141. package/dist/web/webSocketStream.d.ts.map +0 -1
  142. package/dist/web/webSocketStream.js +0 -69
  143. package/dist/web/webSocketStream.js.map +0 -1
  144. package/dist/web/wrapWebSocket.d.ts +0 -12
  145. package/dist/web/wrapWebSocket.d.ts.map +0 -1
  146. package/dist/web/wrapWebSocket.js +0 -41
  147. package/dist/web/wrapWebSocket.js.map +0 -1
  148. package/node/socket-node-stream.test.ts +0 -322
  149. package/node/socket-node-stream.ts +0 -174
  150. package/node/wrapNodeSocket.ts +0 -44
  151. package/socket/socket.test.ts +0 -91
  152. package/socket/socket.ts +0 -149
  153. package/web/webSocketStream.ts +0 -97
  154. package/web/wrapWebSocket.ts +0 -52
@@ -4,7 +4,9 @@ import { TcpClient } from "./tcpClient.ts";
4
4
  import { TcpServer } from "./tcpServer.ts";
5
5
  import { logger, LogLevel } from "../utils/mod.ts";
6
6
  import { delay } from "../dev_utils/mod.ts";
7
- import type { ProtocolLevel, PublishPacket, QoS } from "../mqttPacket/mod.ts";
7
+ import type { ProtocolLevel, PublishPacket } from "../mqttPacket/mod.ts";
8
+ import { testClient } from "../dev_utils/testClient.ts";
9
+ import { MqttServer } from "../server/mod.ts";
8
10
 
9
11
  logger.level(LogLevel.info);
10
12
 
@@ -25,64 +27,10 @@ test("Test pubSub using client and server", async () => {
25
27
  };
26
28
 
27
29
  const client = new TcpClient();
28
-
29
- await client.connect(params);
30
- assert(true, "Client connected to server");
31
-
32
- const publishSet: { topic: string; qos: QoS }[] = [
33
- { topic: "t0@q0", qos: 0 },
34
- { topic: "t1@q0", qos: 0 },
35
- { topic: "t2@q0", qos: 0 },
36
- { topic: "t0@q1", qos: 1 },
37
- { topic: "t1@q1", qos: 1 },
38
- { topic: "t2@q1", qos: 1 },
39
- { topic: "t0@q2", qos: 2 },
40
- { topic: "t1@q2", qos: 2 },
41
- { topic: "t2@q2", qos: 2 },
42
- ];
43
-
44
- const subscriptions = publishSet.map((item) => ({
45
- topicFilter: item.topic,
46
- qos: item.qos,
47
- }));
48
- await client.subscribe({
49
- subscriptions,
50
- });
51
-
52
- // the IIFE ensures message reception runs in parallel
53
- logger.verbose("Start receiving");
54
- const received: PublishPacket[] = [];
55
- (async () => {
56
- for await (const item of client.messages()) {
57
- logger.verbose("Receiving:", item.topic, "--", item.qos);
58
- received.push(item);
59
- }
60
- })();
61
- // end of IIFE
62
- for (const item of publishSet) {
63
- logger.verbose("Publishing:", item.topic, "--", item.qos);
64
- await client.publish({
65
- topic: item.topic,
66
- qos: item.qos,
67
- payload: new Uint8Array([0x01]),
68
- });
69
- }
70
-
71
- await delay(100);
72
- logger.verbose("Disconnect client");
73
- await client.disconnect();
74
-
75
- logger.verbose("Check completeness");
76
- for (const item of publishSet) {
77
- const found = received.find((f) =>
78
- f.topic == item.topic && f.qos === item.qos
79
- );
80
- logger.verbose("Found:", item.topic, "--", item.qos);
81
- assert(found, `${item.topic} -- ${item.qos}`);
82
- }
30
+ await testClient(client, params);
83
31
 
84
32
  logger.verbose("Stop server");
85
- server.stop();
33
+ await server.stop();
86
34
  });
87
35
 
88
36
  test("Test subscription persistence after reconnect", async () => {
@@ -148,5 +96,11 @@ test("Test subscription persistence after reconnect", async () => {
148
96
  "Should receive message on subscribed topic",
149
97
  );
150
98
 
151
- server.stop();
99
+ await server.stop();
100
+ });
101
+
102
+ test("Passing a mqttServer works", () => {
103
+ const mqttServer = new MqttServer({});
104
+ const server = new TcpServer({ port: 0 }, mqttServer);
105
+ assert(server.mqttServer === mqttServer);
152
106
  });
package/node/tcpServer.ts CHANGED
@@ -7,7 +7,7 @@ import type { Server } from "node:net";
7
7
  import { createServer } from "node:net";
8
8
  import type { MqttServerOptions } from "../server/mod.ts";
9
9
  import { MqttServer } from "../server/mod.ts";
10
- import { wrapNodeSocket } from "./wrapNodeSocket.ts";
10
+ import { nodeSocketToNext } from "./nodeSocketToNext.ts";
11
11
 
12
12
  type ServerOptions = {
13
13
  hostname?: string;
@@ -18,16 +18,24 @@ type ServerOptions = {
18
18
  * TCP server that wraps a MqttServer, see the /examples folder
19
19
  */
20
20
  export class TcpServer {
21
- private mqttServer: MqttServer;
21
+ /** the MqttServer instance used by the server */
22
+ readonly mqttServer: MqttServer;
22
23
  private server?: Server;
23
24
  private serverOptions;
24
25
 
25
26
  /**
26
27
  * Create a new TCP server
27
28
  */
28
- constructor(serverOptions: ServerOptions, mqttOptions: MqttServerOptions) {
29
- this.mqttServer = new MqttServer(mqttOptions);
29
+ constructor(
30
+ serverOptions: ServerOptions,
31
+ mqttOptions: MqttServerOptions | MqttServer,
32
+ ) {
30
33
  this.serverOptions = serverOptions;
34
+ if (mqttOptions instanceof MqttServer) {
35
+ this.mqttServer = mqttOptions;
36
+ } else {
37
+ this.mqttServer = new MqttServer(mqttOptions);
38
+ }
31
39
  }
32
40
 
33
41
  /**
@@ -35,7 +43,7 @@ export class TcpServer {
35
43
  */
36
44
  async start(): Promise<void> {
37
45
  this.server = createServer((sock) =>
38
- this.mqttServer.serve(wrapNodeSocket(sock))
46
+ this.mqttServer.serve(nodeSocketToNext(sock))
39
47
  );
40
48
  const isListening = new Promise((resolve) => {
41
49
  this.server?.on("listening", () => {
@@ -48,10 +56,12 @@ export class TcpServer {
48
56
  }
49
57
 
50
58
  /**
51
- * Stop listening
59
+ * Stop listening, pass {closeMqtt:false} if you do not want the mqttServer to close
52
60
  */
53
- stop(): void {
54
- this.mqttServer.close();
61
+ async stop(opts = { closeMqtt: true }): Promise<void> {
62
+ if (opts.closeMqtt) {
63
+ await this.mqttServer.close();
64
+ }
55
65
  this.server?.close();
56
66
  }
57
67
 
@@ -3,8 +3,9 @@ import assert from "node:assert/strict";
3
3
  import { TcpClient } from "./tcpClient.ts";
4
4
  import { TlsServer } from "./tlsServer.ts";
5
5
  import { logger, LogLevel } from "../utils/mod.ts";
6
- import type { PublishPacket, QoS } from "../mqttPacket/mod.ts";
7
- import { delay, generateLocalhostCerts } from "../dev_utils/mod.ts";
6
+ import { generateLocalhostCerts } from "../dev_utils/mod.ts";
7
+ import { testClient } from "../dev_utils/testClient.ts";
8
+ import { MqttServer } from "../server/mod.ts";
8
9
 
9
10
  logger.level(LogLevel.info);
10
11
 
@@ -33,62 +34,14 @@ test("Test pubSub using client and server", async () => {
33
34
  logger.verbose("client parameters: ", params);
34
35
 
35
36
  const client = new TcpClient();
36
-
37
- await client.connect(params);
38
- assert(true, "Client connected to server");
39
-
40
- const publishSet: { topic: string; qos: QoS }[] = [
41
- { topic: "t0@q0", qos: 0 },
42
- { topic: "t1@q0", qos: 0 },
43
- { topic: "t2@q0", qos: 0 },
44
- { topic: "t0@q1", qos: 1 },
45
- { topic: "t1@q1", qos: 1 },
46
- { topic: "t2@q1", qos: 1 },
47
- { topic: "t0@q2", qos: 2 },
48
- { topic: "t1@q2", qos: 2 },
49
- { topic: "t2@q2", qos: 2 },
50
- ];
51
-
52
- const subscriptions = publishSet.map((item) => ({
53
- topicFilter: item.topic,
54
- qos: item.qos,
55
- }));
56
- await client.subscribe({
57
- subscriptions,
58
- });
59
-
60
- // the IIFE ensures message reception runs in parallel
61
- logger.verbose("Start receiving");
62
- const received: PublishPacket[] = [];
63
- (async () => {
64
- for await (const item of client.messages()) {
65
- logger.verbose("Receiving:", item.topic, "--", item.qos);
66
- received.push(item);
67
- }
68
- })();
69
- // end of IIFE
70
- for (const item of publishSet) {
71
- logger.verbose("Publishing:", item.topic, "--", item.qos);
72
- await client.publish({
73
- topic: item.topic,
74
- qos: item.qos,
75
- payload: new Uint8Array([0x01]),
76
- });
77
- }
78
-
79
- await delay(100);
80
- logger.verbose("Disconnect client");
81
- await client.disconnect();
82
-
83
- logger.verbose("Check completeness");
84
- for (const item of publishSet) {
85
- const found = received.find((f) =>
86
- f.topic == item.topic && f.qos === item.qos
87
- );
88
- logger.verbose("Found:", item.topic, "--", item.qos);
89
- assert(found, `${item.topic} -- ${item.qos}`);
90
- }
37
+ await testClient(client, params);
91
38
 
92
39
  logger.verbose("Stop server");
93
- server.stop();
40
+ await server.stop();
41
+ });
42
+
43
+ test("Passing a mqttServer works", () => {
44
+ const mqttServer = new MqttServer({});
45
+ const server = new TlsServer({ port: 0, key, cert }, mqttServer);
46
+ assert(server.mqttServer === mqttServer);
94
47
  });
package/node/tlsServer.ts CHANGED
@@ -7,7 +7,7 @@ import type { Server } from "node:net";
7
7
  import { createServer } from "node:tls";
8
8
  import type { MqttServerOptions } from "../server/mod.ts";
9
9
  import { MqttServer } from "../server/mod.ts";
10
- import { wrapNodeSocket } from "./wrapNodeSocket.ts";
10
+ import { nodeSocketToNext } from "./nodeSocketToNext.ts";
11
11
 
12
12
  type ServerOptions = {
13
13
  hostname?: string;
@@ -20,16 +20,24 @@ type ServerOptions = {
20
20
  * TLS server that wraps a MqttServer, see the /examples folder
21
21
  */
22
22
  export class TlsServer {
23
- private mqttServer: MqttServer;
23
+ /** the MqttServer instance used by the server */
24
+ readonly mqttServer: MqttServer;
24
25
  private server?: Server;
25
26
  private serverOptions;
26
27
 
27
28
  /**
28
29
  * Create a new TLS server
29
30
  */
30
- constructor(serverOptions: ServerOptions, mqttOptions: MqttServerOptions) {
31
- this.mqttServer = new MqttServer(mqttOptions);
31
+ constructor(
32
+ serverOptions: ServerOptions,
33
+ mqttOptions: MqttServerOptions | MqttServer,
34
+ ) {
32
35
  this.serverOptions = serverOptions;
36
+ if (mqttOptions instanceof MqttServer) {
37
+ this.mqttServer = mqttOptions;
38
+ } else {
39
+ this.mqttServer = new MqttServer(mqttOptions);
40
+ }
33
41
  }
34
42
 
35
43
  /**
@@ -44,7 +52,7 @@ export class TlsServer {
44
52
 
45
53
  this.server = createServer(
46
54
  tlsOptions,
47
- (sock) => this.mqttServer.serve(wrapNodeSocket(sock)),
55
+ (sock) => this.mqttServer.serve(nodeSocketToNext(sock)),
48
56
  );
49
57
  const isListening = new Promise((resolve) => {
50
58
  this.server?.on("listening", () => {
@@ -59,8 +67,10 @@ export class TlsServer {
59
67
  /**
60
68
  * Stop listening
61
69
  */
62
- stop(): void {
63
- this.mqttServer.close();
70
+ async stop(opts = { closeMqtt: true }): Promise<void> {
71
+ if (opts.closeMqtt) {
72
+ await this.mqttServer.close();
73
+ }
64
74
  this.server?.close();
65
75
  }
66
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seriousme/opifex",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "MQTT client & server for Deno & NodeJS and Bun",
5
5
  "author": "Hans Klunder",
6
6
  "license": "MIT",
@@ -30,7 +30,8 @@
30
30
  "build": "rm -rf dist && npx -p typescript tsc --build",
31
31
  "format": "deno fmt",
32
32
  "coverage": "deno test -A . --coverage --clean --coverage-raw-data-only && deno coverage --exclude=.config --exclude=dev_utils --html",
33
- "preversion": "npm run test"
33
+ "bundle": "deno bundle --platform=browser --minify -o examples/public/opifex-wsclient.min.js web/wsClient.ts --sourcemap",
34
+ "preversion": "npm run bundle && npm run test"
34
35
  },
35
36
  "files": [
36
37
  "package.json",
@@ -99,7 +100,7 @@
99
100
  }
100
101
  },
101
102
  "devDependencies": {
102
- "@types/node": "^26.6.2",
103
+ "@types/node": "^26.6.5",
103
104
  "@types/node-forge": "^1.3.14",
104
105
  "node-forge": "^1.4.0"
105
106
  }
package/server/context.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  import type {
11
11
  AnyPacket,
12
12
  ConnectPacket,
13
+ ConnNext,
13
14
  ExtPublishPacket,
14
15
  PacketId,
15
16
  Persistence,
@@ -18,7 +19,6 @@ import type {
18
19
  PubrelPacket,
19
20
  ReasonCode,
20
21
  ShareName,
21
- SockConn,
22
22
  Topic,
23
23
  } from "./deps.ts";
24
24
 
@@ -64,11 +64,11 @@ export type SessionState = typeof SessionState[keyof typeof SessionState];
64
64
  export type Handlers = {
65
65
  /**
66
66
  * Default preconnect handler that unconditionally permits all connections.
67
- * @param {SockConn} conn - The connection context.
67
+ * @param conn - The connection context.
68
68
  * @returns {boolean} false will close the connection
69
69
  */
70
70
  preconnect?(
71
- conn: SockConn,
71
+ conn: ConnNext,
72
72
  ): boolean | Promise<boolean>;
73
73
 
74
74
  /**
@@ -162,7 +162,7 @@ export class Context {
162
162
  protocolLevel: ProtocolLevel;
163
163
 
164
164
  /** The underlying raw socket connection. */
165
- conn: SockConn;
165
+ conn: ConnNext;
166
166
 
167
167
  /** The wrapped MQTT connection wrapper handling frame reading and writing. */
168
168
  mqttConn: MqttConn;
@@ -234,7 +234,7 @@ export class Context {
234
234
  constructor(
235
235
  configuration: Configuration, // all settings
236
236
  persistence: Persistence, // The server persistence layer implementation.
237
- conn: SockConn, // The underlying socket connection.
237
+ conn: ConnNext, // The underlying socket connection.
238
238
  handlers: Handlers, // The validation handlers
239
239
  ) {
240
240
  this.config = configuration;
@@ -676,10 +676,13 @@ export class Context {
676
676
  this.sessionEndsTimer.reset();
677
677
  }
678
678
 
679
+ const reason = (!this.mqttConn.reason || executewill)
680
+ ? "normal disconnect"
681
+ : this.mqttConn.reason;
679
682
  logger.info(
680
683
  `closing connection from ${
681
684
  this.clientId || this.mqttConn.remoteAddress
682
- } because of "${this.mqttConn.reason || "normal disconnect"}"`,
685
+ } because of "${reason}"`,
683
686
  );
684
687
  if (!this.mqttConn.isClosed) {
685
688
  this.mqttConn.close();
package/server/deps.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { SockAddr, SockConn } from "../socket/socket.ts";
1
+ export type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
2
2
  export { MqttConn } from "../mqttConn/mqttConn.ts";
3
3
  export type {
4
4
  ClientSubscription,
package/server/mod.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  export { ReasonCode } from "./deps.ts";
6
6
  export type {
7
7
  ConnectPacket,
8
+ ConnNext,
8
9
  Persistence,
9
10
  ShareName,
10
- SockConn,
11
11
  Topic,
12
12
  } from "./deps.ts";
13
13
  export { MqttServer } from "./server.ts";
package/server/server.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  import { logger, MemoryPersistence, ReasonCode } from "./deps.ts";
2
2
  import { Context } from "./context.ts";
3
3
  import type { Handlers } from "./context.ts";
4
- import type { Persistence, ShareName, SockConn, Topic } from "./deps.ts";
4
+ import type { ConnNext, Persistence, ShareName, Topic } from "./deps.ts";
5
5
  import { handlePacket } from "./handlers/handlePacket.ts";
6
6
  import { createConfiguration } from "./config.ts";
7
7
  import type { Configuration, ConfigurationInput } from "./config.ts";
8
8
 
9
9
  /**
10
10
  * Default preconnect handler that unconditionally permits all connections.
11
- * @param {SockConn} _conn - The connection.
12
- * @returns {boolean} Always returns true
11
+ * @param _conn - The connection.
12
+ * @returns Always returns true
13
13
  */
14
14
  const defaultPreconnect = (
15
- _conn: SockConn,
15
+ _conn: ConnNext,
16
16
  ) => (true) as const;
17
17
 
18
18
  /**
@@ -119,10 +119,10 @@ export class MqttServer {
119
119
 
120
120
  /**
121
121
  * Serve a new client connection.
122
- * @param {SockConn} conn - The socket connection to serve.
122
+ * @param conn - The socket connection to serve.
123
123
  * @returns {Promise<void>} A promise that resolves when the connection is closed.
124
124
  */
125
- async serve(conn: SockConn): Promise<void> {
125
+ async serve(conn: ConnNext): Promise<void> {
126
126
  if (this.handlers.preconnect) {
127
127
  if (!await this.handlers.preconnect(conn)) {
128
128
  conn.close();
package/utils/utils.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  * deserve their own dedicated files.
4
4
  */
5
5
 
6
- import { setTimeout as delay } from "node:timers/promises";
7
- export { delay };
6
+ export function delay(ms: number): Promise<void> {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
8
9
 
9
10
  export const noop = () => {};
@@ -0,0 +1,146 @@
1
+ import type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
2
+ import { BaseConnNext } from "../connNext/baseConnNext.ts";
3
+ import { delay } from "../utils/utils.ts";
4
+
5
+ export class WebSocketConnNext extends BaseConnNext {
6
+ localAddr = undefined;
7
+ remoteAddr?: ConnAddr;
8
+
9
+ private ws: WebSocket;
10
+ private messageQueue: Uint8Array[] = [];
11
+ private maxBufferSize: number;
12
+ private currentBufferSize = 0;
13
+ private readResolvers: Array<() => void> = [];
14
+
15
+ constructor(
16
+ ws: WebSocket,
17
+ remoteAddr?: ConnAddr,
18
+ maxBufferSize = 1 * 1024 * 1024,
19
+ ) {
20
+ super();
21
+ this.ws = ws;
22
+ this.remoteAddr = remoteAddr;
23
+ this.maxBufferSize = maxBufferSize;
24
+ this.ws.binaryType = "arraybuffer";
25
+
26
+ this.ws.addEventListener("message", (event: MessageEvent) => {
27
+ let chunk: Uint8Array;
28
+ if (typeof event.data === "string") {
29
+ chunk = new TextEncoder().encode(event.data);
30
+ } else if (event.data instanceof ArrayBuffer) {
31
+ chunk = new Uint8Array(event.data);
32
+ } else {
33
+ return;
34
+ }
35
+
36
+ // Check the bufferlimit (Throttling / Overflow protection)
37
+ if (this.currentBufferSize + chunk.byteLength > this.maxBufferSize) {
38
+ this.close();
39
+ throw new Error(
40
+ "WebSocket buffer overflow: Too much unread data from push source",
41
+ );
42
+ }
43
+
44
+ this.messageQueue.push(chunk);
45
+ this.currentBufferSize += chunk.byteLength;
46
+ this.notifyWaiters();
47
+ });
48
+
49
+ const onClose = () => {
50
+ this.closed = true;
51
+ this.notifyWaiters();
52
+ };
53
+ this.ws.addEventListener("close", onClose);
54
+ this.ws.addEventListener("error", onClose);
55
+ }
56
+
57
+ private notifyWaiters() {
58
+ while (this.readResolvers.length > 0) {
59
+ const resolve = this.readResolvers.shift();
60
+ resolve?.();
61
+ }
62
+ }
63
+
64
+ private waitForData(): Promise<void> {
65
+ return new Promise<void>((resolve) => {
66
+ this.readResolvers.push(resolve);
67
+ });
68
+ }
69
+
70
+ ensureOpen(): Promise<void> {
71
+ if (this.ws.readyState === WebSocket.OPEN) {
72
+ return Promise.resolve();
73
+ }
74
+ if (this.ws.readyState >= WebSocket.CLOSING) {
75
+ return Promise.reject(new Error("WebSocket is closing or closed"));
76
+ }
77
+ return new Promise((resolve, reject) => {
78
+ const onOpen = () => {
79
+ cleanup();
80
+ resolve();
81
+ };
82
+ const onError = (err: Event) => {
83
+ cleanup();
84
+ reject(err);
85
+ };
86
+ const onClose = () => {
87
+ cleanup();
88
+ reject(new Error("WebSocket closed before opening"));
89
+ };
90
+ const cleanup = () => {
91
+ this.ws.removeEventListener("open", onOpen);
92
+ this.ws.removeEventListener("error", onError);
93
+ this.ws.removeEventListener("close", onClose);
94
+ };
95
+
96
+ this.ws.addEventListener("open", onOpen);
97
+ this.ws.addEventListener("error", onError);
98
+ this.ws.addEventListener("close", onClose);
99
+ });
100
+ }
101
+
102
+ protected async pullNextChunk(): Promise<Uint8Array | null> {
103
+ if (this.messageQueue.length === 0) {
104
+ await this.waitForData();
105
+ }
106
+ if (this.closed) {
107
+ return null;
108
+ }
109
+ const chunk = this.messageQueue.shift()!;
110
+ this.currentBufferSize -= chunk.byteLength;
111
+ return chunk;
112
+ }
113
+
114
+ async write(data: Uint8Array): Promise<void> {
115
+ if (this.isClosed()) throw new Error("WebSocket is closed");
116
+ this.ws.send(data.buffer as ArrayBuffer);
117
+
118
+ // Check WebSocket bufferedAmount for write-backpressure
119
+ const HIGH_WATER_MARK = 64 * 1024;
120
+ while (this.ws.bufferedAmount > HIGH_WATER_MARK) {
121
+ if (this.isClosed()) throw new Error("WebSocket closed while writing");
122
+ await delay(10);
123
+ }
124
+ }
125
+
126
+ close(): void {
127
+ if (!this.closed) {
128
+ this.closed = true;
129
+ try {
130
+ this.ws.close();
131
+ } catch {
132
+ /* swallow */
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ export async function webSocketToNext(
139
+ ws: WebSocket,
140
+ remoteAddr?: ConnAddr,
141
+ maxBufferSize?: number,
142
+ ): Promise<ConnNext> {
143
+ const connNext = new WebSocketConnNext(ws, remoteAddr, maxBufferSize);
144
+ await connNext.ensureOpen();
145
+ return connNext;
146
+ }
@@ -24,3 +24,25 @@ test("Web: Test createConn - unsupported protocol", async () => {
24
24
  "Should throw an error when an unsupported protocol is provided",
25
25
  );
26
26
  });
27
+
28
+ test("Web: Test createConn - unsupported host", async () => {
29
+ const client = new WsClient();
30
+
31
+ // Use an host protocol to trigger the ws error
32
+ const invalidParams = {
33
+ url: new URL("ws://test.invalid"),
34
+ numberOfRetries: 0,
35
+ };
36
+
37
+ // Assert that connect rejects with the specific error
38
+ await assert.rejects(
39
+ async () => {
40
+ await client.connect(invalidParams);
41
+ },
42
+ {
43
+ name: "Error",
44
+ message: /^Failed to connect to WebSocket/,
45
+ },
46
+ "Should throw an error when an unreachable host is provided",
47
+ );
48
+ });
package/web/wsClient.ts CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  import { Client } from "../client/client.ts";
8
8
  import { logger } from "../client/deps.ts";
9
- import type { SockConn } from "../client/deps.ts";
10
- import { wrapWebSocket } from "./wrapWebSocket.ts";
9
+ import type { ConnNext, NetAddr } from "../client/deps.ts";
10
+ import { webSocketToNext } from "./webSocketToNext.ts";
11
11
 
12
12
  /**
13
13
  * WsClient extends the Client class to provide WebSocket based clients
@@ -15,22 +15,42 @@ import { wrapWebSocket } from "./wrapWebSocket.ts";
15
15
  * see the /examples folder for an example
16
16
  */
17
17
  export class WsClient extends Client {
18
- /** create a websocket and wrap it in a SockConn */
19
- private async establishSocket(url: string): Promise<SockConn> {
20
- const ws = new WebSocket(url);
21
- const conn = await wrapWebSocket(ws);
22
- return conn;
18
+ /** create a websocket and wrap it in a ConnNext */
19
+ private async establishSocket(
20
+ url: string,
21
+ maxBufferSize?: number,
22
+ ): Promise<ConnNext> {
23
+ try {
24
+ const urlObj = new URL(url);
25
+ const remoteAddr: NetAddr = {
26
+ transport: "tcp",
27
+ hostname: urlObj.hostname,
28
+ port: Number(urlObj.port || 0),
29
+ };
30
+ const ws = new WebSocket(url);
31
+ const connNext = await webSocketToNext(ws, remoteAddr, maxBufferSize);
32
+ return connNext;
33
+ } catch (error) {
34
+ const message = error instanceof ErrorEvent
35
+ ? ` because of ${error.message}`
36
+ : "";
37
+ throw new Error(`Failed to connect to WebSocket at ${url}${message}`);
38
+ }
23
39
  }
24
40
 
25
41
  /** createConn provides support for both "ws:" and "wss:" URLs */
26
- protected override createConn(): Promise<SockConn> {
42
+ protected override createConn(): Promise<ConnNext> {
27
43
  const { protocol, hostname, port: portStr, pathname } = this.connectUrl;
28
44
  const port = portStr ? Number(portStr) : (protocol === "wss:" ? 443 : 80);
29
45
  const path = pathname || "/mqtt";
46
+ const maxBufferSize = this.maxBufferSize;
30
47
 
31
48
  logger.debug({ hostname, port, path });
32
49
  if (protocol === "ws:" || protocol === "wss:") {
33
- return this.establishSocket(`${protocol}//${hostname}:${port}${path}`);
50
+ return this.establishSocket(
51
+ `${protocol}//${hostname}:${port}${path}`,
52
+ maxBufferSize,
53
+ );
34
54
  }
35
55
  throw new Error(`Unsupported protocol: ${protocol}`);
36
56
  }