@seriousme/opifex 1.6.1 → 1.6.3

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 (112) hide show
  1. package/.github/dependabot.yml +7 -0
  2. package/.github/workflows/closeStale.yml +1 -1
  3. package/.github/workflows/codeql-analysis.yml +4 -4
  4. package/CHANGELOG.md +12 -0
  5. package/README.md +6 -7
  6. package/bin/conformanceServer.ts +67 -16
  7. package/bin/demoServer.ts +69 -14
  8. package/bin/mqtt.ts +30 -0
  9. package/client/ConnectionState.ts +12 -0
  10. package/client/client.ts +56 -2
  11. package/client/context.ts +5 -0
  12. package/client/handlers/handleConnack.ts +6 -0
  13. package/client/handlers/handlePacket.ts +7 -0
  14. package/client/handlers/handlePuback.ts +8 -2
  15. package/client/handlers/handlePubcomp.ts +10 -4
  16. package/client/handlers/handlePublish.ts +6 -2
  17. package/client/handlers/handlePubrec.ts +13 -6
  18. package/client/handlers/handlePubrel.ts +16 -6
  19. package/client/handlers/handleSuback.ts +9 -6
  20. package/client/handlers/handleUnsuback.ts +10 -3
  21. package/client/store/memoryStore.ts +25 -0
  22. package/client/store/store.ts +39 -0
  23. package/deno/tcpClient.ts +19 -2
  24. package/deno/tcpServer.test.ts +137 -2
  25. package/deno/tcpServer.ts +9 -2
  26. package/deno.json +2 -1
  27. package/dist/bin/conformanceServer.js +61 -15
  28. package/dist/bin/demoServer.js +63 -13
  29. package/dist/bin/mqtt.js +29 -0
  30. package/dist/client/ConnectionState.js +9 -0
  31. package/dist/client/client.js +56 -1
  32. package/dist/client/context.js +4 -0
  33. package/dist/client/handlers/handleConnack.js +6 -0
  34. package/dist/client/handlers/handlePacket.js +7 -0
  35. package/dist/client/handlers/handlePuback.js +8 -1
  36. package/dist/client/handlers/handlePubcomp.js +10 -3
  37. package/dist/client/handlers/handlePublish.js +6 -1
  38. package/dist/client/handlers/handlePubrec.js +13 -4
  39. package/dist/client/handlers/handlePubrel.js +16 -4
  40. package/dist/client/handlers/handleSuback.js +9 -4
  41. package/dist/client/handlers/handleUnsuback.js +10 -2
  42. package/dist/client/store/memoryStore.js +22 -0
  43. package/dist/client/store/store.js +3 -0
  44. package/dist/mqttPacket/AuthenticationResult.js +3 -0
  45. package/dist/mqttPacket/mod.js +5 -0
  46. package/dist/node/tcpClient.js +19 -2
  47. package/dist/node/tcpServer.js +12 -2
  48. package/dist/node/wrapNodeSocket.js +9 -0
  49. package/dist/persistence/memory/memoryPersistence.js +1 -0
  50. package/dist/server/handlers/handleConnect.js +39 -5
  51. package/dist/server/handlers/handleDisconnect.js +5 -0
  52. package/dist/server/handlers/handlePacket.js +7 -0
  53. package/dist/server/handlers/handlePingreq.js +5 -0
  54. package/dist/server/handlers/handlePuback.js +9 -1
  55. package/dist/server/handlers/handlePubcomp.js +8 -2
  56. package/dist/server/handlers/handlePublish.js +13 -0
  57. package/dist/server/handlers/handlePubrec.js +11 -3
  58. package/dist/server/handlers/handlePubrel.js +12 -3
  59. package/dist/server/handlers/handleSubscribe.js +27 -4
  60. package/dist/server/handlers/handleUnsubscribe.js +6 -0
  61. package/dist/utils/args.js +14 -0
  62. package/dist/utils/assert.js +1 -0
  63. package/dist/utils/asyncQueue.js +13 -0
  64. package/dist/utils/deferred.js +3 -0
  65. package/dist/utils/logger.js +29 -0
  66. package/dist/utils/mod.js +4 -0
  67. package/dist/utils/nextTick.js +15 -0
  68. package/dist/utils/platform.js +3 -0
  69. package/mqttPacket/AuthenticationResult.ts +3 -1
  70. package/mqttPacket/connack.ts +4 -0
  71. package/mqttPacket/connect.ts +3 -0
  72. package/mqttPacket/disconnect.ts +4 -0
  73. package/mqttPacket/mod.ts +9 -0
  74. package/mqttPacket/pingreq.ts +3 -0
  75. package/mqttPacket/pingres.ts +4 -0
  76. package/mqttPacket/puback.ts +3 -0
  77. package/mqttPacket/pubcomp.ts +4 -0
  78. package/mqttPacket/publish.ts +4 -0
  79. package/mqttPacket/pubrec.ts +4 -0
  80. package/mqttPacket/pubrel.ts +4 -0
  81. package/mqttPacket/suback.ts +4 -0
  82. package/mqttPacket/subscribe.ts +6 -0
  83. package/mqttPacket/types.ts +32 -1
  84. package/mqttPacket/unsuback.ts +4 -0
  85. package/mqttPacket/unsubscribe.ts +3 -0
  86. package/node/tcpClient.ts +20 -2
  87. package/node/tcpServer.test.ts +61 -6
  88. package/node/tcpServer.ts +17 -4
  89. package/node/wrapNodeSocket.ts +13 -2
  90. package/package.json +2 -2
  91. package/persistence/memory/memoryPersistence.test.ts +2 -1
  92. package/persistence/memory/memoryPersistence.ts +1 -0
  93. package/persistence/store.ts +1 -0
  94. package/server/handlers/handleConnect.ts +49 -6
  95. package/server/handlers/handleDisconnect.ts +5 -0
  96. package/server/handlers/handlePacket.ts +7 -0
  97. package/server/handlers/handlePingreq.ts +6 -0
  98. package/server/handlers/handlePuback.ts +9 -2
  99. package/server/handlers/handlePubcomp.ts +8 -3
  100. package/server/handlers/handlePublish.ts +13 -0
  101. package/server/handlers/handlePubrec.ts +11 -3
  102. package/server/handlers/handlePubrel.ts +12 -4
  103. package/server/handlers/handleSubscribe.ts +28 -5
  104. package/server/handlers/handleUnsubscribe.ts +6 -0
  105. package/utils/args.ts +17 -0
  106. package/utils/assert.ts +1 -0
  107. package/utils/asyncQueue.ts +13 -1
  108. package/utils/deferred.ts +4 -0
  109. package/utils/logger.ts +29 -0
  110. package/utils/mod.ts +4 -0
  111. package/utils/nextTick.ts +15 -0
  112. package/utils/platform.ts +4 -0
