@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.
Files changed (120) hide show
  1. package/.github/workflows/closeStale.yml +15 -0
  2. package/.github/workflows/codeql-analysis.yml +71 -0
  3. package/.github/workflows/deno-ci.yml +16 -0
  4. package/.github/workflows/nodejs-ci.yml +23 -0
  5. package/.github/workflows/publish.yml +16 -0
  6. package/.vscode/extensions.json +10 -0
  7. package/.vscode/settings.json +12 -0
  8. package/LICENSE.txt +21 -0
  9. package/README.md +34 -0
  10. package/bin/conformanceServer.ts +70 -0
  11. package/bin/demoServer.ts +64 -0
  12. package/bin/mqtt.ts +211 -0
  13. package/client/client.ts +181 -0
  14. package/client/context.ts +197 -0
  15. package/client/deps.ts +36 -0
  16. package/client/handlers/handleConnack.ts +21 -0
  17. package/client/handlers/handlePacket.ts +70 -0
  18. package/client/handlers/handlePuback.ts +10 -0
  19. package/client/handlers/handlePubcomp.ts +13 -0
  20. package/client/handlers/handlePublish.ts +32 -0
  21. package/client/handlers/handlePubrec.ts +24 -0
  22. package/client/handlers/handlePubrel.ts +26 -0
  23. package/client/handlers/handleSuback.ts +14 -0
  24. package/client/handlers/handleUnsuback.ts +11 -0
  25. package/client/mod.ts +11 -0
  26. package/client/store/deps.ts +22 -0
  27. package/client/store/memoryStore.ts +56 -0
  28. package/client/store/store.ts +29 -0
  29. package/deno/README.md +64 -0
  30. package/deno/client.ts +48 -0
  31. package/deno/server.test.ts +14 -0
  32. package/deno/server.ts +31 -0
  33. package/deno/wrapDenoConn.ts +12 -0
  34. package/deno.json +8 -0
  35. package/deno.lock +47 -0
  36. package/dev_utils/dummyConn.ts +95 -0
  37. package/dev_utils/mod.ts +1 -0
  38. package/mqttConn/mqttConn.test.ts +110 -0
  39. package/mqttConn/mqttConn.ts +134 -0
  40. package/mqttPacket/AuthenticationResult.ts +16 -0
  41. package/mqttPacket/BitMask.ts +10 -0
  42. package/mqttPacket/PacketType.ts +21 -0
  43. package/mqttPacket/connack.test.ts +129 -0
  44. package/mqttPacket/connack.ts +37 -0
  45. package/mqttPacket/connect.test.ts +297 -0
  46. package/mqttPacket/connect.ts +159 -0
  47. package/mqttPacket/decoder.test.ts +123 -0
  48. package/mqttPacket/decoder.ts +109 -0
  49. package/mqttPacket/disconnect.test.ts +50 -0
  50. package/mqttPacket/disconnect.ts +21 -0
  51. package/mqttPacket/encoder.test.ts +39 -0
  52. package/mqttPacket/encoder.ts +72 -0
  53. package/mqttPacket/length.test.ts +75 -0
  54. package/mqttPacket/length.ts +57 -0
  55. package/mqttPacket/mod.ts +151 -0
  56. package/mqttPacket/pingreq.test.ts +50 -0
  57. package/mqttPacket/pingreq.ts +22 -0
  58. package/mqttPacket/pingres.test.ts +50 -0
  59. package/mqttPacket/pingres.ts +21 -0
  60. package/mqttPacket/puback.test.ts +55 -0
  61. package/mqttPacket/puback.ts +28 -0
  62. package/mqttPacket/pubcomp.test.ts +55 -0
  63. package/mqttPacket/pubcomp.ts +28 -0
  64. package/mqttPacket/publish.test.ts +198 -0
  65. package/mqttPacket/publish.ts +73 -0
  66. package/mqttPacket/pubrec.test.ts +55 -0
  67. package/mqttPacket/pubrec.ts +28 -0
  68. package/mqttPacket/pubrel.test.ts +55 -0
  69. package/mqttPacket/pubrel.ts +28 -0
  70. package/mqttPacket/suback.test.ts +49 -0
  71. package/mqttPacket/suback.ts +41 -0
  72. package/mqttPacket/subscribe.test.ts +233 -0
  73. package/mqttPacket/subscribe.ts +69 -0
  74. package/mqttPacket/types.ts +27 -0
  75. package/mqttPacket/unsuback.test.ts +55 -0
  76. package/mqttPacket/unsuback.ts +28 -0
  77. package/mqttPacket/unsubscribe.test.ts +136 -0
  78. package/mqttPacket/unsubscribe.ts +48 -0
  79. package/mqttPacket/validators.ts +20 -0
  80. package/node/README.md +46 -0
  81. package/node/client.ts +58 -0
  82. package/node/server.test.ts +14 -0
  83. package/node/server.ts +34 -0
  84. package/node/wrapNodeSocket.ts +54 -0
  85. package/package.json +35 -0
  86. package/persistence/deps.ts +12 -0
  87. package/persistence/memory/memoryPersistence.test.ts +151 -0
  88. package/persistence/memory/memoryPersistence.ts +156 -0
  89. package/persistence/persistence.ts +24 -0
  90. package/persistence/store.ts +14 -0
  91. package/server/context.ts +157 -0
  92. package/server/deps.ts +37 -0
  93. package/server/handlers/handleConnect.ts +73 -0
  94. package/server/handlers/handleDisconnect.ts +6 -0
  95. package/server/handlers/handlePacket.ts +78 -0
  96. package/server/handlers/handlePingreq.ts +8 -0
  97. package/server/handlers/handlePuback.ts +12 -0
  98. package/server/handlers/handlePubcomp.ts +12 -0
  99. package/server/handlers/handlePublish.ts +47 -0
  100. package/server/handlers/handlePubrec.ts +20 -0
  101. package/server/handlers/handlePubrel.ts +24 -0
  102. package/server/handlers/handleSubscribe.ts +47 -0
  103. package/server/handlers/handleUnsubscribe.ts +17 -0
  104. package/server/mod.ts +16 -0
  105. package/server/server.ts +74 -0
  106. package/server/test/handleConnect.test.ts +126 -0
  107. package/server/test/test-handlers.ts +52 -0
  108. package/socket/socket.ts +59 -0
  109. package/timer/timer.test.ts +48 -0
  110. package/timer/timer.ts +45 -0
  111. package/trie/trie.test.ts +147 -0
  112. package/trie/trie.ts +106 -0
  113. package/utils/args.ts +86 -0
  114. package/utils/assert.ts +31 -0
  115. package/utils/asyncQueue.ts +63 -0
  116. package/utils/deferred.ts +12 -0
  117. package/utils/logger.ts +35 -0
  118. package/utils/mod.ts +7 -0
  119. package/utils/nextTick.ts +3 -0
  120. 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
+ });