@seriousme/opifex 2.2.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 (142) hide show
  1. package/README.md +7 -6
  2. package/bin/mqtt.ts +1 -1
  3. package/client/client.ts +57 -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/tcpClient.ts +8 -24
  11. package/deno/tcpServer.test.ts +14 -103
  12. package/deno/tcpServer.ts +14 -6
  13. package/deno/tlsServer.test.ts +14 -58
  14. package/deno/tlsServer.ts +14 -6
  15. package/deno/wsServer.test.ts +15 -61
  16. package/deno/wsServer.ts +74 -32
  17. package/dev_utils/asyncStreamPair.ts +114 -0
  18. package/dev_utils/asyncStreamToNext.ts +171 -0
  19. package/dev_utils/connNextPair.test.ts +179 -0
  20. package/dev_utils/mockConn.ts +115 -29
  21. package/dev_utils/mockServer.ts +3 -4
  22. package/dev_utils/mod.ts +1 -2
  23. package/dev_utils/testClient.ts +64 -0
  24. package/dev_utils/webStreamToNext.ts +187 -0
  25. package/dist/bin/mqtt.js +1 -1
  26. package/dist/bin/mqtt.js.map +1 -1
  27. package/dist/client/client.d.ts +20 -3
  28. package/dist/client/client.d.ts.map +1 -1
  29. package/dist/client/client.js +42 -5
  30. package/dist/client/client.js.map +1 -1
  31. package/dist/client/context.d.ts +2 -2
  32. package/dist/client/context.d.ts.map +1 -1
  33. package/dist/client/deps.d.ts +1 -1
  34. package/dist/client/deps.d.ts.map +1 -1
  35. package/dist/client/mod.d.ts +1 -1
  36. package/dist/client/mod.d.ts.map +1 -1
  37. package/dist/connNext/connNext.d.ts +43 -0
  38. package/dist/connNext/connNext.d.ts.map +1 -0
  39. package/dist/connNext/connNext.js +2 -0
  40. package/dist/connNext/connNext.js.map +1 -0
  41. package/dist/examples/authHandler.test.js +2 -2
  42. package/dist/examples/authHandler.test.js.map +1 -1
  43. package/dist/examples/mqtt+mqtts.test.d.ts +2 -0
  44. package/dist/examples/mqtt+mqtts.test.d.ts.map +1 -0
  45. package/dist/examples/mqtt+mqtts.test.js +104 -0
  46. package/dist/examples/mqtt+mqtts.test.js.map +1 -0
  47. package/dist/examples/mqtt.test.js +2 -2
  48. package/dist/examples/mqtt.test.js.map +1 -1
  49. package/dist/examples/mqtts.test.js +2 -2
  50. package/dist/examples/mqtts.test.js.map +1 -1
  51. package/dist/mqttConn/mqttConn.d.ts +5 -6
  52. package/dist/mqttConn/mqttConn.d.ts.map +1 -1
  53. package/dist/mqttConn/mqttConn.js +2 -3
  54. package/dist/mqttConn/mqttConn.js.map +1 -1
  55. package/dist/node/nodeSocketToNext.d.ts +20 -0
  56. package/dist/node/nodeSocketToNext.d.ts.map +1 -0
  57. package/dist/node/nodeSocketToNext.js +161 -0
  58. package/dist/node/nodeSocketToNext.js.map +1 -0
  59. package/dist/node/tcpClient.d.ts +4 -6
  60. package/dist/node/tcpClient.d.ts.map +1 -1
  61. package/dist/node/tcpClient.js +6 -21
  62. package/dist/node/tcpClient.js.map +1 -1
  63. package/dist/node/tcpServer.d.ts +8 -4
  64. package/dist/node/tcpServer.d.ts.map +1 -1
  65. package/dist/node/tcpServer.js +14 -6
  66. package/dist/node/tcpServer.js.map +1 -1
  67. package/dist/node/tlsServer.d.ts +7 -3
  68. package/dist/node/tlsServer.d.ts.map +1 -1
  69. package/dist/node/tlsServer.js +13 -5
  70. package/dist/node/tlsServer.js.map +1 -1
  71. package/dist/server/context.d.ts +5 -5
  72. package/dist/server/context.d.ts.map +1 -1
  73. package/dist/server/context.js +4 -1
  74. package/dist/server/context.js.map +1 -1
  75. package/dist/server/deps.d.ts +1 -1
  76. package/dist/server/deps.d.ts.map +1 -1
  77. package/dist/server/mod.d.ts +1 -1
  78. package/dist/server/mod.d.ts.map +1 -1
  79. package/dist/server/server.d.ts +3 -3
  80. package/dist/server/server.d.ts.map +1 -1
  81. package/dist/server/server.js +3 -3
  82. package/dist/utils/utils.d.ts +1 -2
  83. package/dist/utils/utils.d.ts.map +1 -1
  84. package/dist/utils/utils.js +3 -2
  85. package/dist/utils/utils.js.map +1 -1
  86. package/dist/web/webSocketToNext.d.ts +3 -0
  87. package/dist/web/webSocketToNext.d.ts.map +1 -0
  88. package/dist/web/webSocketToNext.js +160 -0
  89. package/dist/web/webSocketToNext.js.map +1 -0
  90. package/dist/web/wsClient.d.ts +3 -3
  91. package/dist/web/wsClient.d.ts.map +1 -1
  92. package/dist/web/wsClient.js +19 -5
  93. package/dist/web/wsClient.js.map +1 -1
  94. package/dist/web/wsClient.test.js +15 -0
  95. package/dist/web/wsClient.test.js.map +1 -1
  96. package/examples/README.md +21 -0
  97. package/examples/authHandler.test.ts +2 -2
  98. package/examples/mqtt+mqtts.test.ts +122 -0
  99. package/examples/mqtt.test.ts +2 -2
  100. package/examples/mqtts.test.ts +2 -2
  101. package/examples/webSocket.test.ts +10 -64
  102. package/examples/webSocketServer.ts +98 -0
  103. package/mqttConn/mqttConn.test.ts +6 -6
  104. package/mqttConn/mqttConn.ts +8 -9
  105. package/node/README.md +1 -1
  106. package/node/{socket-node-stream.ts → nodeSocketToNext.ts} +78 -65
  107. package/node/tcpClient.ts +13 -27
  108. package/node/tcpServer.test.ts +12 -58
  109. package/node/tcpServer.ts +18 -8
  110. package/node/tlsServer.test.ts +11 -58
  111. package/node/tlsServer.ts +17 -7
  112. package/package.json +3 -2
  113. package/server/context.ts +9 -6
  114. package/server/deps.ts +1 -1
  115. package/server/mod.ts +1 -1
  116. package/server/server.ts +6 -6
  117. package/utils/utils.ts +3 -2
  118. package/web/webSocketToNext.ts +183 -0
  119. package/web/wsClient.test.ts +22 -0
  120. package/web/wsClient.ts +21 -8
  121. package/dist/node/wrapNodeSocket.d.ts +0 -12
  122. package/dist/node/wrapNodeSocket.d.ts.map +0 -1
  123. package/dist/node/wrapNodeSocket.js +0 -36
  124. package/dist/node/wrapNodeSocket.js.map +0 -1
  125. package/dist/socket/socket.d.ts +0 -58
  126. package/dist/socket/socket.d.ts.map +0 -1
  127. package/dist/socket/socket.js +0 -99
  128. package/dist/socket/socket.js.map +0 -1
  129. package/dist/web/webSocketStream.d.ts +0 -29
  130. package/dist/web/webSocketStream.d.ts.map +0 -1
  131. package/dist/web/webSocketStream.js +0 -69
  132. package/dist/web/webSocketStream.js.map +0 -1
  133. package/dist/web/wrapWebSocket.d.ts +0 -12
  134. package/dist/web/wrapWebSocket.d.ts.map +0 -1
  135. package/dist/web/wrapWebSocket.js +0 -41
  136. package/dist/web/wrapWebSocket.js.map +0 -1
  137. package/node/socket-node-stream.test.ts +0 -322
  138. package/node/wrapNodeSocket.ts +0 -44
  139. package/socket/socket.test.ts +0 -91
  140. package/socket/socket.ts +0 -149
  141. package/web/webSocketStream.ts +0 -97
  142. package/web/wrapWebSocket.ts +0 -52
