@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,181 @@
1
+ import {
2
+ type ConnectPacket,
3
+ Deferred,
4
+ logger,
5
+ MemoryStore,
6
+ PacketType,
7
+ type PublishPacket,
8
+ type SockConn,
9
+ type SubscribePacket,
10
+ type TAuthenticationResult,
11
+ } from "./deps.ts";
12
+
13
+ import { Context } from "./context.ts";
14
+
15
+ function generateClientId(prefix: string): string {
16
+ return `${prefix}-${Math.random().toString().slice(-10)}`;
17
+ }
18
+
19
+ type ConnectOptions = Omit<
20
+ ConnectPacket,
21
+ "type" | "protocolName" | "protocolLevel"
22
+ >;
23
+
24
+ /** ConnectParameters define how to connect */
25
+ export type ConnectParameters = {
26
+ url?: URL;
27
+ caCerts?: string[];
28
+ numberOfRetries?: number;
29
+ options?: ConnectOptions;
30
+ };
31
+
32
+ /** PublishParameters define how a message should be published */
33
+ export type PublishParameters = Omit<PublishPacket, "type" | "id">;
34
+ /** SubscribeParameters define how to subscribe to a topic */
35
+ export type SubscribeParameters = Omit<SubscribePacket, "type" | "id">;
36
+
37
+ function backOffSleep(random: boolean, attempt: number): Promise<void> {
38
+ // based on https://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html
39
+ const factor = 1.5;
40
+ const min = 1000;
41
+ const max = 5000;
42
+ const randomness = 1 + (random ? Math.random() : 0);
43
+ const delay = Math.floor(
44
+ Math.min(randomness * min * (factor ** attempt), max),
45
+ );
46
+ logger.debug({ delay });
47
+ return new Promise((resolve) => setTimeout(resolve, delay));
48
+ }
49
+
50
+ /** the default MQTT URL to connect to */
51
+ export const DEFAULT_URL = "mqtt://localhost:1883/";
52
+ const DEFAULT_KEEPALIVE = 60; // 60 seconds
53
+ const DEFAULT_RETRIES = 3; // on first connect
54
+ const CLIENTID_PREFIX = "opifex"; // on first connect
55
+
56
+ /**
57
+ * The Client class provides an MQTT Client that can be used to connect to
58
+ * a MQTT broker and publish/subscribe messages.
59
+ *
60
+ * The Client class is not meant to be used directly, but
61
+ * instead should be subclassed and the subclass should
62
+ * override the createConn() method to provide a
63
+ * connection type that is supported by the subclass.
64
+ */
65
+
66
+ export class Client {
67
+ protected clientIdPrefix = CLIENTID_PREFIX;
68
+ protected numberOfRetries = DEFAULT_RETRIES;
69
+ protected url: URL = new URL(DEFAULT_URL);
70
+ protected keepAlive = DEFAULT_KEEPALIVE;
71
+ protected autoReconnect = true;
72
+ private caCerts?: string[];
73
+ private clientId: string;
74
+ private ctx = new Context(new MemoryStore());
75
+ private connectPacket?: ConnectPacket;
76
+
77
+ constructor() {
78
+ this.clientId = generateClientId(this.clientIdPrefix);
79
+ this.numberOfRetries = DEFAULT_RETRIES;
80
+ }
81
+
82
+ protected createConn(
83
+ protocol: string,
84
+ _hostname: string,
85
+ _port?: number,
86
+ _caCerts?: string[],
87
+ ): Promise<SockConn> {
88
+ // if you need to support alternative connection types just
89
+ // overload this method in your subclass
90
+ throw `Unsupported protocol: ${protocol}`;
91
+ }
92
+
93
+ private async doConnect(): Promise<void> {
94
+ if (!this.connectPacket) {
95
+ return;
96
+ }
97
+ let isReconnect = false;
98
+ let attempt = 1;
99
+ let lastMessage = "";
100
+ let tryConnect = true;
101
+ while (tryConnect) {
102
+ logger.debug(`${isReconnect ? "re" : ""}connecting`);
103
+ try {
104
+ const conn = await this.createConn(
105
+ this.url.protocol,
106
+ this.url.hostname,
107
+ Number(this.url.port) ?? undefined,
108
+ this.caCerts,
109
+ );
110
+ // if we get this far we have a connection
111
+ tryConnect =
112
+ (await this.ctx.handleConnection(conn, this.connectPacket)) &&
113
+ this.autoReconnect;
114
+ logger.debug({ tryConnect });
115
+ isReconnect = true;
116
+ this.connectPacket.clean = false;
117
+ this.ctx.close();
118
+ } catch (err) {
119
+ if (err instanceof Error) {
120
+ lastMessage = `Connection failed: ${err.message}`;
121
+ }
122
+ logger.debug(lastMessage);
123
+ if (!isReconnect && attempt > this.numberOfRetries) {
124
+ tryConnect = false;
125
+ } else {
126
+ await backOffSleep(true, attempt++);
127
+ }
128
+ }
129
+ }
130
+
131
+ if (isReconnect === false) {
132
+ this.ctx.unresolvedConnect?.reject(Error(lastMessage));
133
+ }
134
+ }
135
+
136
+ connect(params: ConnectParameters = {}): Promise<TAuthenticationResult> {
137
+ this.url = params?.url || this.url;
138
+ this.numberOfRetries = params.numberOfRetries || this.numberOfRetries;
139
+ this.caCerts = params?.caCerts;
140
+ const options = Object.assign(
141
+ {
142
+ keepAlive: this.keepAlive,
143
+ clientId: this.clientId,
144
+ },
145
+ params?.options,
146
+ );
147
+ this.connectPacket = {
148
+ type: PacketType.connect,
149
+ ...options,
150
+ };
151
+ const deferred = new Deferred<TAuthenticationResult>();
152
+ this.ctx.unresolvedConnect = deferred;
153
+ this.doConnect();
154
+ return deferred.promise;
155
+ }
156
+
157
+ async disconnect(): Promise<void> {
158
+ await this.ctx.disconnect();
159
+ }
160
+
161
+ async publish(params: PublishParameters): Promise<void> {
162
+ const packet: PublishPacket = {
163
+ type: PacketType.publish,
164
+ ...params,
165
+ };
166
+ await this.ctx.send(packet);
167
+ }
168
+
169
+ async subscribe(params: SubscribeParameters): Promise<void> {
170
+ const packet: SubscribePacket = {
171
+ type: PacketType.subscribe,
172
+ id: this.ctx.store.nextId(),
173
+ ...params,
174
+ };
175
+ await this.ctx.send(packet);
176
+ }
177
+
178
+ async *messages(): AsyncGenerator<PublishPacket, void, unknown> {
179
+ yield* this.ctx.incoming;
180
+ }
181
+ }
@@ -0,0 +1,197 @@
1
+ import {
2
+ type AnyPacket,
3
+ AsyncQueue,
4
+ type ConnectPacket,
5
+ Deferred,
6
+ logger,
7
+ type MemoryStore,
8
+ MqttConn,
9
+ type PacketId,
10
+ PacketType,
11
+ type PublishPacket,
12
+ type ReturnCodes,
13
+ type SockConn,
14
+ type SubscribePacket,
15
+ type TAuthenticationResult,
16
+ Timer,
17
+ type UnsubscribePacket,
18
+ } from "./deps.ts";
19
+
20
+ import { handlePacket } from "./handlers/handlePacket.ts";
21
+
22
+ export const ConnectionState = {
23
+ offline: "offline",
24
+ connecting: "connecting",
25
+ connected: "connected",
26
+ disconnecting: "disconnecting",
27
+ disconnected: "disconnected",
28
+ } as const;
29
+
30
+ export type TConnectionState =
31
+ typeof ConnectionState[keyof typeof ConnectionState];
32
+
33
+ export class Context {
34
+ mqttConn?: MqttConn;
35
+ connectionState: TConnectionState;
36
+ pingTimer?: Timer;
37
+ unresolvedConnect?: Deferred<TAuthenticationResult>;
38
+ unresolvedPublish: Map<PacketId, Deferred<void>>;
39
+ unresolvedSubscribe: Map<PacketId, Deferred<ReturnCodes>>;
40
+ unresolvedUnSubscribe: Map<PacketId, Deferred<void>>;
41
+ store: MemoryStore;
42
+ incoming: AsyncQueue<PublishPacket>;
43
+
44
+ constructor(store: MemoryStore) {
45
+ this.store = store;
46
+ this.connectionState = ConnectionState.offline;
47
+ this.incoming = new AsyncQueue();
48
+ this.unresolvedPublish = new Map();
49
+ this.unresolvedSubscribe = new Map();
50
+ this.unresolvedUnSubscribe = new Map();
51
+ }
52
+
53
+ async connect(packet: ConnectPacket) {
54
+ this.connectionState = ConnectionState.connecting;
55
+ await this.mqttConn?.send(packet);
56
+ const keepAlive = packet.keepAlive || 0;
57
+ if (keepAlive > 0) {
58
+ this.pingTimer = new Timer(
59
+ this.sendPing.bind(this),
60
+ keepAlive * 1000,
61
+ true,
62
+ );
63
+ }
64
+ }
65
+
66
+ async disconnect() {
67
+ if (this.connectionState !== ConnectionState.connected) {
68
+ throw "Not connected";
69
+ }
70
+ if (this.mqttConn) {
71
+ this.connectionState = ConnectionState.disconnecting;
72
+ await this.mqttConn.send({ type: PacketType.disconnect });
73
+ this.mqttConn.close();
74
+ }
75
+ }
76
+
77
+ async send(packet: AnyPacket) {
78
+ logger.debug({ send: packet });
79
+ if (
80
+ this.connectionState === ConnectionState.connected &&
81
+ !this.mqttConn?.isClosed
82
+ ) {
83
+ await this.mqttConn?.send(packet);
84
+ this.pingTimer?.reset();
85
+ return;
86
+ }
87
+ logger.debug("not connected");
88
+ this.pingTimer?.clear();
89
+ }
90
+
91
+ sendPing() {
92
+ this.send({ type: PacketType.pingreq });
93
+ }
94
+
95
+ async handleConnection(
96
+ conn: SockConn,
97
+ connectPacket: ConnectPacket,
98
+ ): Promise<boolean> {
99
+ this.mqttConn = new MqttConn({ conn });
100
+ if (this.mqttConn === undefined) {
101
+ return true;
102
+ }
103
+ await this.connect(connectPacket);
104
+ try {
105
+ for await (const packet of this.mqttConn) {
106
+ handlePacket(this, packet);
107
+ }
108
+ logger.debug("No more packets");
109
+ } catch (err) {
110
+ logger.debug(err);
111
+ if (this.mqttConn.isClosed) {
112
+ this.mqttConn.close();
113
+ }
114
+ }
115
+ if (this.connectionState === ConnectionState.disconnecting) {
116
+ return false;
117
+ }
118
+ return true;
119
+ }
120
+
121
+ close() {
122
+ logger.debug("closing connection");
123
+ this.connectionState = ConnectionState.disconnected;
124
+ this.pingTimer?.clear();
125
+ }
126
+
127
+ receivePublish(packet: PublishPacket) {
128
+ this.incoming.push(packet);
129
+ }
130
+
131
+ publish(packet: PublishPacket): Promise<void> {
132
+ const qos = packet.qos || 0;
133
+ if (qos === 0) {
134
+ packet.id = 0;
135
+ this.send(packet);
136
+ // return empty promise
137
+ return Promise.resolve();
138
+ }
139
+ packet.id = this.store.nextId();
140
+ this.store.pendingOutgoing.set(packet.id, packet);
141
+ const deferred = new Deferred<void>();
142
+ this.unresolvedPublish.set(packet.id, deferred);
143
+ this.send(packet);
144
+ return deferred.promise;
145
+ }
146
+
147
+ subscribe(packet: SubscribePacket): Promise<ReturnCodes> {
148
+ packet.id = this.store.nextId();
149
+ this.store.pendingOutgoing.set(packet.id, packet);
150
+ const deferred = new Deferred<ReturnCodes>();
151
+ this.unresolvedSubscribe.set(packet.id, deferred);
152
+ this.send(packet);
153
+ return deferred.promise;
154
+ }
155
+
156
+ unsubscribe(packet: UnsubscribePacket): Promise<void> {
157
+ packet.id = this.store.nextId();
158
+ this.store.pendingOutgoing.set(packet.id, packet);
159
+ const deferred = new Deferred<void>();
160
+ this.unresolvedUnSubscribe.set(packet.id, deferred);
161
+ this.send(packet);
162
+ return deferred.promise;
163
+ }
164
+
165
+ receivePuback(id: PacketId): boolean {
166
+ const unresolvedMap = this.unresolvedPublish;
167
+ if (unresolvedMap.has(id)) {
168
+ const deferred = unresolvedMap.get(id);
169
+ unresolvedMap.delete(id);
170
+ deferred?.resolve();
171
+ return true;
172
+ }
173
+ return false;
174
+ }
175
+
176
+ receiveSuback(id: PacketId, returnCodes: ReturnCodes): boolean {
177
+ const unresolvedMap = this.unresolvedSubscribe;
178
+ if (unresolvedMap.has(id)) {
179
+ const deferred = unresolvedMap.get(id);
180
+ unresolvedMap.delete(id);
181
+ deferred?.resolve(returnCodes);
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+
187
+ receiveUnsuback(id: PacketId): boolean {
188
+ const unresolvedMap = this.unresolvedUnSubscribe;
189
+ if (unresolvedMap.has(id)) {
190
+ const deferred = unresolvedMap.get(id);
191
+ unresolvedMap.delete(id);
192
+ deferred?.resolve();
193
+ return true;
194
+ }
195
+ return false;
196
+ }
197
+ }
package/client/deps.ts ADDED
@@ -0,0 +1,36 @@
1
+ export type {
2
+ AnyPacket,
3
+ ConnackPacket,
4
+ ConnectPacket,
5
+ DisconnectPacket,
6
+ Dup,
7
+ PacketId,
8
+ Payload,
9
+ PubackPacket,
10
+ PubcompPacket,
11
+ PublishPacket,
12
+ PubrecPacket,
13
+ PubrelPacket,
14
+ ReturnCodes,
15
+ SubackPacket,
16
+ SubscribePacket,
17
+ TAuthenticationResult,
18
+ Topic,
19
+ UnsubackPacket,
20
+ UnsubscribePacket,
21
+ } from "../mqttPacket/mod.ts";
22
+
23
+ export {
24
+ AuthenticationResult,
25
+ AuthenticationResultByNumber,
26
+ decodePayload,
27
+ encode,
28
+ PacketNameByType,
29
+ PacketType,
30
+ } from "../mqttPacket/mod.ts";
31
+
32
+ export { MqttConn } from "../mqttConn/mqttConn.ts";
33
+ export type { SockConn } from "../socket/socket.ts";
34
+ export { Timer } from "../timer/timer.ts";
35
+ export { AsyncQueue, Deferred, logger } from "../utils/mod.ts";
36
+ export { MemoryStore } from "./store/memoryStore.ts";
@@ -0,0 +1,21 @@
1
+ import { ConnectionState, type Context } from "../context.ts";
2
+ import { AuthenticationResultByNumber, type ConnackPacket } from "../deps.ts";
3
+
4
+ export async function handleConnack(packet: ConnackPacket, ctx: Context) {
5
+ if (packet.returnCode === 0) {
6
+ ctx.connectionState = ConnectionState.connected;
7
+ ctx.pingTimer?.reset();
8
+ ctx.unresolvedConnect?.resolve(packet.returnCode);
9
+ // start transmitting packets that were queued before
10
+ for await (const packet of ctx.store.pendingOutgoingPackets()) {
11
+ ctx.send(packet);
12
+ }
13
+ return;
14
+ }
15
+ const err = new Error(
16
+ `Connect failed: ${AuthenticationResultByNumber[packet.returnCode]}`,
17
+ );
18
+ ctx.connectionState = ConnectionState.disconnecting;
19
+ ctx.pingTimer?.clear();
20
+ ctx.unresolvedConnect?.reject(err);
21
+ }
@@ -0,0 +1,70 @@
1
+ import { ConnectionState, type Context } from "../context.ts";
2
+ import { handleConnack } from "./handleConnack.ts";
3
+ import { handlePublish } from "./handlePublish.ts";
4
+ import { handlePuback } from "./handlePuback.ts";
5
+ import { handlePubrec } from "./handlePubrec.ts";
6
+ import { handlePubrel } from "./handlePubrel.ts";
7
+ import { handlePubcomp } from "./handlePubcomp.ts";
8
+ import { handleSuback } from "./handleSuback.ts";
9
+ import { handleUnsuback } from "./handleUnsuback.ts";
10
+ import { logger, PacketNameByType, PacketType } from "../deps.ts";
11
+ import type {
12
+ AnyPacket,
13
+ ConnackPacket,
14
+ PubackPacket,
15
+ PubcompPacket,
16
+ PublishPacket,
17
+ PubrecPacket,
18
+ PubrelPacket,
19
+ SubackPacket,
20
+ UnsubackPacket,
21
+ } from "../deps.ts";
22
+
23
+ export async function handlePacket(
24
+ ctx: Context,
25
+ packet: AnyPacket,
26
+ ): Promise<void> {
27
+ logger.debug({ received: JSON.stringify(packet, null, 2) });
28
+ if (ctx.connectionState !== ConnectionState.connected) {
29
+ if (packet.type === PacketType.connack) {
30
+ handleConnack(packet as ConnackPacket, ctx);
31
+ } else {
32
+ throw new Error(
33
+ `Received ${PacketNameByType[packet.type]} packet before connect`,
34
+ );
35
+ }
36
+ } else {
37
+ switch (packet.type) {
38
+ case PacketType.pingres:
39
+ break;
40
+ case PacketType.publish:
41
+ await handlePublish(ctx, packet as PublishPacket);
42
+ break;
43
+ case PacketType.puback:
44
+ await handlePuback(ctx, packet as PubackPacket);
45
+ break;
46
+ case PacketType.pubrel:
47
+ await handlePubrel(ctx, packet as PubrelPacket);
48
+ break;
49
+ case PacketType.pubrec:
50
+ await handlePubrec(ctx, packet as PubrecPacket);
51
+ break;
52
+ case PacketType.pubcomp:
53
+ await handlePubcomp(ctx, packet as PubcompPacket);
54
+ break;
55
+ case PacketType.suback:
56
+ await handleSuback(ctx, packet as SubackPacket);
57
+ break;
58
+ case PacketType.unsuback:
59
+ handleUnsuback(ctx, packet as UnsubackPacket);
60
+ break;
61
+
62
+ default:
63
+ throw new Error(
64
+ `Received unexpected ${
65
+ PacketNameByType[packet.type]
66
+ } packet after connect`,
67
+ );
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,10 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { PubackPacket } from "../deps.ts";
3
+
4
+ // A PUBACK Packet is the response to a PUBLISH Packet with QoS level 1.
5
+
6
+ export function handlePuback(ctx: Context, packet: PubackPacket): void {
7
+ const id = packet.id;
8
+ ctx.store.pendingOutgoing.delete(id);
9
+ ctx.receivePuback(id);
10
+ }
@@ -0,0 +1,13 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { PubcompPacket } from "../deps.ts";
3
+
4
+ // The PUBCOMP Packet is the response to a PUBREL Packet.
5
+ // It is the fourth and final packet of the QoS 2 protocol exchange.
6
+
7
+ export function handlePubcomp(ctx: Context, packet: PubcompPacket): void {
8
+ const id = packet.id;
9
+ if (ctx.store.pendingAckOutgoing.has(id)) {
10
+ ctx.store.pendingAckOutgoing.delete(id);
11
+ ctx.receivePuback(id);
12
+ }
13
+ }
@@ -0,0 +1,32 @@
1
+ import type { Context } from "../context.ts";
2
+ import { PacketType, type PublishPacket } from "../deps.ts";
3
+
4
+ // Incoming publish
5
+
6
+ export async function handlePublish(ctx: Context, packet: PublishPacket) {
7
+ const qos = packet.qos || 0;
8
+ if (qos === 0) {
9
+ ctx.receivePublish(packet);
10
+ return;
11
+ }
12
+
13
+ if (packet.id !== undefined) {
14
+ // qos 1
15
+ if (qos === 1) {
16
+ ctx.receivePublish(packet);
17
+ await ctx.send({
18
+ type: PacketType.puback,
19
+ id: packet.id,
20
+ });
21
+ return;
22
+ }
23
+ // qos 2
24
+ if (ctx.store) {
25
+ ctx.store.pendingIncoming.set(packet.id, packet);
26
+ await ctx.send({
27
+ type: PacketType.pubrec,
28
+ id: packet.id,
29
+ });
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,24 @@
1
+ import type { Context } from "../context.ts";
2
+ import { PacketType, type PubrecPacket, type PubrelPacket } from "../deps.ts";
3
+
4
+ // A PUBREC Packet is the response to a PUBLISH Packet with QoS 2.
5
+ // It is the second packet of the QoS 2 protocol exchange.
6
+
7
+ // Discard message, Store PUBREC received <Packet Identifier>
8
+ // send PUBREL <Packet Identifier>
9
+
10
+ export async function handlePubrec(
11
+ ctx: Context,
12
+ packet: PubrecPacket,
13
+ ): Promise<void> {
14
+ const id = packet.id;
15
+ const ack: PubrelPacket = {
16
+ type: PacketType.pubrel,
17
+ id,
18
+ };
19
+ if (ctx.store.pendingOutgoing.has(id)) {
20
+ ctx.store.pendingAckOutgoing.set(id, ack);
21
+ ctx.store.pendingOutgoing.delete(id);
22
+ await ctx.send(ack);
23
+ }
24
+ }
@@ -0,0 +1,26 @@
1
+ import type { Context } from "../context.ts";
2
+ import { PacketType, type PubrelPacket } from "../deps.ts";
3
+
4
+ // A PUBREL Packet is the response to a PUBREC Packet.
5
+ // It is the third packet of the QoS 2 protocol exchange.
6
+
7
+ // Method A, Initiate onward delivery of the Application Message then discard message
8
+ // Send PUBCOMP <Packet Identifier>
9
+
10
+ export async function handlePubrel(
11
+ ctx: Context,
12
+ packet: PubrelPacket,
13
+ ): Promise<void> {
14
+ const id = packet.id;
15
+ if (ctx.store.pendingIncoming.has(id)) {
16
+ const storedPacket = ctx.store.pendingIncoming.get(id);
17
+ if (storedPacket) {
18
+ ctx.receivePublish(storedPacket);
19
+ ctx.store.pendingIncoming.delete(id);
20
+ await ctx.send({
21
+ type: PacketType.pubcomp,
22
+ id,
23
+ });
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { SubackPacket } from "../deps.ts";
3
+
4
+ // A SUBACK Packet is sent by the Server to the Client to confirm receipt
5
+ // and processing of a SUBSCRIBE Packet.
6
+
7
+ // A SUBACK Packet contains a list of return codes, that specify the maximum QoS level
8
+ // that was granted in each Subscription that was requested by the SUBSCRIBE.
9
+
10
+ export function handleSuback(ctx: Context, packet: SubackPacket): void {
11
+ const id = packet.id;
12
+ ctx.store.pendingOutgoing.delete(id);
13
+ ctx.receiveSuback(id, packet.returnCodes);
14
+ }
@@ -0,0 +1,11 @@
1
+ import type { Context } from "../context.ts";
2
+ import type { UnsubackPacket } from "../deps.ts";
3
+
4
+ // The UNSUBACK Packet is sent by the Server to the Client to confirm receipt
5
+ // of an UNSUBSCRIBE Packet.
6
+
7
+ export function handleUnsuback(ctx: Context, packet: UnsubackPacket): void {
8
+ const id = packet.id;
9
+ ctx.store.pendingOutgoing.delete(id);
10
+ ctx.receiveUnsuback(id);
11
+ }
package/client/mod.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * This module provides an MQTT Client interface
3
+ * see the /bin folder for examples
4
+ * @module
5
+ */
6
+ export { Client, DEFAULT_URL } from "./client.ts";
7
+ export type {
8
+ ConnectParameters,
9
+ PublishParameters,
10
+ SubscribeParameters,
11
+ } from "./client.ts";
@@ -0,0 +1,22 @@
1
+ export type {
2
+ AnyPacket,
3
+ ConnackPacket,
4
+ ConnectPacket,
5
+ DisconnectPacket,
6
+ Dup,
7
+ PacketId,
8
+ Payload,
9
+ PubackPacket,
10
+ PubcompPacket,
11
+ PublishPacket,
12
+ PubrecPacket,
13
+ PubrelPacket,
14
+ ReturnCodes,
15
+ SubackPacket,
16
+ SubscribePacket,
17
+ Topic,
18
+ UnsubackPacket,
19
+ UnsubscribePacket,
20
+ } from "../../mqttPacket/mod.ts";
21
+
22
+ export { assert } from "../../utils/mod.ts";