@seriousme/opifex 2.3.0 → 2.4.1

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 (51) hide show
  1. package/client/client.ts +5 -0
  2. package/connNext/baseConnNext.ts +123 -0
  3. package/deno/tlsServer.test.ts +1 -1
  4. package/deno/wsServer.ts +11 -3
  5. package/dev_utils/WebStreamEndpoint.ts +5 -0
  6. package/dev_utils/asyncStreamEndpoint.ts +5 -0
  7. package/dev_utils/asyncStreamPair.ts +4 -9
  8. package/dev_utils/asyncStreamPairToNext.ts +45 -0
  9. package/dev_utils/asyncStreamToNext.ts +30 -137
  10. package/dev_utils/connNextPair.test.ts +2 -2
  11. package/dev_utils/generateCert.ts +297 -77
  12. package/dev_utils/mockConn.ts +37 -93
  13. package/dev_utils/mockServer.ts +1 -1
  14. package/dev_utils/webStreamPair.ts +3 -7
  15. package/dev_utils/webStreamPairToNext.ts +48 -0
  16. package/dev_utils/webStreamToNext.ts +28 -161
  17. package/dist/client/client.d.ts +4 -0
  18. package/dist/client/client.d.ts.map +1 -1
  19. package/dist/client/client.js +3 -0
  20. package/dist/client/client.js.map +1 -1
  21. package/dist/connNext/baseConnNext.d.ts +20 -0
  22. package/dist/connNext/baseConnNext.d.ts.map +1 -0
  23. package/dist/connNext/baseConnNext.js +95 -0
  24. package/dist/connNext/baseConnNext.js.map +1 -0
  25. package/dist/dev_utils/generateCert.d.ts +5 -4
  26. package/dist/dev_utils/generateCert.d.ts.map +1 -1
  27. package/dist/dev_utils/generateCert.js +230 -65
  28. package/dist/dev_utils/generateCert.js.map +1 -1
  29. package/dist/examples/mqtt+mqtts.test.js +1 -1
  30. package/dist/examples/mqtt+mqtts.test.js.map +1 -1
  31. package/dist/examples/mqtts.test.js +1 -1
  32. package/dist/examples/mqtts.test.js.map +1 -1
  33. package/dist/node/nodeSocketToNext.d.ts +10 -13
  34. package/dist/node/nodeSocketToNext.d.ts.map +1 -1
  35. package/dist/node/nodeSocketToNext.js +75 -131
  36. package/dist/node/nodeSocketToNext.js.map +1 -1
  37. package/dist/web/webSocketToNext.d.ts +18 -1
  38. package/dist/web/webSocketToNext.d.ts.map +1 -1
  39. package/dist/web/webSocketToNext.js +92 -127
  40. package/dist/web/webSocketToNext.js.map +1 -1
  41. package/dist/web/wsClient.d.ts.map +1 -1
  42. package/dist/web/wsClient.js +4 -3
  43. package/dist/web/wsClient.js.map +1 -1
  44. package/examples/mqtt+mqtts.test.ts +1 -1
  45. package/examples/mqtts.test.ts +1 -1
  46. package/node/nodeSocketToNext.ts +85 -148
  47. package/node/tlsServer.test.ts +1 -1
  48. package/package.json +2 -4
  49. package/web/webSocketToNext.ts +108 -145
  50. package/web/wsClient.ts +10 -3
  51. package/dev_utils/webStreamPair.test.ts +0 -171
@@ -1,95 +1,315 @@
1
- import forge from "node-forge";
2
-
3
1
  /**
4
- * Generates a minimal, in-memory TLS certificate bundle for localhost development.
5
- * Includes Root CA, Server Certificate, and Server Private Key with SAN support (IPv4 & IPv6).
2
+ * Generates a minimal TLS certificate bundle for localhost using Web Crypto API.
3
+ * Uses 0 external dependencies (pure JS ASN.1 DER encoding).
4
+ * Do NOT use this in a production setting
6
5
  */