@@ -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
 
@@ -36,62 +37,17 @@ test("Deno: Test pubSub using client and server", async () => {
36
37
  logger.verbose("client parameters: ", params);
37
38
 
38
39
  const client = new TcpClient();
39
-
40
- await client.connect(params);
41
- assert(true, "Client connected to server");
42
-
43
- const publishSet: { topic: string; qos: QoS }[] = [
44
- { topic: "t0@q0", qos: 0 },
45
- { topic: "t1@q0", qos: 0 },
46
- { topic: "t2@q0", qos: 0 },
47
- { topic: "t0@q1", qos: 1 },
48
- { topic: "t1@q1", qos: 1 },
49
- { topic: "t2@q1", qos: 1 },
50
- { topic: "t0@q2", qos: 2 },
51
- { topic: "t1@q2", qos: 2 },
52
- { topic: "t2@q2", qos: 2 },
53
- ];
54
-
55
- const subscriptions = publishSet.map((item) => ({
56
- topicFilter: item.topic,
57
- qos: item.qos,
58
- }));
59
- await client.subscribe({
60
- subscriptions,
61
- });
62
-
63
- // the IIFE ensures message reception runs in parallel
64
- logger.verbose("Start receiving");
65
- const received: PublishPacket[] = [];
66
- (async () => {
67
- for await (const item of client.messages()) {
68
- logger.verbose("Receiving:", item.topic, "--", item.qos);
69
- received.push(item);
70
- }
71
- })();
72
- // end of IIFE
73
- for (const item of publishSet) {
74
- logger.verbose("Publishing:", item.topic, "--", item.qos);
75
- await client.publish({
76
- topic: item.topic,
77
- qos: item.qos,
78
- payload: new Uint8Array([0x01]),
79
- });
80
- }
81
-
82
- await delay(100);
83
- logger.verbose("Disconnect client");
84
- await client.disconnect();
85
-
86
- logger.verbose("Check completeness");
87
- for (const item of publishSet) {
88
- const found = received.find((f) =>
89
- f.topic == item.topic && f.qos === item.qos
90
- );
91
- logger.verbose("Found:", item.topic, "--", item.qos);
92
- assert(found, `${item.topic} -- ${item.qos}`);
93
- }
40
+ await testClient(client, params);
94
41
 
95
42
  logger.verbose("Stop server");
96
- server.stop();
43
+ await server.stop();
44
+ });
45
+
46
+ test("Deno: Passing a mqttServer works", () => {
47
+ const mqttServer = new MqttServer({});
48
+ const server = new TlsServer(
49
+ { hostname: "localhost", port: 0, key, cert },
50
+ mqttServer,
51
+ );
52
+ assert(server.mqttServer === mqttServer);
97
53
  });