@@ -1,10 +1,21 @@
1
1
  import { Deferred, logger, MemoryStore, PacketType, } from "./deps.js";
2
2
  import { Context } from "./context.js";
3
+ /**
4
+ * Generates a random client ID with the given prefix
5
+ * @param prefix - The prefix to use for the client ID
6
+ * @returns A string containing the prefix followed by a random number
7
+ */
3
8
  function generateClientId(prefix) {
4
9
  return `${prefix}-${Math.random().toString().slice(-10)}`;
5
10
  }
11
+ /**
12
+ * Implements exponential backoff sleep with optional randomization
13
+ * based on https://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html
14
+ * @param random - Whether to add randomization to the delay
15
+ * @param attempt - The attempt number (used to calculate delay)
16
+ * @returns Promise that resolves after the calculated delay
17
+ */
6
18
  function backOffSleep(random, attempt) {
7
- // based on https://dthain.blogspot.com/2009/02/exponential-backoff-in-distributed.html
8
19
  const factor = 1.5;
9
20
  const min = 1000;
10
21
  const max = 5000;
@@ -39,15 +50,32 @@ export class Client {
39
50
  clientId;
40
51
  ctx = new Context(new MemoryStore());
41
52
  connectPacket;
53
+ /**
54
+ * Creates a new MQTT client instance
55
+ */
42
56
  constructor() {
43
57
  this.clientId = generateClientId(this.clientIdPrefix);
44
58
  this.numberOfRetries = DEFAULT_RETRIES;
45
59
  }
60
+ /**
61
+ * Creates a new connection to the MQTT broker
62
+ * @param protocol - The protocol to use (mqtt, mqtts, etc)
63
+ * @param _hostname - The hostname to connect to
64
+ * @param _port - The port to connect to
65
+ * @param _caCerts - Optional CA certificates
66
+ * @param _cert - Optional client certificate
67
+ * @param _key - Optional client key
68
+ * @returns Promise resolving to a SockConn connection
69
+ */
46
70
  createConn(protocol, _hostname, _port, _caCerts, _cert, _key) {
47
71
  // if you need to support alternative connection types just
48
72
  // overload this method in your subclass
49
73
  throw `Unsupported protocol: ${protocol}`;
50
74
  }
75
+ /**
76
+ * Handles the connection process including retries and reconnection
77
+ * @returns Promise that resolves when connection is established or fails
78
+ */
51
79
  async doConnect() {
52
80
  if (!this.connectPacket) {
53
81
  return;
@@ -86,6 +114,11 @@ export class Client {
86
114
  this.ctx.unresolvedConnect?.reject(lastMessage);
87
115
  }
88
116
  }
117
+ /**
118
+ * Connects to the MQTT broker
119
+ * @param params - Connection parameters
120
+ * @returns Promise resolving to authentication result
121
+ */
89
122
  connect(params = {}) {
90
123
  this.url = params?.url || this.url;
91
124
  this.numberOfRetries = params.numberOfRetries || this.numberOfRetries;
@@ -105,9 +138,18 @@ export class Client {
105
138
  this.doConnect();
106
139
  return deferred.promise;
107
140
  }
141
+ /**
142
+ * Disconnects from the MQTT broker
143
+ * @returns Promise that resolves when disconnected
144
+ */
108
145
  async disconnect() {
109
146
  await this.ctx.disconnect();
110
147
  }
148
+ /**
149
+ * Publishes a message to the MQTT broker
150
+ * @param params - Publish parameters including topic and payload
151
+ * @returns Promise that resolves when published
152
+ */
111
153
  async publish(params) {
112
154
  const packet = {
113
155
  type: PacketType.publish,
@@ -115,6 +157,11 @@ export class Client {
115
157
  };
116
158
  await this.ctx.publish(packet);
117
159
  }
160
+ /**
161
+ * Subscribes to topics on the MQTT broker
162
+ * @param params - Subscribe parameters including topics
163
+ * @returns Promise that resolves when subscribed
164
+ */
118
165
  async subscribe(params) {
119
166
  const packet = {
120
167
  type: PacketType.subscribe,
@@ -123,9 +170,17 @@ export class Client {
123
170
  };
124
171
  await this.ctx.subscribe(packet);
125
172
  }
173
+ /**
174
+ * Gets an async iterator for received messages
175
+ * @returns AsyncGenerator yielding received publish packets
176
+ */
126
177
  async *messages() {
127
178
  yield* this.ctx.incoming;
128
179
  }
180
+ /**
181
+ * Closes the message stream
182
+ * @param reason - Optional reason for closing
183
+ */
129
184
  closeMessages(reason) {
130
185
  this.ctx.incoming.close(reason);
131
186
  }
@@ -125,6 +125,10 @@ export class Context {
125
125
  }
126
126
  return false;
127
127
  }
128
+ // just an alias to clarify protocol flow
129
+ receivePubcomp(id) {
130
+ return this.receivePuback(id);
131
+ }
128
132
  receiveSuback(id, returnCodes) {
129
133
  const unresolvedMap = this.unresolvedSubscribe;
130
134
  if (unresolvedMap.has(id)) {
@@ -1,5 +1,11 @@
1
1
  import { ConnectionState } from "../ConnectionState.js";
2
2
  import { AuthenticationResultByNumber } from "../deps.js";
3
+ /**
4
+ * Handles the CONNACK packet received from the MQTT broker
5
+ * @param packet - The CONNACK packet containing the connection acknowledgment
6
+ * @param ctx - The connection context
7
+ * @returns Promise that resolves when handling is complete
8
+ */
3
9
  export async function handleConnack(packet, ctx) {
4
10
  if (packet.returnCode === 0) {
5
11
  ctx.connectionState = ConnectionState.connected;
@@ -8,6 +8,13 @@ import { handlePubcomp } from "./handlePubcomp.js";
8
8
  import { handleSuback } from "./handleSuback.js";
9
9
  import { handleUnsuback } from "./handleUnsuback.js";
10
10
  import { logger, PacketNameByType, PacketType } from "../deps.js";
11
+ /**
12
+ * Handles incoming MQTT packets based on the connection state and packet type
13
+ * @param ctx - The MQTT client context containing connection state and other information
14
+ * @param packet - The MQTT packet to handle
15
+ * @throws Error if an unexpected packet is received before connection is established
16
+ * @throws Error if an unexpected packet type is received after connection
17
+ */
11
18
  export async function handlePacket(ctx, packet) {
12
19
  logger.debug({ received: PacketNameByType[packet.type], packet });
13
20
  if (ctx.connectionState !== ConnectionState.connected) {
@@ -1,4 +1,11 @@
1
- // A PUBACK Packet is the response to a PUBLISH Packet with QoS level 1.
1
+ /**
2
+ * Handles PUBACK packet processing for QoS level 1 PUBLISH responses
3
+ * @param ctx - The MQTT client context
4
+ * @param packet - The PUBACK packet received from the broker
5
+ * @description
6
+ * - Removes the packet ID from the list of pending outgoing messages
7
+ * - Completes the corresponding publish operation
8
+ */
2
9
  export function handlePuback(ctx, packet) {
3
10
  const id = packet.id;
4
11
  ctx.store.pendingOutgoing.delete(id);
@@ -1,9 +1,16 @@
1
- // The PUBCOMP Packet is the response to a PUBREL Packet.
2
- // It is the fourth and final packet of the QoS 2 protocol exchange.
1
+ /**
2
+ * Handles PUBCOMP packets which are the response to PUBREL packets in QoS 2 flow
3
+ * @param ctx - The connection context containing the client state and store
4
+ * @param packet - The PUBCOMP packet received from the broker
5
+ * @description
6
+ * This function processes PUBCOMP packets which are the fourth and final packet
7
+ * in the QoS 2 protocol exchange. It removes the message from pendingAckOutgoing queue
8
+ * Completes the corresponding publish operation
9
+ */
3
10
  export function handlePubcomp(ctx, packet) {
4
11
  const id = packet.id;
5
12
  if (ctx.store.pendingAckOutgoing.has(id)) {
6
13
  ctx.store.pendingAckOutgoing.delete(id);
7
- ctx.receivePuback(id);
14
+ ctx.receivePubcomp(id);
8
15
  }
9
16
  }
@@ -1,5 +1,10 @@
1
1
  import { PacketType } from "../deps.js";
2
- // Incoming publish
2
+ /**
3
+ * Handles incoming MQTT publish packets based on their QoS level
4
+ * @param ctx - The MQTT connection context
5
+ * @param packet - The incoming publish packet to handle
6
+ * @returns Promise that resolves when packet is handled
7
+ */
3
8
  export async function handlePublish(ctx, packet) {
4
9
  const qos = packet.qos || 0;
5
10
  if (qos === 0) {
@@ -1,8 +1,17 @@
1
1
  import { PacketType } from "../deps.js";
2
- // A PUBREC Packet is the response to a PUBLISH Packet with QoS 2.
3
- // It is the second packet of the QoS 2 protocol exchange.
4
- // Discard message, Store PUBREC received <Packet Identifier>
5
- // send PUBREL <Packet Identifier>
2
+ /**
3
+ * Handles PUBREC (Publish Received) packets for QoS 2 message flow
4
+ * It is the second packet of the QoS 2 protocol exchange.
5
+ * @param ctx - The connection context containing message stores and send function
6
+ * @param packet - The received PUBREC packet
7
+ * @description
8
+ * When a PUBREC packet is received:
9
+ * 1. Checks if there is a pending outgoing message with matching packet ID
10
+ * 2. Creates a PUBREL packet to acknowledge the PUBREC
11
+ * 3. Add the packet ID to pendingAckOutgoing store
12
+ * 4. Removes the message from pendingOutgoing store
13
+ * 5. Sends the PUBREL packet
14
+ */
6
15
  export async function handlePubrec(ctx, packet) {
7
16
  const id = packet.id;
8
17
  const ack = {
@@ -1,8 +1,20 @@
1
1
  import { PacketType } from "../deps.js";
2
- // A PUBREL Packet is the response to a PUBREC Packet.
3
- // It is the third packet of the QoS 2 protocol exchange.
4
- // Method A, Initiate onward delivery of the Application Message then discard message
5
- // Send PUBCOMP <Packet Identifier>
2
+ /**
3
+ * Handles a PUBREL packet in the MQTT QoS 2 flow
4
+ * It is the third packet of the QoS 2 protocol exchange
5
+ * and a response to the PUBREC packet
6
+ *
7
+ * It uses Method A from the MQTT specification
8
+ * @param ctx - The MQTT client context
9
+ * @param packet - The PUBREL packet received
10
+ * @description
11
+ * When a PUBREL packet is received:
12
+ * 1. If there is a matching pending incoming message:
13
+ * - Delivers the stored message to the application
14
+ * - Removes the message from pending incoming store
15
+ * - Sends PUBCOMP packet as acknowledgement
16
+ * 2. If no matching message found, silently ignores
17
+ */
6
18
  export async function handlePubrel(ctx, packet) {
7
19
  const id = packet.id;
8
20
  if (ctx.store.pendingIncoming.has(id)) {
@@ -1,7 +1,12 @@
1
- // A SUBACK Packet is sent by the Server to the Client to confirm receipt
2
- // and processing of a SUBSCRIBE Packet.
3
- // A SUBACK Packet contains a list of return codes, that specify the maximum QoS level
4
- // that was granted in each Subscription that was requested by the SUBSCRIBE.
1
+ /**
2
+ * Handles SUBACK packets received from the server
3
+ * @param ctx - The MQTT client context
4
+ * @param packet - The SUBACK packet containing subscription confirmations
5
+ * @description
6
+ * - Processes SUBACK packets sent by server to confirm SUBSCRIBE requests
7
+ * - Removes the packet ID from pending outgoing messages
8
+ * - Notifies the client about granted QoS levels for each subscription
9
+ */
5
10
  export function handleSuback(ctx, packet) {
6
11
  const id = packet.id;
7
12
  ctx.store.pendingOutgoing.delete(id);
@@ -1,5 +1,13 @@
1
- // The UNSUBACK Packet is sent by the Server to the Client to confirm receipt
2
- // of an UNSUBSCRIBE Packet.
1
+ /**
2
+ * Handles the UNSUBACK packet received from the server
3
+ * @param ctx - The MQTT client context
4
+ * @param packet - The UNSUBACK packet received from the server
5
+ * @description
6
+ * The UNSUBACK Packet is sent by the Server to the Client to confirm receipt of
7
+ * an UNSUBSCRIBE Packet.
8
+ * When received, it removes the pending outgoing request and completes the corresponding
9
+ * unsubscribe operation.
10
+ */
3
11
  export function handleUnsuback(ctx, packet) {
4
12
  const id = packet.id;
5
13
  ctx.store.pendingOutgoing.delete(id);
@@ -1,16 +1,34 @@
1
+ /**
2
+ * @module memoryStore
3
+ */
1
4
  import { assert } from "./deps.js";
2
5
  import { maxPacketId, } from "./store.js";
6
+ /**
7
+ * In-memory implementation of the IStore interface for managing MQTT packet storage
8
+ */
3
9
  export class MemoryStore {
10
+ /** Current packet ID counter */
4
11
  packetId;
12
+ /** Map of pending incoming packets */
5
13
  pendingIncoming;
14
+ /** Map of pending outgoing packets */
6
15
  pendingOutgoing;
16
+ /** Map of pending acknowledgment outgoing packets */
7
17
  pendingAckOutgoing;
18
+ /**
19
+ * Creates a new MemoryStore instance
20
+ */
8
21
  constructor() {
9
22
  this.packetId = 0;
10
23
  this.pendingIncoming = new Map();
11
24
  this.pendingOutgoing = new Map();
12
25
  this.pendingAckOutgoing = new Map();
13
26
  }
27
+ /**
28
+ * Generates the next available packet ID
29
+ * @returns {PacketId} The next available packet ID
30
+ * @throws {Error} If no unused packet ID is available
31
+ */
14
32
  nextId() {
15
33
  const currentId = this.packetId;
16
34
  do {
@@ -25,6 +43,10 @@ export class MemoryStore {
25
43
  assert(this.packetId !== currentId, "No unused packetId available");
26
44
  return this.packetId;
27
45
  }
46
+ /**
47
+ * Asynchronously yields all pending outgoing packets
48
+ * @yields {PendingOutgoingPackets} Pending outgoing packets
49
+ */
28
50
  async *pendingOutgoingPackets() {
29
51
  for (const [_id, packet] of this.pendingAckOutgoing) {
30
52
  yield packet;
@@ -1 +1,4 @@
1
+ /**
2
+ * Maximum value for MQTT packet IDs (65535)
3
+ */
1
4
  export const maxPacketId = 0xffff;
@@ -9,5 +9,8 @@ export const AuthenticationResult = {
9
9
  badUsernameOrPassword: 4,
10
10
  notAuthorized: 5,
11
11
  };
12
+ /**
13
+ * Reverse lookup for AuthenticationResult
14
+ */
12
15
  export const AuthenticationResultByNumber = Object
13
16
  .fromEntries(Object.entries(AuthenticationResult).map(([k, v]) => [v, k]));
@@ -1,3 +1,8 @@
1
+ /**
2
+ * This module provides the encoding and decoding of MQTT packets
3
+ * it is used by the server and client modules
4
+ * @module
5
+ */
1
6
  import { PacketNameByType, PacketType } from "./PacketType.js";
2
7
  import { invalidTopic, invalidTopicFilter, invalidUTF8 } from "./validators.js";
3
8
  import { decodeLength, encodeLength } from "./length.js";
@@ -1,11 +1,23 @@
1
- // this a NodeJS specific implementation of TCP client sockets
2
- // it extends the platform agnostic Client class
1
+ /*
2
+ * this a NodeJS specific implementation of TCP client sockets
3
+ * it extends the platform agnostic Client class
4
+ * @module
5
+ */
3
6
  import { Client } from "../client/client.js";
4
7
  import { logger } from "../client/deps.js";
5
8
  import { readFile } from "node:fs/promises";
6
9
  import { createConnection } from "node:net";
7
10
  import * as tls from "node:tls";
8
11
  import { wrapNodeSocket } from "./wrapNodeSocket.js";
12
+ /**
13
+ * @function getFileData
14
+ * @param filename
15
+ * @returns Promise
16
+ *
17
+ * Fetches data from a file and returns it as a string
18
+ * @example
19
+ * const data = await getFileData("data.txt");
20
+ */
9
21
  export async function getFileData(filename) {
10
22
  if (!filename) {
11
23
  return;
@@ -16,6 +28,11 @@ export async function getFileData(filename) {
16
28
  }
17
29
  return data;
18
30
  }
31
+ /*
32
+ * TCPclient extends the Client class to provide TCP based clients
33
+ * it is used by the MQTTclient to connect to the broker
34
+ * see mqtt.ts in the /bin folder as an example
35
+ */
19
36
  export class TcpClient extends Client {
20
37
  connectMQTT(hostname, port = 1883) {
21
38
  logger.debug({ hostname, port });
@@ -1,6 +1,9 @@
1
1
  import { createServer } from "node:net";
2
2
  import { MqttServer } from "../server/mod.js";
3
3
  import { wrapNodeSocket } from "./wrapNodeSocket.js";
4
+ /*
5
+ * TCP server that wraps a MqttServer, see demoServer.ts in the /bin folder
6
+ */
4
7
  export class TcpServer {
5
8
  mqttServer;
6
9
  server;
@@ -9,16 +12,23 @@ export class TcpServer {
9
12
  this.mqttServer = new MqttServer(mqttOptions);
10
13
  this.serverOptions = serverOptions;
11
14
  }
12
- start() {
15
+ async start() {
13
16
  this.server = createServer((sock) => this.mqttServer.serve(wrapNodeSocket(sock)));
17
+ const isListening = new Promise((resolve) => {
18
+ this.server?.on("listening", () => {
19
+ resolve(true);
20
+ });
21
+ });
14
22
  this.server.listen(this.serverOptions.port, this.serverOptions.hostname);
23
+ await isListening;
24
+ return;
15
25
  }
16
26
  stop() {
17
27
  this.server?.close();
18
28
  }
19
29
  get port() {
20
30
  const address = this.server?.address();
21
- if (typeof address === "object") {
31
+ if (typeof address === "object" && address !== null) {
22
32
  return address?.port;
23
33
  }
24
34
  return this.serverOptions?.port;
@@ -1,9 +1,18 @@
1
1
  import { Writable } from "node:stream";
2
+ /**
3
+ * Closes a Node.js socket if it is not already closed
4
+ * @param sock - The Node.js socket to close
5
+ */
2
6
  function closer(sock) {
3
7
  if (!sock.closed) {
4
8
  sock.end();
5
9
  }
6
10
  }
11
+ /**
12
+ * Wraps a Node.js socket into a standard web streams-based socket connection
13
+ * @param socket - The Node.js socket to wrap
14
+ * @returns A socket connection object with readable/writable streams and close method
15
+ */
7
16
  export function wrapNodeSocket(socket) {
8
17
  const readable = new ReadableStream({
9
18
  type: "bytes",
@@ -2,6 +2,7 @@ import { Trie, } from "../deps.js";
2
2
  import { assert } from "../../utils/mod.js";
3
3
  const maxPacketId = 0xffff;
4
4
  export class MemoryStore {
5
+ existingSession = false;
5
6
  clientId;
6
7
  packetId;
7
8
  pendingIncoming;
@@ -1,20 +1,35 @@
1
1
  import { AuthenticationResult, logger, PacketType, Timer, } from "../deps.js";
2
+ /**
3
+ * Checks if the client is authenticated based on the provided credentials
4
+ * @param ctx - The connection context
5
+ * @param packet - The MQTT CONNECT packet
6
+ * @returns Authentication result indicating if the client is authenticated
7
+ */
2
8
  function isAuthenticated(ctx, packet) {
3
9
  if (ctx.handlers.isAuthenticated) {
4
10
  return ctx.handlers.isAuthenticated(ctx, packet.clientId || "", packet.username || "", packet.password || new Uint8Array(0));
5
11
  }
6
12
  return AuthenticationResult.ok;
7
13
  }
14
+ /**
15
+ * Validates the CONNECT packet
16
+ * @param ctx - The connection context
17
+ * @param packet - The MQTT CONNECT packet to validate
18
+ * @returns Authentication result indicating if the CONNECT packet is valid
19
+ */
8
20
  function validateConnect(ctx, packet) {
9
21
  if (packet.protocolLevel !== 4) {
10
22
  return AuthenticationResult.unacceptableProtocol;
11
23
  }
12
24
  return isAuthenticated(ctx, packet);
13
25
  }
14
- export function handleConnect(ctx, packet) {
15
- const clientId = packet.clientId || `Opifex-${crypto.randomUUID()}`;
16
- const returnCode = validateConnect(ctx, packet);
17
- // connect is ok
26
+ /**
27
+ * Processes the validated CONNECT packet
28
+ * @param packet - The MQTT CONNECT packet
29
+ * @param ctx - The connection context
30
+ * @param clientId - The client ID
31
+ */
32
+ function processValidatedConnect(returnCode, packet, ctx, clientId) {
18
33
  if (returnCode === AuthenticationResult.ok) {
19
34
  if (packet.will) {
20
35
  ctx.will = {
@@ -33,8 +48,27 @@ export function handleConnect(ctx, packet) {
33
48
  ctx.close();
34
49
  }, Math.floor(keepAlive * 1500));
35
50
  }
51
+ // is this a new session?
52
+ // either because its the first time for the client
53
+ // or it specifically asked for a clean one
54
+ const previousSession = ctx.store?.existingSession;
55
+ // client now has a history
56
+ if (!previousSession && ctx.store) {
57
+ ctx.store.existingSession = true;
58
+ }
59
+ return previousSession || false;
36
60
  }
37
- const sessionPresent = false;
61
+ return false;
62
+ }
63
+ /**
64
+ * Handles the MQTT CONNECT packet
65
+ * @param ctx - The connection context
66
+ * @param packet - The MQTT CONNECT packet to handle
67
+ */
68
+ export function handleConnect(ctx, packet) {
69
+ const clientId = packet.clientId || `Opifex-${crypto.randomUUID()}`;
70
+ const returnCode = validateConnect(ctx, packet);
71
+ const sessionPresent = processValidatedConnect(returnCode, packet, ctx, clientId);
38
72
  ctx.send({
39
73
  type: PacketType.connack,
40
74
  sessionPresent,
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Handles client disconnection by clearing the will message and closing the connection
3
+ * @param {Context} ctx - The connection context object
4
+ * @returns {void}
5
+ */
1
6
  export function handleDisconnect(ctx) {
2
7
  ctx.will = undefined;
3
8
  ctx.close();
@@ -10,6 +10,13 @@ import { handleSubscribe } from "./handleSubscribe.js";
10
10
  import { handleUnsubscribe } from "./handleUnsubscribe.js";
11
11
  import { handleDisconnect } from "./handleDisconnect.js";
12
12
  import { logger } from "../deps.js";
13
+ /**
14
+ * Handles incoming MQTT packets based on their type and connection state
15
+ * @param ctx - The connection context containing client state and configuration
16
+ * @param packet - The MQTT packet to handle
17
+ * @throws Error if receiving unexpected packet types or packets before connect
18
+ * @returns Promise that resolves when packet handling is complete
19
+ */
13
20
  export async function handlePacket(ctx, packet) {
14
21
  logger.debug("handling", PacketNameByType[packet.type]);
15
22
  logger.debug(JSON.stringify(packet, null, 2));
@@ -1,4 +1,9 @@
1
1
  import { PacketType } from "../deps.js";
2
+ /**
3
+ * Handles PINGREQ packet by responding with a PINGRESP packet
4
+ * @param ctx - The connection context containing send method
5
+ * @returns Promise that resolves when PINGRESP is sent
6
+ */
2
7
  export async function handlePingreq(ctx) {
3
8
  await ctx.send({
4
9
  type: PacketType.pingres,
@@ -1,4 +1,12 @@
1
- // A PUBACK Packet is the response to a PUBLISH Packet with QoS level 1.
1
+ /**
2
+ * Handles PUBACK (Publish Acknowledgment) packets in MQTT protocol
3
+ * @param ctx - The connection context containing the client's state and configuration
4
+ * @param packet - The PUBACK packet received from the client
5
+ * @description
6
+ * PUBACK packets are sent in response to PUBLISH packets with QoS level 1.
7
+ * This function removes the original PUBLISH packet from the pending outgoing messages store
8
+ * once acknowledgment is received.
9
+ */
2
10
  export function handlePuback(ctx, packet) {
3
11
  // qos 1 only
4
12
  const id = packet.id;
@@ -1,5 +1,11 @@
1
- // The PUBCOMP Packet is the response to a PUBREL Packet.
2
- // It is the fourth and final packet of the QoS 2 protocol exchange.
1
+ /**
2
+ * Handles PUBCOMP packets which are the response to PUBREL packets in QoS 2 flow
3
+ * @param ctx - The connection context containing the client state and configuration
4
+ * @param packet - The PUBCOMP packet received from the client
5
+ * @description
6
+ * This is the fourth and final packet of the QoS 2 protocol exchange.
7
+ * When received, it removes the message from the pendingAckOutgoing store.
8
+ */
3
9
  export function handlePubcomp(ctx, packet) {
4
10
  const id = packet.id;
5
11
  if (ctx.store?.pendingAckOutgoing.has(id)) {
@@ -1,5 +1,11 @@
1
1
  import { SysPrefix } from "../context.js";
2
2
  import { PacketType } from "../deps.js";
3
+ /**
4
+ * Checks if a client is authorized to publish to a given topic
5
+ * @param ctx - The connection context
6
+ * @param topic - The topic to check authorization for
7
+ * @returns boolean indicating if client is authorized to publish
8
+ */
3
9
  function authorizedToPublish(ctx, topic) {
4
10
  if (topic.startsWith(SysPrefix)) {
5
11
  return false;
@@ -9,6 +15,13 @@ function authorizedToPublish(ctx, topic) {
9
15
  }
10
16
  return true;
11
17
  }
18
+ /**
19
+ * Handles MQTT PUBLISH packets
20
+ * @param ctx - The connection context
21
+ * @param packet - The PUBLISH packet to process
22
+ * @returns Promise that resolves when packet is processed
23
+ * @throws Error if packet processing fails
24
+ */
12
25
  export async function handlePublish(ctx, packet) {
13
26
  if (!authorizedToPublish(ctx, packet.topic)) {
14
27
  return;
@@ -1,7 +1,15 @@
1
1
  import { PacketType } from "../deps.js";
2
- // qos 2
3
- // Discard message, Store PUBREC received <Packet Identifier>
4
- // send PUBREL <Packet Identifier>
2
+ /**
3
+ * Handles PUBREC (QoS 2 Publish Received) packets
4
+ * @param ctx - The connection context
5
+ * @param packet - The PUBREC packet received from the client
6
+ * @returns Promise that resolves when handling is complete
7
+ * @description
8
+ * For QoS 2 message flow:
9
+ * 1. Discards the original publish message
10
+ * 2. Stores that PUBREC was received for the packet ID
11
+ * 3. Sends PUBREL packet in response
12
+ */
5
13
  export async function handlePubrec(ctx, packet) {
6
14
  const id = packet.id;
7
15
  if (ctx.store?.pendingOutgoing.has(id)) {