@seriousme/opifex 1.1.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/.github/workflows/closeStale.yml +15 -0
- package/.github/workflows/codeql-analysis.yml +71 -0
- package/.github/workflows/deno-ci.yml +16 -0
- package/.github/workflows/nodejs-ci.yml +23 -0
- package/.github/workflows/publish.yml +16 -0
- package/.vscode/extensions.json +10 -0
- package/.vscode/settings.json +12 -0
- package/LICENSE.txt +21 -0
- package/README.md +34 -0
- package/bin/conformanceServer.ts +70 -0
- package/bin/demoServer.ts +64 -0
- package/bin/mqtt.ts +211 -0
- package/client/client.ts +181 -0
- package/client/context.ts +197 -0
- package/client/deps.ts +36 -0
- package/client/handlers/handleConnack.ts +21 -0
- package/client/handlers/handlePacket.ts +70 -0
- package/client/handlers/handlePuback.ts +10 -0
- package/client/handlers/handlePubcomp.ts +13 -0
- package/client/handlers/handlePublish.ts +32 -0
- package/client/handlers/handlePubrec.ts +24 -0
- package/client/handlers/handlePubrel.ts +26 -0
- package/client/handlers/handleSuback.ts +14 -0
- package/client/handlers/handleUnsuback.ts +11 -0
- package/client/mod.ts +11 -0
- package/client/store/deps.ts +22 -0
- package/client/store/memoryStore.ts +56 -0
- package/client/store/store.ts +29 -0
- package/deno/README.md +64 -0
- package/deno/client.ts +48 -0
- package/deno/server.test.ts +14 -0
- package/deno/server.ts +31 -0
- package/deno/wrapDenoConn.ts +12 -0
- package/deno.json +8 -0
- package/deno.lock +47 -0
- package/dev_utils/dummyConn.ts +95 -0
- package/dev_utils/mod.ts +1 -0
- package/mqttConn/mqttConn.test.ts +110 -0
- package/mqttConn/mqttConn.ts +134 -0
- package/mqttPacket/AuthenticationResult.ts +16 -0
- package/mqttPacket/BitMask.ts +10 -0
- package/mqttPacket/PacketType.ts +21 -0
- package/mqttPacket/connack.test.ts +129 -0
- package/mqttPacket/connack.ts +37 -0
- package/mqttPacket/connect.test.ts +297 -0
- package/mqttPacket/connect.ts +159 -0
- package/mqttPacket/decoder.test.ts +123 -0
- package/mqttPacket/decoder.ts +109 -0
- package/mqttPacket/disconnect.test.ts +50 -0
- package/mqttPacket/disconnect.ts +21 -0
- package/mqttPacket/encoder.test.ts +39 -0
- package/mqttPacket/encoder.ts +72 -0
- package/mqttPacket/length.test.ts +75 -0
- package/mqttPacket/length.ts +57 -0
- package/mqttPacket/mod.ts +151 -0
- package/mqttPacket/pingreq.test.ts +50 -0
- package/mqttPacket/pingreq.ts +22 -0
- package/mqttPacket/pingres.test.ts +50 -0
- package/mqttPacket/pingres.ts +21 -0
- package/mqttPacket/puback.test.ts +55 -0
- package/mqttPacket/puback.ts +28 -0
- package/mqttPacket/pubcomp.test.ts +55 -0
- package/mqttPacket/pubcomp.ts +28 -0
- package/mqttPacket/publish.test.ts +198 -0
- package/mqttPacket/publish.ts +73 -0
- package/mqttPacket/pubrec.test.ts +55 -0
- package/mqttPacket/pubrec.ts +28 -0
- package/mqttPacket/pubrel.test.ts +55 -0
- package/mqttPacket/pubrel.ts +28 -0
- package/mqttPacket/suback.test.ts +49 -0
- package/mqttPacket/suback.ts +41 -0
- package/mqttPacket/subscribe.test.ts +233 -0
- package/mqttPacket/subscribe.ts +69 -0
- package/mqttPacket/types.ts +27 -0
- package/mqttPacket/unsuback.test.ts +55 -0
- package/mqttPacket/unsuback.ts +28 -0
- package/mqttPacket/unsubscribe.test.ts +136 -0
- package/mqttPacket/unsubscribe.ts +48 -0
- package/mqttPacket/validators.ts +20 -0
- package/node/README.md +46 -0
- package/node/client.ts +58 -0
- package/node/server.test.ts +14 -0
- package/node/server.ts +34 -0
- package/node/wrapNodeSocket.ts +54 -0
- package/package.json +35 -0
- package/persistence/deps.ts +12 -0
- package/persistence/memory/memoryPersistence.test.ts +151 -0
- package/persistence/memory/memoryPersistence.ts +156 -0
- package/persistence/persistence.ts +24 -0
- package/persistence/store.ts +14 -0
- package/server/context.ts +157 -0
- package/server/deps.ts +37 -0
- package/server/handlers/handleConnect.ts +73 -0
- package/server/handlers/handleDisconnect.ts +6 -0
- package/server/handlers/handlePacket.ts +78 -0
- package/server/handlers/handlePingreq.ts +8 -0
- package/server/handlers/handlePuback.ts +12 -0
- package/server/handlers/handlePubcomp.ts +12 -0
- package/server/handlers/handlePublish.ts +47 -0
- package/server/handlers/handlePubrec.ts +20 -0
- package/server/handlers/handlePubrel.ts +24 -0
- package/server/handlers/handleSubscribe.ts +47 -0
- package/server/handlers/handleUnsubscribe.ts +17 -0
- package/server/mod.ts +16 -0
- package/server/server.ts +74 -0
- package/server/test/handleConnect.test.ts +126 -0
- package/server/test/test-handlers.ts +52 -0
- package/socket/socket.ts +59 -0
- package/timer/timer.test.ts +48 -0
- package/timer/timer.ts +45 -0
- package/trie/trie.test.ts +147 -0
- package/trie/trie.ts +106 -0
- package/utils/args.ts +86 -0
- package/utils/assert.ts +31 -0
- package/utils/asyncQueue.ts +63 -0
- package/utils/deferred.ts +12 -0
- package/utils/logger.ts +35 -0
- package/utils/mod.ts +7 -0
- package/utils/nextTick.ts +3 -0
- package/utils/platform.ts +4 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { PacketType } from "./PacketType.ts";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
|
|
5
|
+
import { decode, encode } from "./mod.ts";
|
|
6
|
+
|
|
7
|
+
test("encode Unsubscribe", () => {
|
|
8
|
+
assert.deepStrictEqual(
|
|
9
|
+
encode({
|
|
10
|
+
type: PacketType.unsubscribe,
|
|
11
|
+
id: 1,
|
|
12
|
+
topicFilters: ["a/b", "c/d"],
|
|
13
|
+
}),
|
|
14
|
+
Uint8Array.from([
|
|
15
|
+
// fixedHeader
|
|
16
|
+
0xa2, // packetType + flags
|
|
17
|
+
12, // remainingLength
|
|
18
|
+
// variableHeader
|
|
19
|
+
0, // id MSB
|
|
20
|
+
1, // id LSB
|
|
21
|
+
// payload
|
|
22
|
+
0, // topic filter length MSB
|
|
23
|
+
3, // topic filter length LSB
|
|
24
|
+
97, // 'a'
|
|
25
|
+
47, // '/'
|
|
26
|
+
98, // 'b'
|
|
27
|
+
0, // topic filter length MSB
|
|
28
|
+
3, // topic filter length LSB
|
|
29
|
+
99, // 'c'
|
|
30
|
+
47, // '/'
|
|
31
|
+
100, // 'd'
|
|
32
|
+
]),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("decode Unsubscribe", () => {
|
|
37
|
+
assert.deepStrictEqual(
|
|
38
|
+
decode(
|
|
39
|
+
Uint8Array.from([
|
|
40
|
+
// fixedHeader
|
|
41
|
+
0xa2, // packetType + flags
|
|
42
|
+
12, // remainingLength
|
|
43
|
+
// variableHeader
|
|
44
|
+
0, // id MSB
|
|
45
|
+
1, // id LSB
|
|
46
|
+
// payload
|
|
47
|
+
0, // topic filter length MSB
|
|
48
|
+
3, // topic filter length LSB
|
|
49
|
+
97, // 'a'
|
|
50
|
+
47, // '/'
|
|
51
|
+
98, // 'b'
|
|
52
|
+
0, // topic filter length MSB
|
|
53
|
+
3, // topic filter length LSB
|
|
54
|
+
99, // 'c'
|
|
55
|
+
47, // '/'
|
|
56
|
+
100, // 'd'
|
|
57
|
+
]),
|
|
58
|
+
),
|
|
59
|
+
{
|
|
60
|
+
type: PacketType.unsubscribe,
|
|
61
|
+
id: 1,
|
|
62
|
+
topicFilters: ["a/b", "c/d"],
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("decode Unsubscribe missing bit 1 flag", () => {
|
|
68
|
+
assert.throws(
|
|
69
|
+
() =>
|
|
70
|
+
decode(
|
|
71
|
+
Uint8Array.from([
|
|
72
|
+
// fixedHeader
|
|
73
|
+
0xa0, // packetType + flags
|
|
74
|
+
12, // remainingLength
|
|
75
|
+
// variableHeader
|
|
76
|
+
0, // id MSB
|
|
77
|
+
1, // id LSB
|
|
78
|
+
// payload
|
|
79
|
+
0, // topic filter length MSB
|
|
80
|
+
3, // topic filter length LSB
|
|
81
|
+
97, // 'a'
|
|
82
|
+
47, // '/'
|
|
83
|
+
98, // 'b'
|
|
84
|
+
0, // topic filter length MSB
|
|
85
|
+
3, // topic filter length LSB
|
|
86
|
+
99, // 'c'
|
|
87
|
+
47, // '/'
|
|
88
|
+
100, // 'd'
|
|
89
|
+
]),
|
|
90
|
+
),
|
|
91
|
+
Error,
|
|
92
|
+
"Invalid header",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("decode unsubscribe packet too short", () => {
|
|
97
|
+
assert.throws(
|
|
98
|
+
() =>
|
|
99
|
+
decode(
|
|
100
|
+
Uint8Array.from([
|
|
101
|
+
// fixedHeader
|
|
102
|
+
0xa2, // packetType + flags
|
|
103
|
+
10, // remainingLength
|
|
104
|
+
// variableHeader
|
|
105
|
+
0, // id MSB
|
|
106
|
+
1, // id LSB
|
|
107
|
+
// payload
|
|
108
|
+
0, // topic filter length MSB
|
|
109
|
+
3, // topic filter length LSB
|
|
110
|
+
97, // 'a'
|
|
111
|
+
47, // '/'
|
|
112
|
+
98, // 'b'
|
|
113
|
+
0, // topic filter length MSB
|
|
114
|
+
3, // topic filter length LSB
|
|
115
|
+
99, // 'c'
|
|
116
|
+
]),
|
|
117
|
+
),
|
|
118
|
+
Error,
|
|
119
|
+
"too short",
|
|
120
|
+
);
|
|
121
|
+
assert.throws(
|
|
122
|
+
() =>
|
|
123
|
+
decode(
|
|
124
|
+
Uint8Array.from([
|
|
125
|
+
// fixedHeader
|
|
126
|
+
0xa2, // packetType + flags
|
|
127
|
+
14, // remainingLength
|
|
128
|
+
// variableHeader
|
|
129
|
+
0, // id MSB
|
|
130
|
+
1, // id LSB
|
|
131
|
+
]),
|
|
132
|
+
),
|
|
133
|
+
Error,
|
|
134
|
+
"too short",
|
|
135
|
+
);
|
|
136
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { PacketId, Topic, TopicFilter, TPacketType } from "./types.ts";
|
|
2
|
+
import { PacketType } from "./PacketType.ts";
|
|
3
|
+
import { BitMask } from "./BitMask.ts";
|
|
4
|
+
import { Encoder } from "./encoder.ts";
|
|
5
|
+
import { booleanFlag, Decoder, DecoderError } from "./decoder.ts";
|
|
6
|
+
|
|
7
|
+
export type UnsubscribePacket = {
|
|
8
|
+
type: TPacketType;
|
|
9
|
+
id: PacketId;
|
|
10
|
+
topicFilters: TopicFilter[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
encode(packet: UnsubscribePacket) {
|
|
15
|
+
// Bits 3,2,1 and 0 of the fixed header of the UNSUBSCRIBE Control Packet are reserved and
|
|
16
|
+
// MUST be set to 0,0,1 and 0 respectively. The Server MUST treat any other value as
|
|
17
|
+
// malformed and close the Network Connection [MQTT-3.10.1-1].
|
|
18
|
+
const flags = 0b0010;
|
|
19
|
+
|
|
20
|
+
const encoder = new Encoder();
|
|
21
|
+
encoder.setInt16(packet.id);
|
|
22
|
+
for (const topicFilter of packet.topicFilters) {
|
|
23
|
+
encoder.setTopicFilter(topicFilter);
|
|
24
|
+
}
|
|
25
|
+
return { flags, bytes: encoder.done() };
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
decode(buffer: Uint8Array, flags: number): UnsubscribePacket {
|
|
29
|
+
if (!booleanFlag(flags, BitMask.bit1)) {
|
|
30
|
+
throw new DecoderError("Invalid header");
|
|
31
|
+
}
|
|
32
|
+
const decoder = new Decoder(buffer);
|
|
33
|
+
const id = decoder.getInt16();
|
|
34
|
+
|
|
35
|
+
const topicFilters: Topic[] = [];
|
|
36
|
+
do {
|
|
37
|
+
const topicFilter = decoder.getTopicFilter();
|
|
38
|
+
topicFilters.push(topicFilter);
|
|
39
|
+
} while (!decoder.atEnd());
|
|
40
|
+
decoder.done();
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
type: PacketType.unsubscribe,
|
|
44
|
+
id,
|
|
45
|
+
topicFilters,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Topic, TopicFilter } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
// deno-lint-ignore no-control-regex
|
|
4
|
+
const invalidUTF8regEx = new RegExp(/\x00|\uFFFD/);
|
|
5
|
+
// deno-lint-ignore no-control-regex
|
|
6
|
+
const invalidTopicRegEx = new RegExp(/^$|\+|#|\x00|\uFFFD/);
|
|
7
|
+
// deno-lint-ignore no-control-regex
|
|
8
|
+
const invalidTopicFilterRegEx = new RegExp(/^$|#.|[^\/]\+|\+[^\/]|\x00|\uFFFD/);
|
|
9
|
+
|
|
10
|
+
export function invalidUTF8(value: string): boolean {
|
|
11
|
+
return invalidUTF8regEx.test(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function invalidTopic(value: Topic): boolean {
|
|
15
|
+
return invalidTopicRegEx.test(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function invalidTopicFilter(value: TopicFilter): boolean {
|
|
19
|
+
return invalidTopicFilterRegEx.test(value);
|
|
20
|
+
}
|
package/node/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Running Opifex server/client on NodeJS
|
|
2
|
+
|
|
3
|
+
These instructions specifify how to run [Opifex](README.md) on NodeJS. To run
|
|
4
|
+
Opifex on Deno see the [instructions](../deno/README.md)
|
|
5
|
+
|
|
6
|
+
## Playing around
|
|
7
|
+
|
|
8
|
+
Make sure you have NodeJS with a version > 22.6.0 installed.
|
|
9
|
+
|
|
10
|
+
### Server
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx -p @seriousme/opifex demoserver --help
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Client
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @seriousme/opifex mqtt --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Local deployment
|
|
23
|
+
|
|
24
|
+
You can use:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @seriousme/opifex
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
to install Opifex.
|
|
31
|
+
|
|
32
|
+
Or clone the repository, e.g. using:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/seriousme/opifex.git
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
and then use:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
./bin/demoServer.ts --help
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
./bin/mqtt.ts --help
|
|
46
|
+
```
|
package/node/client.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Client } from "../client/client.ts";
|
|
2
|
+
import { logger } from "../client/deps.ts";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { connect } from "node:net";
|
|
5
|
+
import * as tls from "node:tls";
|
|
6
|
+
import type { SockConn } from "../socket/socket.ts";
|
|
7
|
+
import { wrapNodeSocket } from "./wrapNodeSocket.ts";
|
|
8
|
+
|
|
9
|
+
export async function getCaCerts(filename: string | undefined) {
|
|
10
|
+
if (!filename) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const caCerts = await readFile(filename, { encoding: "utf-8" });
|
|
14
|
+
if (caCerts === "") {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
return [caCerts];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class TcpClient extends Client {
|
|
21
|
+
protected async connectMQTT(hostname: string, port = 1883) {
|
|
22
|
+
logger.debug({ hostname, port });
|
|
23
|
+
return wrapNodeSocket(await connect({ host: hostname, port }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected async connectMQTTS(
|
|
27
|
+
hostname: string,
|
|
28
|
+
port = 8883,
|
|
29
|
+
caCerts?: string[],
|
|
30
|
+
) {
|
|
31
|
+
const opts = {
|
|
32
|
+
host: hostname,
|
|
33
|
+
port,
|
|
34
|
+
secureContext: caCerts
|
|
35
|
+
? tls.createSecureContext({ cert: caCerts })
|
|
36
|
+
: undefined,
|
|
37
|
+
};
|
|
38
|
+
logger.debug({ hostname, port, caCerts });
|
|
39
|
+
return wrapNodeSocket(await tls.connect(opts));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected override createConn(
|
|
43
|
+
protocol: string,
|
|
44
|
+
hostname: string,
|
|
45
|
+
port?: number,
|
|
46
|
+
caCerts?: string[],
|
|
47
|
+
): Promise<SockConn> {
|
|
48
|
+
// if you need to support alternative connection types just
|
|
49
|
+
// overload this method in your subclass
|
|
50
|
+
if (protocol === "mqtts:") {
|
|
51
|
+
return this.connectMQTTS(hostname, port, caCerts);
|
|
52
|
+
}
|
|
53
|
+
if (protocol === "mqtt:") {
|
|
54
|
+
return this.connectMQTT(hostname, port);
|
|
55
|
+
}
|
|
56
|
+
throw `Unsupported protocol: ${protocol}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { TcpServer } from "./server.ts";
|
|
4
|
+
|
|
5
|
+
test("testServer", () => {
|
|
6
|
+
const server = new TcpServer({ port: 0 }, {});
|
|
7
|
+
server.start();
|
|
8
|
+
assert.deepStrictEqual(
|
|
9
|
+
server.port !== undefined,
|
|
10
|
+
true,
|
|
11
|
+
"server runs a a random port",
|
|
12
|
+
);
|
|
13
|
+
server.stop();
|
|
14
|
+
});
|
package/node/server.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Server } from "node:net";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import type { MqttServerOptions } from "../server/mod.ts";
|
|
4
|
+
import { MqttServer } from "../server/mod.ts";
|
|
5
|
+
import { wrapNodeSocket } from "./wrapNodeSocket.ts";
|
|
6
|
+
|
|
7
|
+
type ServerOptions = {
|
|
8
|
+
hostname?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class TcpServer {
|
|
13
|
+
private mqttServer: MqttServer;
|
|
14
|
+
private server?: Server;
|
|
15
|
+
private serverOptions;
|
|
16
|
+
constructor(serverOptions: ServerOptions, mqttOptions: MqttServerOptions) {
|
|
17
|
+
this.mqttServer = new MqttServer(mqttOptions);
|
|
18
|
+
this.serverOptions = serverOptions;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
start() {
|
|
22
|
+
this.server = createServer((sock) =>
|
|
23
|
+
this.mqttServer.serve(wrapNodeSocket(sock))
|
|
24
|
+
);
|
|
25
|
+
this.server.listen(this.serverOptions.port, this.serverOptions.hostname);
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
this.server?.close();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get port() {
|
|
32
|
+
return this.serverOptions.port;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Socket } from "node:net";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
3
|
+
import type { SockConn } from "../socket/socket.ts";
|
|
4
|
+
|
|
5
|
+
function closer(sock: Socket) {
|
|
6
|
+
if (!sock.closed) {
|
|
7
|
+
sock.end();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function wrapNodeSocket(socket: Socket): SockConn {
|
|
12
|
+
const readable = new ReadableStream(
|
|
13
|
+
{
|
|
14
|
+
type: "bytes",
|
|
15
|
+
start(controller) {
|
|
16
|
+
socket.on("data", (data: ArrayBufferView) => {
|
|
17
|
+
controller.enqueue(data);
|
|
18
|
+
const desiredSize = controller.desiredSize ?? 0;
|
|
19
|
+
if (desiredSize <= 0) {
|
|
20
|
+
// The internal queue is full, so propagate
|
|
21
|
+
// the backpressure signal to the underlying source.
|
|
22
|
+
socket.pause();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
socket.on("error", (err: unknown) => controller.error(err));
|
|
26
|
+
socket.on("end", () => {
|
|
27
|
+
// unlock the last BYOB read request
|
|
28
|
+
controller.byobRequest?.respond(1);
|
|
29
|
+
// and close the controller
|
|
30
|
+
controller.close();
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
pull: () => {
|
|
34
|
+
socket.resume();
|
|
35
|
+
},
|
|
36
|
+
cancel: () => {
|
|
37
|
+
socket.end();
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
const writable = Writable.toWeb(socket);
|
|
42
|
+
const remoteAddr = {
|
|
43
|
+
hostname: socket.remoteAddress || "",
|
|
44
|
+
port: socket.remotePort || 0,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const conn: SockConn = {
|
|
48
|
+
readable: readable as ReadableStream<Uint8Array>,
|
|
49
|
+
writable: writable as WritableStream<Uint8Array>,
|
|
50
|
+
closer: () => closer(socket),
|
|
51
|
+
remoteAddr,
|
|
52
|
+
};
|
|
53
|
+
return conn;
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seriousme/opifex",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MQTT client & server for Deno & NodeJS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --experimental-strip-types --test --test-skip-pattern='/^Deno/'"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"MQTT",
|
|
12
|
+
"typescript",
|
|
13
|
+
"mqtt-broker"
|
|
14
|
+
],
|
|
15
|
+
"bin": {
|
|
16
|
+
"mqtt": "bin/mqtt.ts",
|
|
17
|
+
"demoServer": "bin/demoServer.ts"
|
|
18
|
+
},
|
|
19
|
+
"author": "Hans Klunder",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/seriousme/opifex.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/seriousme/opifex/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/seriousme/opifex#readme",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.6.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@types/node": "^22.8.6"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ClientId,
|
|
3
|
+
PacketId,
|
|
4
|
+
PublishPacket,
|
|
5
|
+
QoS,
|
|
6
|
+
Subscription,
|
|
7
|
+
Topic,
|
|
8
|
+
TopicFilter,
|
|
9
|
+
} from "../mqttPacket/mod.ts";
|
|
10
|
+
export { PacketType } from "../mqttPacket/mod.ts";
|
|
11
|
+
export { Trie } from "../trie/trie.ts";
|
|
12
|
+
export { logger } from "../utils/mod.ts";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MemoryPersistence as Persistence,
|
|
3
|
+
MemoryStore as Store,
|
|
4
|
+
} from "./memoryPersistence.ts";
|
|
5
|
+
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import { PacketType, type PublishPacket } from "../deps.ts";
|
|
9
|
+
|
|
10
|
+
const payloadAny = new TextEncoder().encode("any");
|
|
11
|
+
const qos = 1;
|
|
12
|
+
|
|
13
|
+
function delay(ms: number): Promise<unknown> {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("new should create new Persistence object", () => {
|
|
18
|
+
const persistence = new Persistence();
|
|
19
|
+
assert.deepStrictEqual(typeof persistence, "object");
|
|
20
|
+
assert.deepStrictEqual(persistence instanceof Persistence, true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test(
|
|
24
|
+
"Registring a client should register the client and return a Store Object",
|
|
25
|
+
() => {
|
|
26
|
+
const persistence = new Persistence();
|
|
27
|
+
const clientId = "myClient";
|
|
28
|
+
const client = persistence.registerClient(clientId, () => {}, false);
|
|
29
|
+
assert.deepStrictEqual(persistence.clientList.has(clientId), true);
|
|
30
|
+
assert.deepStrictEqual(typeof client, "object");
|
|
31
|
+
assert.deepStrictEqual(client instanceof Store, true);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
test("pub/sub should work", async () => {
|
|
36
|
+
const persistence = new Persistence();
|
|
37
|
+
const clientId = "myClient";
|
|
38
|
+
const topic = "/myTopic";
|
|
39
|
+
const publishPacket: PublishPacket = {
|
|
40
|
+
type: PacketType.publish,
|
|
41
|
+
id: 1,
|
|
42
|
+
topic,
|
|
43
|
+
payload: payloadAny,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function makePacket(id: number | undefined) {
|
|
47
|
+
publishPacket.id = id;
|
|
48
|
+
return publishPacket;
|
|
49
|
+
}
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
|
|
52
|
+
function handler(packet: PublishPacket): void {
|
|
53
|
+
seen.add(packet.id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const store = persistence.registerClient(clientId, handler, false);
|
|
57
|
+
|
|
58
|
+
persistence.subscribe(store, topic, qos);
|
|
59
|
+
assert.deepStrictEqual(
|
|
60
|
+
store.subscriptions.has(topic),
|
|
61
|
+
true,
|
|
62
|
+
"topic is registered as subscription",
|
|
63
|
+
);
|
|
64
|
+
persistence.publish(topic, makePacket(25));
|
|
65
|
+
persistence.publish(topic, makePacket(27));
|
|
66
|
+
persistence.publish(topic, makePacket(undefined));
|
|
67
|
+
persistence.publish("noTopic", makePacket(undefined));
|
|
68
|
+
await delay(10);
|
|
69
|
+
assert.deepStrictEqual(seen.size, 3, `received ${seen.size} messages`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("many packets should work", async () => {
|
|
73
|
+
const persistence = new Persistence();
|
|
74
|
+
const clientId = "myClient";
|
|
75
|
+
const topic = "/myTopic";
|
|
76
|
+
const numMessages = 1000;
|
|
77
|
+
const publishPacket: PublishPacket = {
|
|
78
|
+
type: PacketType.publish,
|
|
79
|
+
id: 1,
|
|
80
|
+
topic,
|
|
81
|
+
payload: payloadAny,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function makePacket(id: number | undefined) {
|
|
85
|
+
const newPacket = Object.assign({}, publishPacket);
|
|
86
|
+
newPacket.id = id;
|
|
87
|
+
return newPacket;
|
|
88
|
+
}
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
|
|
91
|
+
function handler(packet: PublishPacket): void {
|
|
92
|
+
assert.deepStrictEqual(
|
|
93
|
+
seen.has(packet.id),
|
|
94
|
+
false,
|
|
95
|
+
`Not seen ${packet.id} before`,
|
|
96
|
+
);
|
|
97
|
+
seen.add(packet.id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const store = persistence.registerClient(clientId, handler, false);
|
|
101
|
+
|
|
102
|
+
persistence.subscribe(store, topic, qos);
|
|
103
|
+
assert.deepStrictEqual(
|
|
104
|
+
store.subscriptions.has(topic),
|
|
105
|
+
true,
|
|
106
|
+
"topic is registered as subscription",
|
|
107
|
+
);
|
|
108
|
+
for (let i = 0; i < numMessages; i++) {
|
|
109
|
+
persistence.publish(topic, makePacket(i));
|
|
110
|
+
}
|
|
111
|
+
await delay(10);
|
|
112
|
+
assert.deepStrictEqual(
|
|
113
|
+
seen.size,
|
|
114
|
+
numMessages,
|
|
115
|
+
`received all ${numMessages} messages`,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("unsubscribe should work", () => {
|
|
120
|
+
const persistence = new Persistence();
|
|
121
|
+
const clientId = "myClient";
|
|
122
|
+
const topic = "/myTopic";
|
|
123
|
+
const publishPacket: PublishPacket = {
|
|
124
|
+
type: PacketType.publish,
|
|
125
|
+
id: 1,
|
|
126
|
+
topic,
|
|
127
|
+
payload: payloadAny,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
function handler(packet: PublishPacket): void {
|
|
132
|
+
assert.deepStrictEqual(
|
|
133
|
+
seen.has(packet.id),
|
|
134
|
+
false,
|
|
135
|
+
`Not seen ${packet.id} before`,
|
|
136
|
+
);
|
|
137
|
+
seen.add(packet.id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const store = persistence.registerClient(clientId, handler, false);
|
|
141
|
+
persistence.subscribe(store, topic, qos);
|
|
142
|
+
persistence.unsubscribe(store, topic);
|
|
143
|
+
|
|
144
|
+
assert.deepStrictEqual(
|
|
145
|
+
store.subscriptions.has(topic),
|
|
146
|
+
false,
|
|
147
|
+
"topic is still registered as subscription after unsubscription",
|
|
148
|
+
);
|
|
149
|
+
persistence.publish(topic, publishPacket);
|
|
150
|
+
assert.deepStrictEqual(seen.size, 0);
|
|
151
|
+
});
|