@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.
- package/README.md +7 -6
- package/bin/mqtt.ts +1 -1
- package/client/client.ts +62 -7
- package/client/context.ts +2 -2
- package/client/deps.ts +3 -3
- package/client/mod.ts +2 -2
- package/connNext/baseConnNext.ts +123 -0
- package/connNext/connNext.ts +44 -0
- package/deno/README.md +1 -1
- package/deno/denoConnToNext.ts +104 -0
- package/deno/tcpClient.ts +8 -24
- package/deno/tcpServer.test.ts +14 -103
- package/deno/tcpServer.ts +14 -6
- package/deno/tlsServer.test.ts +14 -58
- package/deno/tlsServer.ts +14 -6
- package/deno/wsServer.test.ts +15 -61
- package/deno/wsServer.ts +82 -32
- package/dev_utils/WebStreamEndpoint.ts +5 -0
- package/dev_utils/asyncStreamEndpoint.ts +5 -0
- package/dev_utils/asyncStreamPair.ts +109 -0
- package/dev_utils/asyncStreamPairToNext.ts +45 -0
- package/dev_utils/asyncStreamToNext.ts +64 -0
- package/dev_utils/connNextPair.test.ts +179 -0
- package/dev_utils/mockConn.ts +59 -29
- package/dev_utils/mockServer.ts +3 -4
- package/dev_utils/mod.ts +1 -2
- package/dev_utils/testClient.ts +64 -0
- package/dev_utils/webStreamPair.ts +3 -7
- package/dev_utils/webStreamPairToNext.ts +48 -0
- package/dev_utils/webStreamToNext.ts +54 -0
- package/dist/bin/mqtt.js +1 -1
- package/dist/bin/mqtt.js.map +1 -1
- package/dist/client/client.d.ts +24 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +45 -5
- package/dist/client/client.js.map +1 -1
- package/dist/client/context.d.ts +2 -2
- package/dist/client/context.d.ts.map +1 -1
- package/dist/client/deps.d.ts +1 -1
- package/dist/client/deps.d.ts.map +1 -1
- package/dist/client/mod.d.ts +1 -1
- package/dist/client/mod.d.ts.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/connNext/connNext.d.ts +43 -0
- package/dist/connNext/connNext.d.ts.map +1 -0
- package/dist/connNext/connNext.js +2 -0
- package/dist/connNext/connNext.js.map +1 -0
- package/dist/examples/authHandler.test.js +2 -2
- package/dist/examples/authHandler.test.js.map +1 -1
- package/dist/examples/mqtt+mqtts.test.d.ts +2 -0
- package/dist/examples/mqtt+mqtts.test.d.ts.map +1 -0
- package/dist/examples/mqtt+mqtts.test.js +104 -0
- package/dist/examples/mqtt+mqtts.test.js.map +1 -0
- package/dist/examples/mqtt.test.js +2 -2
- package/dist/examples/mqtt.test.js.map +1 -1
- package/dist/examples/mqtts.test.js +2 -2
- package/dist/examples/mqtts.test.js.map +1 -1
- package/dist/mqttConn/mqttConn.d.ts +5 -6
- package/dist/mqttConn/mqttConn.d.ts.map +1 -1
- package/dist/mqttConn/mqttConn.js +2 -3
- package/dist/mqttConn/mqttConn.js.map +1 -1
- package/dist/node/nodeSocketToNext.d.ts +17 -0
- package/dist/node/nodeSocketToNext.d.ts.map +1 -0
- package/dist/node/nodeSocketToNext.js +105 -0
- package/dist/node/nodeSocketToNext.js.map +1 -0
- package/dist/node/tcpClient.d.ts +4 -6
- package/dist/node/tcpClient.d.ts.map +1 -1
- package/dist/node/tcpClient.js +6 -21
- package/dist/node/tcpClient.js.map +1 -1
- package/dist/node/tcpServer.d.ts +8 -4
- package/dist/node/tcpServer.d.ts.map +1 -1
- package/dist/node/tcpServer.js +14 -6
- package/dist/node/tcpServer.js.map +1 -1
- package/dist/node/tlsServer.d.ts +7 -3
- package/dist/node/tlsServer.d.ts.map +1 -1
- package/dist/node/tlsServer.js +13 -5
- package/dist/node/tlsServer.js.map +1 -1
- package/dist/server/context.d.ts +5 -5
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +4 -1
- package/dist/server/context.js.map +1 -1
- package/dist/server/deps.d.ts +1 -1
- package/dist/server/deps.d.ts.map +1 -1
- package/dist/server/mod.d.ts +1 -1
- package/dist/server/mod.d.ts.map +1 -1
- package/dist/server/server.d.ts +3 -3
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +3 -3
- package/dist/utils/utils.d.ts +1 -2
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +3 -2
- package/dist/utils/utils.js.map +1 -1
- package/dist/web/webSocketToNext.d.ts +20 -0
- package/dist/web/webSocketToNext.d.ts.map +1 -0
- package/dist/web/webSocketToNext.js +125 -0
- package/dist/web/webSocketToNext.js.map +1 -0
- package/dist/web/wsClient.d.ts +3 -3
- package/dist/web/wsClient.d.ts.map +1 -1
- package/dist/web/wsClient.js +22 -7
- package/dist/web/wsClient.js.map +1 -1
- package/dist/web/wsClient.test.js +15 -0
- package/dist/web/wsClient.test.js.map +1 -1
- package/examples/README.md +21 -0
- package/examples/authHandler.test.ts +2 -2
- package/examples/mqtt+mqtts.test.ts +122 -0
- package/examples/mqtt.test.ts +2 -2
- package/examples/mqtts.test.ts +2 -2
- package/examples/webSocket.test.ts +10 -64
- package/examples/webSocketServer.ts +98 -0
- package/mqttConn/mqttConn.test.ts +6 -6
- package/mqttConn/mqttConn.ts +8 -9
- package/node/README.md +1 -1
- package/node/nodeSocketToNext.ts +124 -0
- package/node/tcpClient.ts +13 -27
- package/node/tcpServer.test.ts +12 -58
- package/node/tcpServer.ts +18 -8
- package/node/tlsServer.test.ts +11 -58
- package/node/tlsServer.ts +17 -7
- package/package.json +4 -3
- package/server/context.ts +9 -6
- package/server/deps.ts +1 -1
- package/server/mod.ts +1 -1
- package/server/server.ts +6 -6
- package/utils/utils.ts +3 -2
- package/web/webSocketToNext.ts +146 -0
- package/web/wsClient.test.ts +22 -0
- package/web/wsClient.ts +29 -9
- package/dev_utils/webStreamPair.test.ts +0 -171
- package/dist/node/wrapNodeSocket.d.ts +0 -12
- package/dist/node/wrapNodeSocket.d.ts.map +0 -1
- package/dist/node/wrapNodeSocket.js +0 -36
- package/dist/node/wrapNodeSocket.js.map +0 -1
- package/dist/socket/socket.d.ts +0 -58
- package/dist/socket/socket.d.ts.map +0 -1
- package/dist/socket/socket.js +0 -99
- package/dist/socket/socket.js.map +0 -1
- package/dist/web/webSocketStream.d.ts +0 -29
- package/dist/web/webSocketStream.d.ts.map +0 -1
- package/dist/web/webSocketStream.js +0 -69
- package/dist/web/webSocketStream.js.map +0 -1
- package/dist/web/wrapWebSocket.d.ts +0 -12
- package/dist/web/wrapWebSocket.d.ts.map +0 -1
- package/dist/web/wrapWebSocket.js +0 -41
- package/dist/web/wrapWebSocket.js.map +0 -1
- package/node/socket-node-stream.test.ts +0 -322
- package/node/socket-node-stream.ts +0 -174
- package/node/wrapNodeSocket.ts +0 -44
- package/socket/socket.test.ts +0 -91
- package/socket/socket.ts +0 -149
- package/web/webSocketStream.ts +0 -97
- package/web/wrapWebSocket.ts +0 -52
package/node/tcpServer.test.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
29
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
package/node/tlsServer.test.ts
CHANGED
|
@@ -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
|
|
7
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
31
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
67
|
+
* @param conn - The connection context.
|
|
68
68
|
* @returns {boolean} false will close the connection
|
|
69
69
|
*/
|
|
70
70
|
preconnect?(
|
|
71
|
-
conn:
|
|
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:
|
|
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:
|
|
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 "${
|
|
685
|
+
} because of "${reason}"`,
|
|
683
686
|
);
|
|
684
687
|
if (!this.mqttConn.isClosed) {
|
|
685
688
|
this.mqttConn.close();
|
package/server/deps.ts
CHANGED
package/server/mod.ts
CHANGED
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,
|
|
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
|
|
12
|
-
* @returns
|
|
11
|
+
* @param _conn - The connection.
|
|
12
|
+
* @returns Always returns true
|
|
13
13
|
*/
|
|
14
14
|
const defaultPreconnect = (
|
|
15
|
-
_conn:
|
|
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
|
|
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:
|
|
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
|
-
|
|
7
|
-
|
|
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
|
+
}
|
package/web/wsClient.test.ts
CHANGED
|
@@ -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 {
|
|
10
|
-
import {
|
|
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
|
|
19
|
-
private async establishSocket(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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<
|
|
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(
|
|
50
|
+
return this.establishSocket(
|
|
51
|
+
`${protocol}//${hostname}:${port}${path}`,
|
|
52
|
+
maxBufferSize,
|
|
53
|
+
);
|
|
34
54
|
}
|
|
35
55
|
throw new Error(`Unsupported protocol: ${protocol}`);
|
|
36
56
|
}
|