package/deno/tlsServer.ts CHANGED
@@ -6,22 +6,28 @@
6
6
 
7
7
  import { MqttServer } from "../server/mod.ts";
8
8
  import type { MqttServerOptions } from "../server/mod.ts";
9
+ import { denoConnToNext } from "./denoConnToNext.ts";
9
10
 
10
11
  /**
11
12
  * TLS server that wraps a MqttServer, see the /examples folder
12
13
  */
13
14
  export class TlsServer {
15
+ /** the MqttServer instance used by the server */
16
+ readonly mqttServer: MqttServer;
14
17
  private listener: Deno.TlsListener;
15
- private mqttServer: MqttServer;
16
18
  /**
17
19
  * Create a new TLS server
18
20
  */
19
21
  constructor(
20
22
  serverOptions: Deno.ListenTlsOptions & Deno.TlsCertifiedKeyPem,
21
- mqttOptions: MqttServerOptions,
23
+ mqttOptions: MqttServerOptions | MqttServer,
22
24
  ) {
23
25
  this.listener = Deno.listenTls(serverOptions);
24
- this.mqttServer = new MqttServer(mqttOptions);
26
+ if (mqttOptions instanceof MqttServer) {
27
+ this.mqttServer = mqttOptions;
28
+ } else {
29
+ this.mqttServer = new MqttServer(mqttOptions);
30
+ }
25
31
  }
26
32
 
27
33
  /**
@@ -29,15 +35,17 @@ export class TlsServer {
29
35
  */
30
36
  async start(): Promise<void> {
31
37
  for await (const conn of this.listener) {
32
- this.mqttServer.serve(conn);
38
+ this.mqttServer.serve(denoConnToNext(conn));
33
39
  }
34
40
  }
35
41
 
36
42
  /**
37
43
  * Stop listening
38
44
  */
39
- stop(): void {
40
- this.mqttServer.close();
45
+ async stop(opts = { closeMqtt: true }): Promise<void> {
46
+ if (opts.closeMqtt) {
47
+ await this.mqttServer.close();
48
+ }
41
49
  this.listener.close();
42
50
  }
43
51
  /**
@@ -2,8 +2,9 @@ import { test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { WsClient } from "../web/wsClient.ts";
4
4
  import { WsServer } from "./wsServer.ts";
5
- import { delay, logger, LogLevel } from "../utils/mod.ts";
6
- import type { PublishPacket, QoS } from "../mqttPacket/mod.ts";
5
+ import { logger, LogLevel } from "../utils/mod.ts";
6
+ import { testClient } from "../dev_utils/testClient.ts";
7
+ import { MqttServer } from "../server/mod.ts";
7
8
 
8
9
  logger.level(LogLevel.info);
9
10
 
@@ -25,69 +26,22 @@ test("Deno: Test pubSub using client and server over webSockets", async () => {
25
26
  });
26
27
 
27
28
  const params = {
28
- url: new URL(`ws://localhost:${server.port}`),
29
+ url: new URL(`ws://localhost:${server.port}/mqtt`),
29
30
  numberOfRetries: 0,
30
31
  };
31
32
 
32
- logger.verbose("client parameters: ", params);
33
-
34
33
  const client = new WsClient();
35
- client.onConnected = () => logger.verbose("Client connected to server");
36
-
37
- await client.connect(params);
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,
57
- });
58
-
59
- // the IIFE ensures message reception runs in parallel
60
- logger.verbose("Start receiving");
61
- const received: PublishPacket[] = [];
62
- (async () => {
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.verbose("Disconnect client");
80
- await client.disconnect();
81
-
82
- logger.verbose("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
- }
34
+ await testClient(client, params);
90
35
 
91
36
  logger.verbose("Stop server");
92
- server.stop();
37
+ await server.stop();
38
+ });
39
+
40
+ test("Deno: Passing a mqttServer works", () => {
41
+ const mqttServer = new MqttServer({});
42
+ const server = new WsServer(
43
+ { hostname: "localhost", port: 0 },
44
+ mqttServer,
45
+ );
46
+ assert(server.mqttServer === mqttServer);
93
47
  });
package/deno/wsServer.ts CHANGED
@@ -5,66 +5,108 @@
5
5
  import { MqttServer } from "../server/mod.ts";
6
6
  import type { MqttServerOptions } from "../server/mod.ts";
7
7
  import { logger } from "../utils/logger.ts";
8
- import { wrapWebSocket } from "../web/wrapWebSocket.ts";
9
- import type { HostnamePort } from "../web/wrapWebSocket.ts";
8
+ import { webSocketToNext } from "../web/webSocketToNext.ts";
9
+ import type { NetAddr } from "../connNext/connNext.ts";
10
+
11
+ /** for those who want wrap their own websockets */
12
+ export { webSocketToNext };
13
+ export type RequestHandler = (
14
+ req: Request,
15
+ info: Deno.ServeHandlerInfo<NetAddr>,
16
+ ) => Response | Promise<Response>;
17
+
18
+ export const pageNotFound: RequestHandler = (_req, _info) => {
19
+ return new Response("Page not found", { status: 404 });
20
+ };
21
+
22
+ async function handleWsClient(
23
+ socket: WebSocket,
24
+ mqttServer: MqttServer,
25
+ remoteAddr: NetAddr,
26
+ ) {
27
+ const conn = await webSocketToNext(socket, remoteAddr);
28
+ mqttServer.serve(conn);
29
+ logger.debug("serving mqtt");
30
+ }
31
+
32
+ export function mqttOverWS(
33
+ mqttServer: MqttServer,
34
+ pathName: string,
35
+ next: RequestHandler,
36
+ ): RequestHandler {
37
+ const wsRequestHandler: RequestHandler = (req, info) => {
38
+ const url = new URL(req.url);
39
+ if (url.pathname === pathName) {
40
+ if (req.headers.get("upgrade") !== "websocket") {
41
+ return new Response("Expected WebSocket connection.", { status: 426 });
42
+ }
43
+ logger.debug("starting upgrade to websocket");
44
+ const remoteAddr = info.remoteAddr;
45
+ const { socket, response } = Deno.upgradeWebSocket(req);
46
+ logger.debug("upgraded to websocket");
47
+ // do not await on purpose
48
+ handleWsClient(socket, mqttServer, remoteAddr);
49
+ return response;
50
+ }
51
+ return next(req, info);
52
+ };
53
+ return wsRequestHandler;
54
+ }
10
55
 
11
56
  /**
12
57
  * WebSocket server that wraps a MqttServer, see the /examples folder
13
58
  */
14
59
  export class WsServer {
60
+ /** the MqttServer instance used by the server */
61
+ readonly mqttServer: MqttServer;
15
62
  private server?: Deno.HttpServer;
16
- private mqttServer: MqttServer;
17
63
  private listenOptions: Deno.ServeOptions & {
18
64
  port: number;
19
65
  hostname?: string;
20
66
  };
67
+ private pathName: string;
21
68
 
22
69
  /**
23
70
  * Create a new WebSocket server
24
71
  */
25
72
  constructor(
26
- serverOptions: Deno.ServeOptions & { port: number; hostname?: string },
27
- mqttOptions: MqttServerOptions,
73
+ serverOptions: Deno.ServeOptions & {
74
+ port: number;
75
+ hostname?: string;
76
+ pathName?: string;
77
+ },
78
+ mqttOptions: MqttServerOptions | MqttServer,
28
79
  ) {
29
- this.listenOptions = serverOptions;
30
- this.mqttServer = new MqttServer(mqttOptions);
31
- }
32
-
33
- private async handleWsClient(socket: WebSocket, remoteAddr: HostnamePort) {
34
- const conn = await wrapWebSocket(socket, remoteAddr);
35
- logger.debug("created conn");
36
- this.mqttServer.serve(conn);
37
- logger.debug("serving mqtt");
80
+ const { pathName, ...listenOptions } = serverOptions;
81
+ this.listenOptions = listenOptions;
82
+ this.pathName = pathName ?? "/mqtt";
83
+ if (mqttOptions instanceof MqttServer) {
84
+ this.mqttServer = mqttOptions;
85
+ } else {
86
+ this.mqttServer = new MqttServer(mqttOptions);
87
+ }
38
88
  }
39
89
 
40
90
  /**
41
91
  * Start listening
42
92
  */
43
93
  async start(): Promise<void> {
44
- this.server = Deno.serve(this.listenOptions, (req, info) => {
45
- const { hostname, port } = info.remoteAddr;
46
- logger.debug(
47
- `Server received http connection from ${hostname}:${port}`,
48
- );
49
-
50
- if (req.headers.get("upgrade") !== "websocket") {
51
- return new Response("Expected WebSocket connection.", { status: 426 });
52
- }
53
- logger.debug("starting upgrade to websocket");
54
- const { socket, response } = Deno.upgradeWebSocket(req);
55
- logger.debug("upgraded to websocket");
56
- this.handleWsClient(socket, { hostname, port });
57
- return response;
58
- });
59
-
94
+ const requestHandler = mqttOverWS(
95
+ this.mqttServer,
96
+ this.pathName,
97
+ pageNotFound,
98
+ );
99
+ this.server = Deno.serve(this.listenOptions, requestHandler);
60
100
  await this.server.finished;
61
101
  }
62
102
 
63
103
  /**
64
104
  * Stop listening
65
105
  */
66
- stop(): void {
67
- this.mqttServer.close(true);
106
+ async stop(opts = { closeMqtt: true }): Promise<void> {
107
+ if (opts.closeMqtt) {
108
+ await this.mqttServer.close();
109
+ }
68
110
  if (this.server) {
69
111
  this.server.shutdown();
70
112
  }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Duplex AsyncStream
3
+ */
4
+
5
+ export interface AsyncStreamPairEndpoint {
6
+ readable: AsyncIterable<Uint8Array>;
7
+ write: (chunk: Uint8Array) => Promise<void>;
8
+ close: () => void;
9
+ }
10
+
11
+ /**
12
+ * Interne push/pull queue to support AsyncIterable iteration.
13
+ */
14
+ class AsyncQueue<T> implements AsyncIterable<T> {
15
+ private queue: T[] = [];
16
+ private resolvers: Array<(value: IteratorResult<T>) => void> = [];
17
+ private closed = false;
18
+ private error: unknown = null;
19
+
20
+ push(value: T): void {
21
+ if (this.closed) return;
22
+
23
+ if (this.resolvers.length > 0) {
24
+ const resolve = this.resolvers.shift()!;
25
+ resolve({ value, done: false });
26
+ } else {
27
+ this.queue.push(value);
28
+ }
29
+ }
30
+
31
+ close(): void {
32
+ if (this.closed) return;
33
+ this.closed = true;
34
+
35
+ while (this.resolvers.length > 0) {
36
+ const resolve = this.resolvers.shift()!;
37
+ if (this.error) {
38
+ resolve(Promise.reject(this.error) as unknown as IteratorResult<T>);
39
+ } else {
40
+ resolve({ value: undefined, done: true });
41
+ }
42
+ }
43
+ }
44
+
45
+ abort(reason: unknown): void {
46
+ this.error = reason;
47
+ this.close();
48
+ }
49
+
50
+ async *[Symbol.asyncIterator](): AsyncIterator<T> {
51
+ while (true) {
52
+ if (this.queue.length > 0) {
53
+ yield this.queue.shift()!;
54
+ } else if (this.closed) {
55
+ if (this.error) {
56
+ throw this.error;
57
+ }
58
+ return;
59
+ } else {
60
+ const nextResult = await new Promise<IteratorResult<T>>((resolve) => {
61
+ this.resolvers.push(resolve);
62
+ });
63
+
64
+ if (nextResult.done) {
65
+ if (this.error) {
66
+ throw this.error;
67
+ }
68
+ return;
69
+ }
70
+
71
+ yield nextResult.value;
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ export function createAsyncStreamPair(): {
78
+ input: AsyncStreamPairEndpoint;
79
+ output: AsyncStreamPairEndpoint;
80
+ } {
81
+ const queueA = new AsyncQueue<Uint8Array>();
82
+ const queueB = new AsyncQueue<Uint8Array>();
83
+
84
+ let closed = false;
85
+
86
+ const closeBoth = () => {
87
+ if (!closed) {
88
+ closed = true;
89
+ queueA.close();
90
+ queueB.close();
91
+ }
92
+ };
93
+
94
+ return {
95
+ input: {
96
+ readable: queueA,
97
+ write: (chunk: Uint8Array): Promise<void> => {
98
+ if (closed) throw new Error("Stream is closed");
99
+ queueB.push(chunk);
100
+ return Promise.resolve();
101
+ },
102
+ close: closeBoth,
103
+ },
104
+ output: {
105
+ readable: queueB,
106
+ write: (chunk: Uint8Array): Promise<void> => {
107
+ if (closed) throw new Error("Stream is closed");
108
+ queueA.push(chunk);
109
+ return Promise.resolve();
110
+ },
111
+ close: closeBoth,
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,171 @@
1
+ import type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
2
+ import { createAsyncStreamPair } from "./asyncStreamPair.ts";
3
+ import type { AsyncStreamPairEndpoint } from "./asyncStreamPair.ts";
4
+
5
+ /** wrapper class */
6
+ export class AsyncStreamConnNextWrapper {
7
+ closed: boolean;
8
+ private endPoint: AsyncStreamPairEndpoint;
9
+ private leftover: Uint8Array = new Uint8Array(0);
10
+ private reader: AsyncIterator<Uint8Array>;
11
+
12
+ /** Create a new Endpoint*/
13
+ constructor(endPoint: AsyncStreamPairEndpoint) {
14
+ this.endPoint = endPoint;
15
+ this.closed = false;
16
+ this.reader = endPoint.readable[Symbol.asyncIterator]();
17
+ }
18
+
19
+ /**
20
+ * Writes data chunk to the underlying AsyncStream
21
+ */ async write(chunk: Uint8Array): Promise<void> {
22
+ if (this.closed) {
23
+ return Promise.reject(new Error("Pipe closed"));
24
+ }
25
+ try {
26
+ await this.endPoint.write(chunk);
27
+ return;
28
+ } catch (err) {
29
+ this.closed = true;
30
+ throw err;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Reads data from the stream.
36
+ * If `length` is provided, waits until exact number of bytes are buffered.
37
+ */
38
+ async read(length?: number): Promise<Uint8Array | null> {
39
+ if (this.closed || (length !== undefined && length < 0)) {
40
+ return null;
41
+ }
42
+
43
+ if (length === 0) {
44
+ return new Uint8Array(0);
45
+ }
46
+
47
+ // Helper to fetch the next chunk from the underlying iterable
48
+ const fetchNextChunk = async (): Promise<Uint8Array | null> => {
49
+ const { value, done } = await this.reader.next();
50
+ if (done) {
51
+ this.closed = true;
52
+ return null;
53
+ }
54
+ return value;
55
+ };
56
+
57
+ // Unbounded read (length === undefined)
58
+ if (length === undefined) {
59
+ if (this.leftover.length > 0) {
60
+ const result = this.leftover;
61
+ this.leftover = new Uint8Array(0);
62
+ return result;
63
+ }
64
+ return await fetchNextChunk();
65
+ }
66
+
67
+ // Exact length read
68
+ const bytesRequested = length;
69
+ const result = new Uint8Array(bytesRequested);
70
+ let bytesAccumulated = 0;
71
+
72
+ // Use leftover data first if available
73
+ if (this.leftover.length > 0) {
74
+ const take = Math.min(this.leftover.length, bytesRequested);
75
+ result.set(this.leftover.subarray(0, take), bytesAccumulated);
76
+ bytesAccumulated += take;
77
+ this.leftover = this.leftover.subarray(take);
78
+ }
79
+
80
+ // Read remaining required bytes
81
+ while (bytesAccumulated < bytesRequested) {
82
+ const value = await fetchNextChunk();
83
+
84
+ if (value === null) {
85
+ // Stream ended before collecting required bytes
86
+ if (bytesAccumulated === 0) return null;
87
+ return result.subarray(0, bytesAccumulated);
88
+ }
89
+
90
+ const needed = bytesRequested - bytesAccumulated;
91
+
92
+ if (value.length <= needed) {
93
+ result.set(value, bytesAccumulated);
94
+ bytesAccumulated += value.length;
95
+ } else {
96
+ result.set(value.subarray(0, needed), bytesAccumulated);
97
+ bytesAccumulated += needed;
98
+ this.leftover = value.subarray(needed);
99
+ }
100
+ }
101
+
102
+ return result;
103
+ }
104
+
105
+ /**
106
+ * Closes the reader, writer, and underlying AsyncStream pair endpoint.
107
+ */
108
+ close(): void {
109
+ if (!this.closed) {
110
+ this.closed = true;
111
+ try {
112
+ this.endPoint.close();
113
+ } catch (_err) { /* swallow */ }
114
+ }
115
+ }
116
+ async *[Symbol.asyncIterator]() {
117
+ while (true) {
118
+ const chunk = await this.read();
119
+ if (chunk === null) {
120
+ break;
121
+ }
122
+ yield chunk;
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Creates a pair of cross-connected ConnNext sockets backed by AsyncStreams.
129
+ * Offers the exact same API as `createConnNextPair`.
130
+ */
131
+ export function createConnNextPair(): {
132
+ input: ConnNext;
133
+ output: ConnNext;
134
+ } {
135
+ const { input: asyncInput, output: asyncOutput } = createAsyncStreamPair();
136
+
137
+ const mockAddr: ConnAddr = {
138
+ transport: "tcp",
139
+ hostname: "127.0.0.1",
140
+ port: 1883,
141
+ };
142
+
143
+ const inputWrapper = new AsyncStreamConnNextWrapper(asyncInput);
144
+ const outputWrapper = new AsyncStreamConnNextWrapper(asyncOutput);
145
+
146
+ const input: ConnNext = {
147
+ remoteAddr: mockAddr,
148
+ localAddr: mockAddr,
149
+ read: (length) => inputWrapper.read(length),
150
+ write: (data) => inputWrapper.write(data),
151
+ [Symbol.asyncIterator]: () => inputWrapper[Symbol.asyncIterator](),
152
+ close: () => {
153
+ inputWrapper.close();
154
+ outputWrapper.close();
155
+ },
156
+ };
157
+
158
+ const output: ConnNext = {
159
+ remoteAddr: mockAddr,
160
+ localAddr: mockAddr,
161
+ read: (length) => outputWrapper.read(length),
162
+ write: (data) => outputWrapper.write(data),
163
+ [Symbol.asyncIterator]: () => outputWrapper[Symbol.asyncIterator](),
164
+ close: () => {
165
+ outputWrapper.close();
166
+ inputWrapper.close();
167
+ },
168
+ };
169
+
170
+ return { input, output };
171
+ }