@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.
- package/client/client.ts +5 -0
- package/connNext/baseConnNext.ts +123 -0
- package/deno/tlsServer.test.ts +1 -1
- package/deno/wsServer.ts +11 -3
- package/dev_utils/WebStreamEndpoint.ts +5 -0
- package/dev_utils/asyncStreamEndpoint.ts +5 -0
- package/dev_utils/asyncStreamPair.ts +4 -9
- package/dev_utils/asyncStreamPairToNext.ts +45 -0
- package/dev_utils/asyncStreamToNext.ts +30 -137
- package/dev_utils/connNextPair.test.ts +2 -2
- package/dev_utils/generateCert.ts +297 -77
- package/dev_utils/mockConn.ts +37 -93
- package/dev_utils/mockServer.ts +1 -1
- package/dev_utils/webStreamPair.ts +3 -7
- package/dev_utils/webStreamPairToNext.ts +48 -0
- package/dev_utils/webStreamToNext.ts +28 -161
- package/dist/client/client.d.ts +4 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +3 -0
- package/dist/client/client.js.map +1 -1
- package/dist/connNext/baseConnNext.d.ts +20 -0
- package/dist/connNext/baseConnNext.d.ts.map +1 -0
- package/dist/connNext/baseConnNext.js +95 -0
- package/dist/connNext/baseConnNext.js.map +1 -0
- package/dist/dev_utils/generateCert.d.ts +5 -4
- package/dist/dev_utils/generateCert.d.ts.map +1 -1
- package/dist/dev_utils/generateCert.js +230 -65
- package/dist/dev_utils/generateCert.js.map +1 -1
- package/dist/examples/mqtt+mqtts.test.js +1 -1
- package/dist/examples/mqtt+mqtts.test.js.map +1 -1
- package/dist/examples/mqtts.test.js +1 -1
- package/dist/examples/mqtts.test.js.map +1 -1
- package/dist/node/nodeSocketToNext.d.ts +10 -13
- package/dist/node/nodeSocketToNext.d.ts.map +1 -1
- package/dist/node/nodeSocketToNext.js +75 -131
- package/dist/node/nodeSocketToNext.js.map +1 -1
- package/dist/web/webSocketToNext.d.ts +18 -1
- package/dist/web/webSocketToNext.d.ts.map +1 -1
- package/dist/web/webSocketToNext.js +92 -127
- package/dist/web/webSocketToNext.js.map +1 -1
- package/dist/web/wsClient.d.ts.map +1 -1
- package/dist/web/wsClient.js +4 -3
- package/dist/web/wsClient.js.map +1 -1
- package/examples/mqtt+mqtts.test.ts +1 -1
- package/examples/mqtts.test.ts +1 -1
- package/node/nodeSocketToNext.ts +85 -148
- package/node/tlsServer.test.ts +1 -1
- package/package.json +2 -4
- package/web/webSocketToNext.ts +108 -145
- package/web/wsClient.ts +10 -3
- package/dev_utils/webStreamPair.test.ts +0 -171
package/examples/mqtts.test.ts
CHANGED
|
@@ -13,7 +13,7 @@ test("Test pubSub using TLS client and server and sqlitePersistence", async func
|
|
|
13
13
|
// the localhost certs generate here are great for testing but lack securiity
|
|
14
14
|
// use something like LetsEncrypt for any serious server
|
|
15
15
|
// generateLocalhostCerts is a dev tool and relies on node-forge as a (dev)dependency
|
|
16
|
-
const { key, cert, caCert } = generateLocalhostCerts();
|
|
16
|
+
const { key, cert, caCert } = await generateLocalhostCerts();
|
|
17
17
|
const dbFile = ":memory:";
|
|
18
18
|
// .start() combines instance creation and async initialization
|
|
19
19
|
const persistence = await SqlitePersistence.start(dbFile);
|
package/node/nodeSocketToNext.ts
CHANGED
|
@@ -1,187 +1,124 @@
|
|
|
1
1
|
import type { Socket } from "node:net";
|
|
2
|
-
import type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
|
|
3
|
-
|
|
4
|
-
/** wrapper class */
|
|
5
|
-
export class NodeSocketConnNextWrapper {
|
|
6
|
-
private endPoint: Socket;
|
|
7
|
-
private closed: boolean;
|
|
8
|
-
private leftover: Uint8Array = new Uint8Array(0);
|
|
9
|
-
|
|
10
|
-
// Store bound event listeners so we can cleanly remove them later
|
|
11
|
-
private onClose = () => {
|
|
12
|
-
this.closed = true;
|
|
13
|
-
};
|
|
14
|
-
private onError = () => {
|
|
15
|
-
this.closed = true;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/** Create a new ConnNext object from a Webstream */
|
|
19
|
-
constructor(endPoint: Socket) {
|
|
20
|
-
this.endPoint = endPoint;
|
|
21
|
-
this.closed = false;
|
|
22
|
-
this.endPoint;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async read(length?: number): Promise<Uint8Array | null> {
|
|
26
|
-
if (this.closed || (length ?? 0) < 0) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
2
|
+
import type { ConnAddr, ConnNext } from "../connNext/connNext.ts";
|
|
3
|
+
import { BaseConnNext } from "../connNext/baseConnNext.ts";
|
|
29
4
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}
|
|
5
|
+
export class NodeSocketConnNext extends BaseConnNext {
|
|
6
|
+
private socket: Socket;
|
|
39
7
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
8
|
+
constructor(socket: Socket) {
|
|
9
|
+
super();
|
|
10
|
+
this.socket = socket;
|
|
43
11
|
|
|
44
|
-
const
|
|
45
|
-
|
|
12
|
+
const markClosed = () => {
|
|
13
|
+
this.closed = true;
|
|
14
|
+
};
|
|
46
15
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
bytesAccumulated += take;
|
|
52
|
-
this.leftover = this.leftover.subarray(take);
|
|
53
|
-
}
|
|
16
|
+
this.socket.on("end", markClosed);
|
|
17
|
+
this.socket.on("close", markClosed);
|
|
18
|
+
this.socket.on("error", markClosed);
|
|
19
|
+
}
|
|
54
20
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
21
|
+
get localAddr(): ConnAddr | undefined {
|
|
22
|
+
return this.socket.localAddress
|
|
23
|
+
? {
|
|
24
|
+
transport: "tcp",
|
|
25
|
+
hostname: this.socket.localAddress,
|
|
26
|
+
port: this.socket.localPort ?? 0,
|
|
59
27
|
}
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
60
30
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.endPoint.off("error", onEvent);
|
|
68
|
-
this.endPoint.off("close", onEvent);
|
|
69
|
-
};
|
|
70
|
-
const onEvent = () => {
|
|
71
|
-
cleanup();
|
|
72
|
-
resolve();
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
this.endPoint.on("readable", onEvent);
|
|
76
|
-
this.endPoint.on("end", onEvent);
|
|
77
|
-
this.endPoint.on("error", onEvent);
|
|
78
|
-
this.endPoint.on("close", onEvent);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
continue;
|
|
31
|
+
get remoteAddr(): ConnAddr | undefined {
|
|
32
|
+
return this.socket.remoteAddress
|
|
33
|
+
? {
|
|
34
|
+
transport: "tcp",
|
|
35
|
+
hostname: this.socket.remoteAddress,
|
|
36
|
+
port: this.socket.remotePort ?? 0,
|
|
82
37
|
}
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
83
40
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
bytesAccumulated += chunkLength;
|
|
91
|
-
} else {
|
|
92
|
-
result.set(chunk.subarray(0, needed), bytesAccumulated);
|
|
93
|
-
bytesAccumulated += needed;
|
|
94
|
-
this.leftover = chunk.subarray(needed);
|
|
95
|
-
}
|
|
41
|
+
/**
|
|
42
|
+
* Haalt direct op verzoek een chunk op via Node's ingebouwde socket.read() mechanisme.
|
|
43
|
+
*/
|
|
44
|
+
protected pullNextChunk(): Promise<Uint8Array | null> {
|
|
45
|
+
if (this.closed || !this.socket.readable) {
|
|
46
|
+
return Promise.resolve(null);
|
|
96
47
|
}
|
|
97
48
|
|
|
98
|
-
|
|
99
|
-
|
|
49
|
+
// Vraag direct om data uit de Node socket buffer
|
|
50
|
+
const chunk = this.socket.read();
|
|
51
|
+
|
|
52
|
+
if (chunk !== null) {
|
|
53
|
+
return Promise.resolve(
|
|
54
|
+
new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
|
|
55
|
+
);
|
|
100
56
|
}
|
|
101
57
|
|
|
102
|
-
|
|
58
|
+
// Als er nog geen data is ('null'), wachten we op het 'readable' (of 'end'/'close') event van Node
|
|
59
|
+
return new Promise<Uint8Array | null>((resolve) => {
|
|
60
|
+
const cleanup = () => {
|
|
61
|
+
this.socket.off("readable", onReadable);
|
|
62
|
+
this.socket.off("end", onEnd);
|
|
63
|
+
this.socket.off("error", onClose);
|
|
64
|
+
this.socket.off("close", onClose);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const onReadable = () => {
|
|
68
|
+
cleanup();
|
|
69
|
+
// Roep zichzelf direct weer aan om de data op te halen
|
|
70
|
+
resolve(this.pullNextChunk());
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const onEnd = () => {
|
|
74
|
+
cleanup();
|
|
75
|
+
this.closed = true;
|
|
76
|
+
resolve(null);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const onClose = () => {
|
|
80
|
+
cleanup();
|
|
81
|
+
this.closed = true;
|
|
82
|
+
resolve(null);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this.socket.on("readable", onReadable);
|
|
86
|
+
this.socket.on("end", onEnd);
|
|
87
|
+
this.socket.on("error", onClose);
|
|
88
|
+
this.socket.on("close", onClose);
|
|
89
|
+
});
|
|
103
90
|
}
|
|
104
91
|
|
|
105
92
|
write(data: Uint8Array): Promise<void> {
|
|
106
|
-
if (this.
|
|
107
|
-
return Promise.reject(new Error("
|
|
93
|
+
if (this.isClosed() || !this.socket.writable) {
|
|
94
|
+
return Promise.reject(new Error("Socket is closed or not writable"));
|
|
108
95
|
}
|
|
109
96
|
|
|
110
97
|
return new Promise((resolve, reject) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (err) {
|
|
114
|
-
reject(err);
|
|
115
|
-
}
|
|
98
|
+
const canWriteMore = this.socket.write(data, (err) => {
|
|
99
|
+
if (err) reject(err);
|
|
116
100
|
});
|
|
117
101
|
|
|
118
|
-
// 2. Determine when to resolve based on the return value of write()
|
|
119
102
|
if (canWriteMore) {
|
|
120
|
-
// If the buffer is not full, we can resolve immediately
|
|
121
103
|
resolve();
|
|
122
104
|
} else {
|
|
123
|
-
|
|
124
|
-
this.endPoint.once("drain", () => {
|
|
125
|
-
resolve();
|
|
126
|
-
});
|
|
105
|
+
this.socket.once("drain", resolve);
|
|
127
106
|
}
|
|
128
107
|
});
|
|
129
108
|
}
|
|
130
109
|
|
|
131
|
-
close() {
|
|
110
|
+
close(): void {
|
|
132
111
|
if (!this.closed) {
|
|
133
112
|
this.closed = true;
|
|
134
|
-
|
|
135
|
-
// Clean up permanent event listeners to prevent memory leaks
|
|
136
|
-
this.endPoint.off("close", this.onClose);
|
|
137
|
-
this.endPoint.off("error", this.onError);
|
|
138
|
-
|
|
139
|
-
// Safely destroy the stream first, then trigger the closer callback
|
|
140
113
|
try {
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Suppress stream destruction errors
|
|
145
|
-
// deno-coverage-ignore
|
|
114
|
+
this.socket.destroy();
|
|
115
|
+
} catch {
|
|
116
|
+
/* swallow */
|
|
146
117
|
}
|
|
147
118
|
}
|
|
148
119
|
}
|
|
149
|
-
|
|
150
|
-
async *[Symbol.asyncIterator]() {
|
|
151
|
-
while (true) {
|
|
152
|
-
const chunk = await this.read();
|
|
153
|
-
if (chunk === null) {
|
|
154
|
-
break;
|
|
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
|
-
};
|
|
179
|
-
}
|
|
180
|
-
return undefined;
|
|
181
|
-
}
|
|
182
120
|
}
|
|
183
121
|
|
|
184
122
|
export function nodeSocketToNext(socket: Socket): ConnNext {
|
|
185
|
-
|
|
186
|
-
return connNext;
|
|
123
|
+
return new NodeSocketConnNext(socket);
|
|
187
124
|
}
|
package/node/tlsServer.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { MqttServer } from "../server/mod.ts";
|
|
|
9
9
|
|
|
10
10
|
logger.level(LogLevel.info);
|
|
11
11
|
|
|
12
|
-
const { key, cert, caCert } = generateLocalhostCerts();
|
|
12
|
+
const { key, cert, caCert } = await generateLocalhostCerts();
|
|
13
13
|
|
|
14
14
|
test("Test pubSub using client and server", async () => {
|
|
15
15
|
const server = new TlsServer({ port: 0, key, cert }, {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seriousme/opifex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "MQTT client & server for Deno & NodeJS and Bun",
|
|
5
5
|
"author": "Hans Klunder",
|
|
6
6
|
"license": "MIT",
|
|
@@ -100,8 +100,6 @@
|
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@types/node": "^26.6.
|
|
104
|
-
"@types/node-forge": "^1.3.14",
|
|
105
|
-
"node-forge": "^1.4.0"
|
|
103
|
+
"@types/node": "^26.6.5"
|
|
106
104
|
}
|
|
107
105
|
}
|
package/web/webSocketToNext.ts
CHANGED
|
@@ -1,28 +1,77 @@
|
|
|
1
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
|
+
}
|
|
2
35
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
}
|
|
8
43
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
44
|
+
this.messageQueue.push(chunk);
|
|
45
|
+
this.currentBufferSize += chunk.byteLength;
|
|
46
|
+
this.notifyWaiters();
|
|
47
|
+
});
|
|
12
48
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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();
|
|
16
60
|
resolve?.();
|
|
17
61
|
}
|
|
18
|
-
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private waitForData(): Promise<void> {
|
|
65
|
+
return new Promise<void>((resolve) => {
|
|
66
|
+
this.readResolvers.push(resolve);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
19
69
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
70
|
+
ensureOpen(): Promise<void> {
|
|
71
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
23
72
|
return Promise.resolve();
|
|
24
73
|
}
|
|
25
|
-
if (ws.readyState >= WebSocket.CLOSING) {
|
|
74
|
+
if (this.ws.readyState >= WebSocket.CLOSING) {
|
|
26
75
|
return Promise.reject(new Error("WebSocket is closing or closed"));
|
|
27
76
|
}
|
|
28
77
|
return new Promise((resolve, reject) => {
|
|
@@ -39,145 +88,59 @@ export async function webSocketToNext(
|
|
|
39
88
|
reject(new Error("WebSocket closed before opening"));
|
|
40
89
|
};
|
|
41
90
|
const cleanup = () => {
|
|
42
|
-
ws.removeEventListener("open", onOpen);
|
|
43
|
-
ws.removeEventListener("error", onError);
|
|
44
|
-
ws.removeEventListener("close", onClose);
|
|
91
|
+
this.ws.removeEventListener("open", onOpen);
|
|
92
|
+
this.ws.removeEventListener("error", onError);
|
|
93
|
+
this.ws.removeEventListener("close", onClose);
|
|
45
94
|
};
|
|
46
95
|
|
|
47
|
-
ws.addEventListener("open", onOpen);
|
|
48
|
-
ws.addEventListener("error", onError);
|
|
49
|
-
ws.addEventListener("close", onClose);
|
|
96
|
+
this.ws.addEventListener("open", onOpen);
|
|
97
|
+
this.ws.addEventListener("error", onError);
|
|
98
|
+
this.ws.addEventListener("close", onClose);
|
|
50
99
|
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
chunk = new TextEncoder().encode(event.data);
|
|
57
|
-
} else if (event.data instanceof ArrayBuffer) {
|
|
58
|
-
chunk = new Uint8Array(event.data);
|
|
59
|
-
} else {
|
|
60
|
-
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected async pullNextChunk(): Promise<Uint8Array | null> {
|
|
103
|
+
if (this.messageQueue.length === 0) {
|
|
104
|
+
await this.waitForData();
|
|
61
105
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
ws.addEventListener("close", notifyWaiters);
|
|
67
|
-
ws.addEventListener("error", notifyWaiters);
|
|
68
|
-
|
|
69
|
-
const getBufferedBytes = (): number => {
|
|
70
|
-
let total = 0;
|
|
71
|
-
for (let i = 0; i < messageQueue.length; i++) {
|
|
72
|
-
total += i === 0
|
|
73
|
-
? messageQueue[i]!.byteLength - queueOffset
|
|
74
|
-
: messageQueue[i]!.byteLength;
|
|
106
|
+
if (this.closed) {
|
|
107
|
+
return null;
|
|
75
108
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (getBufferedBytes() === 0 && ws.readyState >= WebSocket.CLOSING) {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
while (getBufferedBytes() === 0) {
|
|
101
|
-
if (ws.readyState >= WebSocket.CLOSING) return null;
|
|
102
|
-
await waitForData();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (length === undefined) {
|
|
106
|
-
const firstChunk = messageQueue.shift()!;
|
|
107
|
-
const result = firstChunk.subarray(queueOffset);
|
|
108
|
-
queueOffset = 0;
|
|
109
|
-
return result;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
while (getBufferedBytes() < length) {
|
|
113
|
-
if (ws.readyState >= WebSocket.CLOSING) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
await waitForData();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const result = new Uint8Array(length);
|
|
120
|
-
let bytesCopied = 0;
|
|
121
|
-
|
|
122
|
-
while (bytesCopied < length && messageQueue.length > 0) {
|
|
123
|
-
const currentChunk = messageQueue[0]!;
|
|
124
|
-
const bytesNeeded = length - bytesCopied;
|
|
125
|
-
const bytesAvailable = currentChunk.byteLength - queueOffset;
|
|
126
|
-
|
|
127
|
-
if (bytesAvailable <= bytesNeeded) {
|
|
128
|
-
result.set(currentChunk.subarray(queueOffset), bytesCopied);
|
|
129
|
-
bytesCopied += bytesAvailable;
|
|
130
|
-
messageQueue.shift();
|
|
131
|
-
queueOffset = 0;
|
|
132
|
-
} else {
|
|
133
|
-
result.set(
|
|
134
|
-
currentChunk.subarray(queueOffset, queueOffset + bytesNeeded),
|
|
135
|
-
bytesCopied,
|
|
136
|
-
);
|
|
137
|
-
bytesCopied += bytesNeeded;
|
|
138
|
-
queueOffset += bytesNeeded;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return result;
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
async write(data: Uint8Array): Promise<void> {
|
|
146
|
-
await ensureOpen();
|
|
147
|
-
|
|
148
|
-
if (data instanceof Uint8Array) {
|
|
149
|
-
ws.send(data.buffer as ArrayBuffer);
|
|
150
|
-
} else {
|
|
151
|
-
ws.send(data);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const HIGH_WATER_MARK = 64 * 1024;
|
|
155
|
-
while (ws.bufferedAmount > HIGH_WATER_MARK) {
|
|
156
|
-
if (ws.readyState >= WebSocket.CLOSING) {
|
|
157
|
-
throw new Error("WebSocket closed while writing");
|
|
158
|
-
}
|
|
159
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
160
|
-
}
|
|
161
|
-
},
|
|
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
|
+
}
|
|
162
125
|
|
|
163
|
-
|
|
126
|
+
close(): void {
|
|
127
|
+
if (!this.closed) {
|
|
128
|
+
this.closed = true;
|
|
164
129
|
try {
|
|
165
|
-
ws.close();
|
|
130
|
+
this.ws.close();
|
|
166
131
|
} catch {
|
|
167
132
|
/* swallow */
|
|
168
133
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
async *[Symbol.asyncIterator]() {
|
|
173
|
-
while (true) {
|
|
174
|
-
const chunk = await this.read();
|
|
175
|
-
if (chunk === null) break;
|
|
176
|
-
yield chunk;
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
180
137
|
|
|
181
|
-
|
|
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();
|
|
182
145
|
return connNext;
|
|
183
146
|
}
|
package/web/wsClient.ts
CHANGED
|
@@ -16,7 +16,10 @@ import { webSocketToNext } from "./webSocketToNext.ts";
|
|
|
16
16
|
*/
|
|
17
17
|
export class WsClient extends Client {
|
|
18
18
|
/** create a websocket and wrap it in a ConnNext */
|
|
19
|
-
private async establishSocket(
|
|
19
|
+
private async establishSocket(
|
|
20
|
+
url: string,
|
|
21
|
+
maxBufferSize?: number,
|
|
22
|
+
): Promise<ConnNext> {
|
|
20
23
|
try {
|
|
21
24
|
const urlObj = new URL(url);
|
|
22
25
|
const remoteAddr: NetAddr = {
|
|
@@ -25,7 +28,7 @@ export class WsClient extends Client {
|
|
|
25
28
|
port: Number(urlObj.port || 0),
|
|
26
29
|
};
|
|
27
30
|
const ws = new WebSocket(url);
|
|
28
|
-
const connNext = await webSocketToNext(ws, remoteAddr);
|
|
31
|
+
const connNext = await webSocketToNext(ws, remoteAddr, maxBufferSize);
|
|
29
32
|
return connNext;
|
|
30
33
|
} catch (error) {
|
|
31
34
|
const message = error instanceof ErrorEvent
|
|
@@ -40,10 +43,14 @@ export class WsClient extends Client {
|
|
|
40
43
|
const { protocol, hostname, port: portStr, pathname } = this.connectUrl;
|
|
41
44
|
const port = portStr ? Number(portStr) : (protocol === "wss:" ? 443 : 80);
|
|
42
45
|
const path = pathname || "/mqtt";
|
|
46
|
+
const maxBufferSize = this.maxBufferSize;
|
|
43
47
|
|
|
44
48
|
logger.debug({ hostname, port, path });
|
|
45
49
|
if (protocol === "ws:" || protocol === "wss:") {
|
|
46
|
-
return this.establishSocket(
|
|
50
|
+
return this.establishSocket(
|
|
51
|
+
`${protocol}//${hostname}:${port}${path}`,
|
|
52
|
+
maxBufferSize,
|
|
53
|
+
);
|
|
47
54
|
}
|
|
48
55
|
throw new Error(`Unsupported protocol: ${protocol}`);
|
|
49
56
|
}
|