7
- export function generateLocalhostCerts(): {
6
+ export async function generateLocalhostCerts(): Promise<{
8
7
  caCert: string;
9
8
  key: string;
10
9
  cert: string;
11
10
  fullChain: string;
12
- } {
13
- // Generate the keypairs
14
- // 2048-bit RSA is used here for fast generation during local development/testing
15
- const caKeys = forge.pki.rsa.generateKeyPair(2048);
16
- const serverKeys = forge.pki.rsa.generateKeyPair(2048);
17
-
18
- // Create root CA
19
- const caCert = forge.pki.createCertificate();
20
- caCert.publicKey = caKeys.publicKey;
21
- caCert.serialNumber = "01";
22
- caCert.validity.notBefore = new Date();
23
- caCert.validity.notAfter = new Date();
24
- caCert.validity.notAfter.setFullYear(
25
- caCert.validity.notBefore.getFullYear() + 1,
26
- ); // Valid for 1 year
27
-
28
- // Hardcoded CA Attributes
29
- const caAttrs = [{ name: "commonName", value: "Local Dev Root CA" }];
30
- caCert.setSubject(caAttrs);
31
- caCert.setIssuer(caAttrs); // Self-signed: issuer equals subject
32
-
33
- // Critical for CA: Define basic constraints to flag this certificate as a trusted issuer
34
- caCert.setExtensions([
35
- { name: "basicConstraints", cA: true, critical: true },
36
- {
37
- name: "keyUsage",
38
- keyCertSign: true,
39
- digitalSignature: true,
40
- critical: true,
41
- },
11
+ }> {
12
+ const algorithm: RsaHashedKeyGenParams = {
13
+ name: "RSASSA-PKCS1-v1_5",
14
+ hash: "SHA-256",
15
+ modulusLength: 2048,
16
+ publicExponent: new Uint8Array([1, 0, 1]),
17
+ };
18
+
19
+ // 1. Generate Keypairs using Web Crypto
20
+ const caKeys = await crypto.subtle.generateKey(algorithm, true, [
21
+ "sign",
22
+ "verify",
23
+ ]);
24
+ const serverKeys = await crypto.subtle.generateKey(algorithm, true, [
25
+ "sign",
26
+ "verify",
42
27
  ]);
43
28
 
44
- // Sign the CA certificate using its own private key
45
- caCert.sign(caKeys.privateKey, forge.md.sha256.create());
46
-
47
- // Create server certificate for localhost, valid for a year
48
- const serverCert = forge.pki.createCertificate();
49
- serverCert.publicKey = serverKeys.publicKey;
50
- serverCert.serialNumber = "02"; // Unique serial number within this CA scope
51
- serverCert.validity.notBefore = new Date();
52
- serverCert.validity.notAfter = new Date();
53
- serverCert.validity.notAfter.setFullYear(
54
- serverCert.validity.notBefore.getFullYear() + 1,
55
- );
29
+ const now = new Date();
30
+ const nextYear = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
56
31
 
57
- // Hardcoded Server Attributes
58
- serverCert.setSubject([{ name: "commonName", value: "localhost" }]);
59
- serverCert.setIssuer(caCert.subject.attributes); // Issued by our custom Root CA
60
-
61
- // Critical for Modern TLS: X509v3 Extensions containing Subject Alternative Names (SAN)
62
- // Ensures compatibility with strict TLS parsers (like Deno/rustls, Go, Node.js, and modern browsers)
63
- serverCert.setExtensions([
64
- { name: "basicConstraints", cA: false, critical: true },
65
- {
66
- name: "keyUsage",
67
- digitalSignature: true,
68
- keyEncipherment: true,
69
- critical: true,
70
- },
71
- {
72
- name: "subjectAltName",
73
- altNames: [
74
- { type: 2, value: "localhost" }, // type 2 = DNS
75
- { type: 7, ip: "127.0.0.1" }, // type 7 = IPv4 loopback
76
- { type: 7, ip: "::1" }, // type 7 = IPv6 loopback
77
- ],
78
- },
79
- ]);
32
+ // 2. Generate Root CA Certificate (Self-signed)
33
+ const caCertDer = await createX509Cert({
34
+ serialNumber: 1,
35
+ subjectCN: "Local Dev Root CA",
36
+ issuerCN: "Local Dev Root CA",
37
+ notBefore: now,
38
+ notAfter: nextYear,
39
+ publicKey: caKeys.publicKey,
40
+ signingPrivateKey: caKeys.privateKey,
41
+ isCA: true,
42
+ });
80
43
 
81
- // Sign the server certificate using the Root CA's private key
82
- serverCert.sign(caKeys.privateKey, forge.md.sha256.create());
44
+ // 3. Generate Server Certificate (Signed by Root CA)
45
+ const serverCertDer = await createX509Cert({
46
+ serialNumber: 2,
47
+ subjectCN: "localhost",
48
+ issuerCN: "Local Dev Root CA",
49
+ notBefore: now,
50
+ notAfter: nextYear,
51
+ publicKey: serverKeys.publicKey,
52
+ signingPrivateKey: caKeys.privateKey,
53
+ isCA: false,
54
+ sans: [
55
+ { type: "dns", value: "localhost" },
56
+ { type: "ip", value: "127.0.0.1" },
57
+ { type: "ip", value: "::1" },
58
+ ],
59
+ });
83
60
 
84
- // convert data to PEM
85
- const caCertPem = forge.pki.certificateToPem(caCert);
86
- const serverCertPem = forge.pki.certificateToPem(serverCert);
87
- const serverKeyPem = forge.pki.privateKeyToPem(serverKeys.privateKey);
61
+ // Export Server Private Key to PKCS#8 PEM
62
+ const exportedPrivateKey = await crypto.subtle.exportKey(
63
+ "pkcs8",
64
+ serverKeys.privateKey,
65
+ );
66
+ const serverKeyPem = pemEncode(exportedPrivateKey, "PRIVATE KEY");
67
+ const caCertPem = pemEncode(caCertDer, "CERTIFICATE");
68
+ const serverCertPem = pemEncode(serverCertDer, "CERTIFICATE");
88
69
 
89
70
  return {
90
71
  caCert: caCertPem,
91
72
  key: serverKeyPem,
92
73
  cert: serverCertPem,
93
- fullChain: `${serverCertPem}\n${caCertPem}`, // Full certificate chain (Server + CA)
74
+ fullChain: `${serverCertPem}\n${caCertPem}`,
94
75
  };
95
76
  }
77
+
78
+ /* ====================================================================
79
+ Pure ASN.1 / DER Encoders (Zero-Dependency X.509 Construction)
80
+ ==================================================================== */
81
+
82
+ interface CertOptions {
83
+ serialNumber: number;
84
+ subjectCN: string;
85
+ issuerCN: string;
86
+ notBefore: Date;
87
+ notAfter: Date;
88
+ publicKey: CryptoKey;
89
+ signingPrivateKey: CryptoKey;
90
+ isCA: boolean;
91
+ sans?: Array<{ type: "dns" | "ip"; value: string }>;
92
+ }
93
+
94
+ async function createX509Cert(opts: CertOptions): Promise<ArrayBuffer> {
95
+ const spkiBuffer = await crypto.subtle.exportKey("spki", opts.publicKey);
96
+
97
+ // Building Extensions
98
+ const extensionsList: Uint8Array[] = [];
99
+
100
+ // 1. Basic Constraints
101
+ const basicConstraintsValue = derSequence([
102
+ ...(opts.isCA ? [derBoolean(true)] : []),
103
+ ]);
104
+ extensionsList.push(
105
+ derSequence([
106
+ derOid("2.5.29.19"), // id-ce-basicConstraints
107
+ derBoolean(true), // Critical
108
+ derOctetString(basicConstraintsValue),
109
+ ]),
110
+ );
111
+
112
+ // 2. Key Usage (Fixed BitString unused bits padding)
113
+ if (opts.isCA) {
114
+ // keyCertSign (bit 5) | digitalSignature (bit 0) -> 0x84, 1 unused bit in last byte (0x01)
115
+ extensionsList.push(
116
+ derSequence([
117
+ derOid("2.5.29.15"),
118
+ derBoolean(true),
119
+ derOctetString(derBitString(new Uint8Array([0x84]), 1)),
120
+ ]),
121
+ );
122
+ } else {
123
+ // digitalSignature (bit 0) | keyEncipherment (bit 2) -> 0xa0, 5 unused bits (0x05)
124
+ extensionsList.push(
125
+ derSequence([
126
+ derOid("2.5.29.15"),
127
+ derBoolean(true),
128
+ derOctetString(derBitString(new Uint8Array([0xa0]), 5)),
129
+ ]),
130
+ );
131
+ }
132
+
133
+ // 3. Extended Key Usage (Required for TLS Server Authentication)
134
+ if (!opts.isCA) {
135
+ const ekuValue = derSequence([
136
+ derOid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth
137
+ ]);
138
+ extensionsList.push(
139
+ derSequence([
140
+ derOid("2.5.29.37"), // id-ce-extKeyUsage
141
+ derOctetString(ekuValue),
142
+ ]),
143
+ );
144
+ }
145
+
146
+ // 4. Subject Alternative Names (SAN)
147
+ if (opts.sans && opts.sans.length > 0) {
148
+ const sanEntries: Uint8Array[] = opts.sans.map((san) => {
149
+ if (san.type === "dns") {
150
+ return derTag(0x82, new TextEncoder().encode(san.value)); // dNSName [2]
151
+ } else {
152
+ const ipParts = san.value.includes(":")
153
+ ? parseIPv6(san.value)
154
+ : san.value.split(".").map((v) => parseInt(v, 10));
155
+ return derTag(0x87, new Uint8Array(ipParts)); // iPAddress [7]
156
+ }
157
+ });
158
+
159
+ const sanValue = derSequence(sanEntries);
160
+ extensionsList.push(
161
+ derSequence([
162
+ derOid("2.5.29.17"), // id-ce-subjectAltName
163
+ derOctetString(sanValue),
164
+ ]),
165
+ );
166
+ }
167
+
168
+ const extensionsContainer = derTag(0xa3, derSequence(extensionsList));
169
+
170
+ // TBSCertificate (To Be Signed)
171
+ const tbsCert = derSequence([
172
+ derTag(0xa0, derInteger(2)), // Version v3 (2)
173
+ derInteger(opts.serialNumber),
174
+ derSequence([derOid("1.2.840.113549.1.1.11")]), // sha256WithRSAEncryption
175
+ derSequence([
176
+ derSet([
177
+ derSequence([derOid("2.5.4.3"), derPrintableString(opts.issuerCN)]),
178
+ ]),
179
+ ]),
180
+ derSequence([derUTCTime(opts.notBefore), derUTCTime(opts.notAfter)]),
181
+ derSequence([
182
+ derSet([
183
+ derSequence([derOid("2.5.4.3"), derPrintableString(opts.subjectCN)]),
184
+ ]),
185
+ ]),
186
+ new Uint8Array(spkiBuffer),
187
+ extensionsContainer,
188
+ ]);
189
+
190
+ // Sign TBSCertificate
191
+ const tbsBuffer = tbsCert.buffer.slice(
192
+ tbsCert.byteOffset,
193
+ tbsCert.byteOffset + tbsCert.byteLength,
194
+ );
195
+
196
+ const signature = await crypto.subtle.sign(
197
+ { name: "RSASSA-PKCS1-v1_5" },
198
+ opts.signingPrivateKey,
199
+ tbsBuffer as unknown as ArrayBuffer,
200
+ );
201
+
202
+ // Final X.509 Certificate
203
+ const finalSequence = derSequence([
204
+ tbsCert,
205
+ derSequence([derOid("1.2.840.113549.1.1.11")]),
206
+ derBitString(new Uint8Array(signature), 0),
207
+ ]);
208
+
209
+ return finalSequence.buffer.slice(
210
+ finalSequence.byteOffset,
211
+ finalSequence.byteOffset + finalSequence.byteLength,
212
+ ) as unknown as ArrayBuffer;
213
+ }
214
+
215
+ /* --- DER Primitive Encoding Helpers --- */
216
+
217
+ function concatBuffers(bufs: Uint8Array[]): Uint8Array {
218
+ const total = bufs.reduce((sum, b) => sum + b.length, 0);
219
+ const res = new Uint8Array(new ArrayBuffer(total));
220
+ let offset = 0;
221
+ for (const b of bufs) {
222
+ res.set(b, offset);
223
+ offset += b.length;
224
+ }
225
+ return res;
226
+ }
227
+
228
+ function derTag(tag: number, payload: Uint8Array): Uint8Array {
229
+ const len = payload.length;
230
+ let lenBytes: Uint8Array;
231
+ if (len < 128) {
232
+ lenBytes = new Uint8Array([len]);
233
+ } else if (len < 256) {
234
+ lenBytes = new Uint8Array([0x81, len]);
235
+ } else {
236
+ lenBytes = new Uint8Array([0x82, (len >> 8) & 0xff, len & 0xff]);
237
+ }
238
+ return concatBuffers([new Uint8Array([tag]), lenBytes, payload]);
239
+ }
240
+
241
+ function derSequence(items: Uint8Array[]): Uint8Array {
242
+ return derTag(0x30, concatBuffers(items));
243
+ }
244
+
245
+ function derSet(items: Uint8Array[]): Uint8Array {
246
+ return derTag(0x31, concatBuffers(items));
247
+ }
248
+
249
+ function derBoolean(val: boolean): Uint8Array {
250
+ return derTag(0x01, new Uint8Array([val ? 0xff : 0x00]));
251
+ }
252
+
253
+ function derInteger(val: number): Uint8Array {
254
+ return derTag(0x02, new Uint8Array([val]));
255
+ }
256
+
257
+ function derOctetString(val: Uint8Array): Uint8Array {
258
+ return derTag(0x04, val);
259
+ }
260
+
261
+ function derPrintableString(val: string): Uint8Array {
262
+ return derTag(0x13, new TextEncoder().encode(val));
263
+ }
264
+
265
+ function derOid(oidStr: string): Uint8Array {
266
+ const parts = oidStr.split(".").map((v) => parseInt(v, 10));
267
+ const bytes = [parts[0]! * 40 + parts[1]!];
268
+ for (let i = 2; i < parts.length; i++) {
269
+ let v = parts[i] || 0;
270
+ const stack: number[] = [];
271
+ stack.push(v & 0x7f);
272
+ while ((v >>= 7) > 0) {
273
+ stack.push((v & 0x7f) | 0x80);
274
+ }
275
+ bytes.push(...stack.reverse());
276
+ }
277
+ return derTag(0x06, new Uint8Array(bytes));
278
+ }
279
+
280
+ function derBitString(val: Uint8Array, unusedBits = 0): Uint8Array {
281
+ return derTag(0x03, concatBuffers([new Uint8Array([unusedBits]), val]));
282
+ }
283
+
284
+ function derUTCTime(date: Date): Uint8Array {
285
+ const pad = (n: number) => n.toString().padStart(2, "0");
286
+ const str = date.getUTCFullYear().toString().slice(-2) +
287
+ pad(date.getUTCMonth() + 1) +
288
+ pad(date.getUTCDate()) +
289
+ pad(date.getUTCHours()) +
290
+ pad(date.getUTCMinutes()) +
291
+ pad(date.getUTCSeconds()) +
292
+ "Z";
293
+ return derTag(0x17, new TextEncoder().encode(str));
294
+ }
295
+
296
+ function parseIPv6(ip: string): number[] {
297
+ const parts = ip.split("::");
298
+ const head = parts[0] ? parts[0].split(":").map((h) => parseInt(h, 16)) : [];
299
+ const tail = parts[1] ? parts[1].split(":").map((h) => parseInt(h, 16)) : [];
300
+ const missing = 8 - (head.length + tail.length);
301
+ const words = [...head, ...Array(missing).fill(0), ...tail];
302
+ const bytes: number[] = [];
303
+ for (const w of words) {
304
+ bytes.push((w >> 8) & 0xff, w & 0xff);
305
+ }
306
+ return bytes;
307
+ }
308
+
309
+ function pemEncode(buffer: ArrayBuffer, label: string): string {
310
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
311
+ const lines = base64.match(/.{1,64}/g) || [];
312
+ return `-----BEGIN ${label}-----\n${
313
+ lines.join("\n")
314
+ }\n-----END ${label}-----`;
315
+ }
@@ -1,4 +1,6 @@
1
1
  import type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
2
+ import { createAsyncStreamPair } from "./asyncStreamPair.ts";
3
+ import { AsyncStreamConnNext } from "./asyncStreamToNext.ts";
2
4
 
3
5
  /**
4
6
  * Creates an in-memory mock ConnNext object for unit testing.
@@ -12,21 +14,8 @@ export function createMockConn(
12
14
  writerBuf: Uint8Array,
13
15
  close = () => {},
14
16
  ): ConnNext {
15
- let readIndex = 0;
16
- let readOffset = 0;
17
- let writerPos = 0;
18
- let isClosed = false;
19
-
20
- // Helper function to calculate total remaining bytes across readerBuffs
21
- const getRemainingBytes = (): number => {
22
- let total = 0;
23
- for (let i = readIndex; i < readerBuffs.length; i++) {
24
- total += i === readIndex
25
- ? readerBuffs[i].byteLength - readOffset
26
- : readerBuffs[i].byteLength;
27
- }
28
- return total;
29
- };
17
+ // Maak een async stream pair aan
18
+ const { input, output } = createAsyncStreamPair();
30
19
 
31
20
  const mockAddr: ConnAddr = {
32
21
  transport: "tcp",
@@ -34,89 +23,44 @@ export function createMockConn(
34
23
  port: 1883,
35
24
  };
36
25
 
37
- return {
38
- remoteAddr: mockAddr,
39
- localAddr: mockAddr,
40
-
41
- /**
42
- * Read exactly 'length' bytes from the provided readerBuffs
43
- */
44
- read(length?: number): Promise<Uint8Array | null> {
45
- if (isClosed) return Promise.resolve(null);
46
-
47
- // No specific length requested: return the next available chunk immediately
48
- if (length === undefined) {
49
- if (readIndex >= readerBuffs.length) Promise.resolve(null);
50
- const chunk = readerBuffs[readIndex].subarray(readOffset);
51
- readIndex++;
52
- readOffset = 0;
53
- Promise.resolve(chunk);
54
- }
55
-
56
- length = length ?? 0;
57
-
58
- // EOF reached / not enough remaining bytes in the mock buffers
59
- if (getRemainingBytes() < length) {
60
- return Promise.resolve(null);
61
- }
62
-
63
- const result = new Uint8Array(length);
64
- let bytesCopied = 0;
65
-
66
- while (bytesCopied < length && readIndex < readerBuffs.length) {
67
- const current = readerBuffs[readIndex];
68
- const available = current.byteLength - readOffset;
69
- const needed = length - bytesCopied;
70
-
71
- if (available <= needed) {
72
- result.set(current.subarray(readOffset), bytesCopied);
73
- bytesCopied += available;
74
- readIndex++;
75
- readOffset = 0;
76
- } else {
77
- result.set(
78
- current.subarray(readOffset, readOffset + needed),
79
- bytesCopied,
80
- );
81
- bytesCopied += needed;
82
- readOffset += needed;
83
- }
26
+ // Push all input directly to the input of the StreamPair
27
+ (async () => {
28
+ try {
29
+ for (const buf of readerBuffs) {
30
+ await input.write(buf);
84
31
  }
32
+ input.close();
33
+ } catch {
34
+ // ignore errors
35
+ }
36
+ })();
85
37
 
86
- return Promise.resolve(result);
87
- },
88
-
89
- /**
90
- * Write data directly at the current offset in writerBuf
91
- */
92
- write(data: Uint8Array): Promise<void> {
93
- if (isClosed) {
94
- throw new Error("Connection closed");
38
+ // Capture written data and store it in the write buffer
39
+ (async () => {
40
+ try {
41
+ let writerPos = 0;
42
+ for await (const chunk of input.readable) {
43
+ writerBuf.set(chunk, writerPos);
44
+ writerPos += chunk.byteLength;
95
45
  }
96
- writerBuf.set(data, writerPos);
97
- writerPos += data.byteLength;
98
- return Promise.resolve();
99
- },
46
+ } catch {
47
+ // ignore errors
48
+ }
49
+ })();
100
50
 
101
- /**
102
- * Close the mock connection
103
- */
104
- close(): void {
105
- if (!isClosed) {
106
- isClosed = true;
107
- close();
108
- }
109
- },
51
+ // Return a ConnNext wrapper connected to the output side
52
+ const wrapper = new AsyncStreamConnNext(output);
110
53
 
111
- /**
112
- * Async iterator for streaming test scenarios
113
- */
114
- async *[Symbol.asyncIterator]() {
115
- while (true) {
116
- const chunk = await this.read();
117
- if (chunk === null) break;
118
- yield chunk;
119
- }
54
+ return {
55
+ remoteAddr: mockAddr,
56
+ localAddr: mockAddr,
57
+ read: (length) => wrapper.read(length),
58
+ write: (data) => wrapper.write(data),
59
+ [Symbol.asyncIterator]: () => wrapper[Symbol.asyncIterator](),
60
+ close: () => {
61
+ wrapper.close();
62
+ output.close();
63
+ close();
120
64
  },
121
65
  };
122
66
  }
@@ -1,4 +1,4 @@
1
- import { createConnNextPair } from "./webStreamToNext.ts";
1
+ import { createConnNextPair } from "./asyncStreamPairToNext.ts";
2
2
  import { MqttConn } from "../mqttConn/mqttConn.ts";
3
3
  import { Context, MqttServer } from "../server/mod.ts";
4
4
  import { handlers } from "./test-handlers.ts";
@@ -1,12 +1,8 @@
1
- export interface WebStreamPairEndpoint {
2
- readable: ReadableStream<Uint8Array>;
3
- writable: WritableStream<Uint8Array>;
4
- close: () => void;
5
- }
1
+ import type { WebStreamEndpoint } from "./WebStreamEndpoint.ts";
6
2
 
7
3
  export function createWebStreamPair(): {
8
- input: WebStreamPairEndpoint;
9
- output: WebStreamPairEndpoint;
4
+ input: WebStreamEndpoint;
5
+ output: WebStreamEndpoint;
10
6
  } {
11
7
  let aController: ReadableStreamDefaultController | null = null;
12
8
  let bController: ReadableStreamDefaultController | null = null;
@@ -0,0 +1,48 @@
1
+ import type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
2
+ import { createWebStreamPair } from "./webStreamPair.ts";
3
+ import { webStreamToNext } from "./webStreamToNext.ts";
4
+
5
+ /**
6
+ * Creates a pair of cross-connected ConnNext sockets backed by WebStreams.
7
+ */
8
+ export function createConnNextPair(): {
9
+ input: ConnNext;
10
+ output: ConnNext;
11
+ } {
12
+ const { input: webInput, output: webOutput } = createWebStreamPair();
13
+
14
+ const mockAddr: ConnAddr = {
15
+ transport: "tcp",
16
+ hostname: "127.0.0.1",
17
+ port: 1883,
18
+ };
19
+
20
+ const inputWrapper = webStreamToNext(webInput);
21
+ const outputWrapper = webStreamToNext(webOutput);
22
+
23
+ const input: ConnNext = {
24
+ remoteAddr: mockAddr,
25
+ localAddr: mockAddr,
26
+ read: (length) => inputWrapper.read(length),
27
+ write: (data) => inputWrapper.write(data),
28
+ [Symbol.asyncIterator]: () => inputWrapper[Symbol.asyncIterator](),
29
+ close: () => {
30
+ inputWrapper.close();
31
+ outputWrapper.close();
32
+ },
33
+ };
34
+
35
+ const output: ConnNext = {
36
+ remoteAddr: mockAddr,
37
+ localAddr: mockAddr,
38
+ read: (length) => outputWrapper.read(length),
39
+ write: (data) => outputWrapper.write(data),
40
+ [Symbol.asyncIterator]: () => outputWrapper[Symbol.asyncIterator](),
41
+ close: () => {
42
+ outputWrapper.close();
43
+ inputWrapper.close();
44
+ },
45
+ };
46
+
47
+ return { input, output };
48
+ }