websocket-mqtt 0.0.7 → 0.0.9

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/dist/base.d.ts CHANGED
@@ -3,13 +3,12 @@ import { type ConnackPacket, type PublishPacket, type QoSLevel } from "./packets
3
3
  import { type EventEmitter } from "./utils/events.js";
4
4
  export type MqttEvents = {
5
5
  connect: [packet: ConnackPacket];
6
+ offline: [];
7
+ reconnect: [attempt: number, delayMs: number];
8
+ close: [];
6
9
  message: [topic: string, payload: Uint8Array, packet: PublishPacket];
7
10
  error: [error: unknown];
8
- close: [];
9
11
  };
10
- export type MqttOptions = {
11
- url: string;
12
- } & ConnectionOptions;
13
12
  export type MqttClient = EventEmitter<MqttEvents> & {
14
13
  connection: Connection;
15
14
  isConnected: () => boolean;
@@ -18,6 +17,7 @@ export type MqttClient = EventEmitter<MqttEvents> & {
18
17
  retain?: boolean;
19
18
  }) => Promise<void>;
20
19
  subscribe: (topic: string, qos?: QoSLevel) => Promise<number[]>;
20
+ unsubscribe: (topic: string | string[]) => Promise<void>;
21
21
  connect: () => Promise<void>;
22
22
  close: () => void;
23
23
  };
@@ -27,7 +27,8 @@ export type MqttClientInternalRequest<K> = {
27
27
  };
28
28
  export type MqttClientInternalPending = {
29
29
  sub: Map<number, MqttClientInternalRequest<number[]>>;
30
+ unsub: Map<number, MqttClientInternalRequest<void>>;
30
31
  pub: Map<number, MqttClientInternalRequest<void>>;
31
32
  };
32
- export declare const createMqtt: (options: MqttOptions) => MqttClient;
33
+ export declare const createMqtt: (options: ConnectionOptions) => MqttClient;
33
34
  //# sourceMappingURL=base.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,iBAAiB,EAElB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,aAAa,EAElB,KAAK,QAAQ,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAG1E,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;IACrE,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACxB,KAAK,EAAE,EAAE,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,iBAAiB,CAAC;AAEtB,MAAM,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,GAAG;IAClD,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B,OAAO,EAAE,CACP,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,QAAQ,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KAC3C,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IACzC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC9B,CAAC;AACF,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACtD,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;CACnD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,SAAS,WAAW,KAAG,UAqIjD,CAAC"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,iBAAiB,EAElB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,aAAa,EAElB,KAAK,QAAQ,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAG1E,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjC,OAAO,EAAE,EAAE,CAAC;IACZ,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC9C,KAAK,EAAE,EAAE,CAAC;IACV,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;IACrE,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,GAAG;IAClD,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,MAAM,OAAO,CAAC;IAC3B,OAAO,EAAE,CACP,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,QAAQ,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;KAAE,KAC5C,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IACzC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACtD,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC;CACnD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,SAAS,iBAAiB,KAAG,UAsLvD,CAAC"}
package/dist/base.js CHANGED
@@ -6,17 +6,28 @@ import { createLogger } from "./utils/logger.js";
6
6
  export const createMqtt = (options) => {
7
7
  const pending = {
8
8
  sub: new Map(),
9
+ unsub: new Map(),
9
10
  pub: new Map(),
10
11
  };
11
12
  const events = createEventEmitter();
12
13
  const log = createLogger(options);
13
14
  const connection = createConnection(options);
14
- connection.on("connect", (packet) => {
15
- events.emit("connect", packet);
16
- });
17
- connection.on("message", (topic, payload, packet) => {
18
- events.emit("message", topic, payload, packet);
19
- });
15
+ const rejectAllPending = () => {
16
+ for (const request of pending.sub.values()) {
17
+ request.reject(new Error("Connection closed"));
18
+ }
19
+ pending.sub.clear();
20
+ for (const request of pending.unsub.values()) {
21
+ request.reject(new Error("Connection closed"));
22
+ }
23
+ pending.unsub.clear();
24
+ for (const request of pending.pub.values()) {
25
+ request.reject(new Error("Connection closed"));
26
+ }
27
+ pending.pub.clear();
28
+ };
29
+ connection.on("connect", packet => events.emit("connect", packet));
30
+ connection.on("message", (topic, payload, packet) => events.emit("message", topic, payload, packet));
20
31
  connection.on("packet", (packet) => {
21
32
  switch (packet.type) {
22
33
  case PacketType.SUBACK: {
@@ -24,7 +35,7 @@ export const createMqtt = (options) => {
24
35
  const request = pending.sub.get(packet.messageId);
25
36
  if (request) {
26
37
  pending.sub.delete(packet.messageId);
27
- const hasFailure = packet.granted.some((qos) => qos === 0x80);
38
+ const hasFailure = packet.granted.includes(0x80);
28
39
  if (hasFailure) {
29
40
  request.reject(new Error("Subscription failed"));
30
41
  }
@@ -34,6 +45,15 @@ export const createMqtt = (options) => {
34
45
  }
35
46
  break;
36
47
  }
48
+ case PacketType.UNSUBACK: {
49
+ log("UNSUBACK", packet);
50
+ const request = pending.unsub.get(packet.messageId);
51
+ if (request) {
52
+ pending.unsub.delete(packet.messageId);
53
+ request.resolve();
54
+ }
55
+ break;
56
+ }
37
57
  case PacketType.PUBACK: {
38
58
  log("PUBACK", packet);
39
59
  const request = pending.pub.get(packet.messageId);
@@ -45,21 +65,19 @@ export const createMqtt = (options) => {
45
65
  }
46
66
  }
47
67
  });
48
- connection.on("error", (err) => {
49
- console.error("error", err);
50
- events.emit("error", err);
68
+ connection.on("error", err => events.emit("error", err));
69
+ connection.on("reconnect", (attempt, delayMs) => events.emit("reconnect", attempt, delayMs));
70
+ connection.on("offline", () => {
71
+ rejectAllPending();
72
+ events.emit("offline");
51
73
  });
52
74
  connection.on("close", () => {
53
- for (const request of pending.sub.values()) {
54
- request.reject(new Error("Connection closed"));
55
- }
56
- pending.sub.clear();
57
- for (const request of pending.pub.values()) {
58
- request.reject(new Error("Connection closed"));
59
- }
60
- pending.pub.clear();
75
+ rejectAllPending();
61
76
  events.emit("close");
62
77
  });
78
+ connection.on("reconnect", (attempt, delayMs) => {
79
+ events.emit("reconnect", attempt, delayMs);
80
+ });
63
81
  const publish = (topic, payload, options) => {
64
82
  const qos = options?.qos ?? QoS.AT_MOST_ONCE;
65
83
  const retain = options?.retain ?? false;
@@ -81,34 +99,53 @@ export const createMqtt = (options) => {
81
99
  pending.pub.set(messageId, { resolve, reject });
82
100
  });
83
101
  };
84
- const subscribe = (topic, qos = QoS.AT_MOST_ONCE) => {
85
- return new Promise((resolve, reject) => {
86
- const messageId = connection.nextMessageId();
87
- pending.sub.set(messageId, { resolve, reject });
88
- connection.send({
89
- type: PacketType.SUBSCRIBE,
90
- messageId,
91
- subscriptions: [{ topic, qos }],
92
- });
102
+ const subscribe = (topic, qos = QoS.AT_MOST_ONCE) => new Promise((resolve, reject) => {
103
+ const messageId = connection.nextMessageId();
104
+ pending.sub.set(messageId, { resolve, reject });
105
+ connection.send({
106
+ type: PacketType.SUBSCRIBE,
107
+ messageId,
108
+ subscriptions: [{ topic, qos }],
93
109
  });
94
- };
95
- const connect = () => {
96
- return new Promise((resolve, reject) => {
97
- connection.open();
98
- connection.on("connect", () => resolve());
99
- connection.on("error", (err) => reject(err));
110
+ });
111
+ const unsubscribe = (topic) => new Promise((resolve, reject) => {
112
+ const topics = Array.isArray(topic) ? topic : [topic];
113
+ const messageId = connection.nextMessageId();
114
+ pending.unsub.set(messageId, { resolve, reject });
115
+ connection.send({
116
+ type: PacketType.UNSUBSCRIBE,
117
+ messageId,
118
+ topics,
100
119
  });
101
- };
120
+ });
121
+ const connect = () => new Promise((resolve, reject) => {
122
+ // Resolves on the first successful connect (including after retries).
123
+ // Rejects only when the connection is permanently closed (retries exhausted
124
+ // or intentional close).
125
+ const onConnect = () => {
126
+ cleanup();
127
+ resolve();
128
+ };
129
+ const onClose = () => {
130
+ cleanup();
131
+ reject(new Error("Connection closed"));
132
+ };
133
+ const cleanup = () => {
134
+ connection.off("connect", onConnect);
135
+ connection.off("close", onClose);
136
+ };
137
+ connection.on("connect", onConnect);
138
+ connection.on("close", onClose);
139
+ connection.open();
140
+ });
102
141
  return {
103
142
  ...events,
104
143
  publish,
105
144
  subscribe,
145
+ unsubscribe,
106
146
  connection,
107
147
  isConnected: () => connection.isConnected(),
108
148
  connect,
109
- close: () => {
110
- log("closing from up above");
111
- connection.close();
112
- },
149
+ close: () => connection.close(),
113
150
  };
114
151
  };
@@ -1,5 +1,11 @@
1
1
  import { ConnackPacket, IncomingPacket, OutgoingPacket, PublishPacket, QoSLevel } from "./packets/index.js";
2
2
  import { type LogOptions } from "./utils/logger.js";
3
+ export type RetryOptions = {
4
+ retries?: number;
5
+ initialDelayMs?: number;
6
+ maxDelayMs?: number;
7
+ jitter?: boolean;
8
+ };
3
9
  export type ConnectionOptions = {
4
10
  url: string;
5
11
  clientId?: string;
@@ -14,6 +20,7 @@ export type ConnectionOptions = {
14
20
  retain?: boolean;
15
21
  };
16
22
  signal?: AbortSignal;
23
+ retry?: RetryOptions;
17
24
  } & LogOptions;
18
25
  export type HandlePacketFunction = (packet: IncomingPacket) => void;
19
26
  export type Connection = {
@@ -25,10 +32,12 @@ export type Connection = {
25
32
  };
26
33
  export type ConnectionEvents = {
27
34
  connect: [packet: ConnackPacket];
35
+ offline: [];
36
+ reconnect: [attempt: number, delayMs: number];
37
+ close: [];
28
38
  message: [topic: string, payload: Uint8Array, packet: PublishPacket];
29
39
  packet: [packet: IncomingPacket];
30
40
  error: [error: unknown];
31
- close: [];
32
41
  };
33
42
  export declare const DEFAULT_CLIENT_ID = "websocket_mqtt_";
34
43
  export declare const createConnection: (options: ConnectionOptions) => {
@@ -1 +1 @@
1
- {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,aAAa,EAGb,cAAc,EACd,cAAc,EAEd,aAAa,EAEb,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAElE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;QAC7B,GAAG,CAAC,EAAE,QAAQ,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,UAAU,CAAC;AAEf,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;AAEpE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACvC,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;IACrE,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACjC,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACxB,KAAK,EAAE,EAAE,CAAC;CACX,CAAC;AAEF,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAEnD,eAAO,MAAM,gBAAgB,GAAI,SAAS,iBAAiB;;;;;;mBA8CnC,cAAc,KAAG,IAAI;yBApBjB,MAAM;;CA2JjC,CAAC"}
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,aAAa,EAGb,cAAc,EACd,cAAc,EAEd,aAAa,EAEb,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAIlE,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;QAC7B,GAAG,CAAC,EAAE,QAAQ,CAAC;QACf,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,GAAG,UAAU,CAAC;AAEf,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;AAEpE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACvC,aAAa,EAAE,MAAM,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACjC,OAAO,EAAE,EAAE,CAAC;IACZ,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC9C,KAAK,EAAE,EAAE,CAAC;IACV,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;IACrE,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACjC,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;CACzB,CAAC;AAEF,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AAEnD,eAAO,MAAM,gBAAgB,GAAI,SAAS,iBAAiB;;;;;;mBAqEnC,cAAc,KAAG,IAAI;yBAVjB,MAAM;;CAwQjC,CAAC"}
@@ -3,15 +3,43 @@ import { decodeAll, DEFAULT_KEEPALIVE_SECONDS, PacketType, QoS, } from "./packet
3
3
  import { toUint8Array } from "./utils/buffer.js";
4
4
  import { createEventEmitter } from "./utils/events.js";
5
5
  import { createLogger } from "./utils/logger.js";
6
+ const FORCE_CLOSE_TIMEOUT_MS = 2000;
6
7
  export const DEFAULT_CLIENT_ID = "websocket_mqtt_";
7
8
  export const createConnection = (options) => {
8
9
  const events = createEventEmitter();
9
10
  const log = createLogger(options);
10
11
  let lastMessageId = 1;
11
12
  let connected = false;
12
- let pingInterval = null;
13
- let ws = null;
14
- const { clientId = DEFAULT_CLIENT_ID + Math.random().toString(36).substring(2, 15), username, password, keepalive = DEFAULT_KEEPALIVE_SECONDS, clean = true, url, signal, } = options;
13
+ let ws;
14
+ let activeAc;
15
+ const timers = {
16
+ pingInterval: undefined,
17
+ pingTimeout: undefined,
18
+ retry: undefined,
19
+ forceClose: undefined,
20
+ clearAll() {
21
+ if (this.pingInterval)
22
+ clearInterval(this.pingInterval);
23
+ if (this.pingTimeout)
24
+ clearTimeout(this.pingTimeout);
25
+ if (this.retry)
26
+ clearTimeout(this.retry);
27
+ if (this.forceClose)
28
+ clearTimeout(this.forceClose);
29
+ this.pingInterval = undefined;
30
+ this.pingTimeout = undefined;
31
+ this.retry = undefined;
32
+ this.forceClose = undefined;
33
+ },
34
+ };
35
+ const { clientId = DEFAULT_CLIENT_ID + Math.random().toString(36)
36
+ .slice(2, 15), username, password, keepalive = DEFAULT_KEEPALIVE_SECONDS, clean = true, url, signal, retry, } = options;
37
+ const maxRetries = Math.max(0, retry?.retries ?? 0);
38
+ const initialDelayMs = retry?.initialDelayMs ?? 1000;
39
+ const maxDelayMs = retry?.maxDelayMs ?? 30000;
40
+ const jitter = retry?.jitter ?? true;
41
+ let intentionalClose = false;
42
+ let retryAttempt = 0;
15
43
  const will = options.will
16
44
  ? {
17
45
  topic: options.will.topic,
@@ -25,15 +53,7 @@ export const createConnection = (options) => {
25
53
  lastMessageId = (messageId % 65535) + 1;
26
54
  return lastMessageId;
27
55
  };
28
- const startPingInterval = () => {
29
- log("starting ping interval");
30
- if (keepalive > 0) {
31
- const pingIntervalMs = (keepalive * 1000) / 2;
32
- pingInterval = setInterval(() => {
33
- sendRaw(encodePingreq());
34
- }, pingIntervalMs);
35
- }
36
- };
56
+ let receiveBuffer = new Uint8Array(0);
37
57
  const send = (packet) => {
38
58
  log("sending packet", packet);
39
59
  sendRaw(encodePacket(packet));
@@ -47,69 +67,160 @@ export const createConnection = (options) => {
47
67
  log("not sending raw data", data);
48
68
  }
49
69
  };
70
+ const scheduleRetry = () => {
71
+ if (maxRetries === 0 || (maxRetries !== Infinity && retryAttempt >= maxRetries)) {
72
+ events.emit("close");
73
+ return;
74
+ }
75
+ retryAttempt++;
76
+ const exponentialDelay = Math.min(initialDelayMs * 2 ** (retryAttempt - 1), maxDelayMs);
77
+ const delay = jitter
78
+ ? exponentialDelay * (0.5 + Math.random() * 0.5)
79
+ : exponentialDelay;
80
+ log(`scheduling retry #${retryAttempt} in ${Math.round(delay)}ms`);
81
+ events.emit("reconnect", retryAttempt, Math.round(delay));
82
+ timers.retry = setTimeout(() => {
83
+ timers.retry = undefined;
84
+ open();
85
+ }, delay);
86
+ };
50
87
  const close = () => {
51
88
  log("closing connection");
89
+ intentionalClose = true;
90
+ timers.clearAll();
91
+ // Detach all WebSocket listeners before closing to prevent handleDisconnect
92
+ if (activeAc) {
93
+ activeAc.abort();
94
+ activeAc = undefined;
95
+ }
52
96
  if (connected && ws) {
53
97
  sendRaw(encodeDisconnect());
54
98
  }
55
99
  ws?.close();
100
+ ws = undefined;
56
101
  connected = false;
57
- if (pingInterval) {
58
- clearInterval(pingInterval);
59
- pingInterval = null;
60
- }
102
+ events.emit("close");
61
103
  };
62
104
  const open = () => {
63
- ws = new WebSocket(url, ["mqtt"]);
64
- ws.binaryType = "arraybuffer";
65
- ws.onopen = () => {
66
- // sendConnect();
67
- const packet = encodeConnect({
68
- type: PacketType.CONNECT,
69
- clientId,
70
- username,
71
- password,
72
- keepalive,
73
- clean,
74
- will,
75
- });
76
- sendRaw(packet);
105
+ intentionalClose = false;
106
+ receiveBuffer = new Uint8Array(0);
107
+ // Abort previous WebSocket listeners and close socket if open() is called again
108
+ if (activeAc) {
109
+ activeAc.abort();
110
+ }
111
+ ws?.close();
112
+ const ac = new AbortController();
113
+ activeAc = ac;
114
+ const { signal: wsSignal } = ac;
115
+ let disconnected = false;
116
+ const handleDisconnect = () => {
117
+ if (disconnected)
118
+ return;
119
+ disconnected = true;
120
+ ac.abort();
121
+ activeAc = undefined;
122
+ log("ws:disconnect");
123
+ connected = false;
124
+ timers.clearAll();
125
+ if (!intentionalClose) {
126
+ events.emit("offline");
127
+ scheduleRetry();
128
+ }
77
129
  };
78
- let receiveBuffer = new Uint8Array(0);
79
- const handlePacket2 = (packet) => {
80
- log("handlePacket2", packet);
130
+ const handleConnectionLost = () => {
131
+ connected = false;
132
+ timers.clearAll();
133
+ ws?.close();
134
+ timers.forceClose = setTimeout(() => {
135
+ log("force closing stale connection");
136
+ timers.forceClose = undefined;
137
+ ws = undefined;
138
+ handleDisconnect();
139
+ }, FORCE_CLOSE_TIMEOUT_MS);
140
+ };
141
+ const startPingInterval = () => {
142
+ if (keepalive <= 0)
143
+ return;
144
+ log("starting ping interval");
145
+ const pingIntervalMs = (keepalive * 1000) / 2;
146
+ const pingTimeoutMs = keepalive * 1000;
147
+ timers.pingInterval = setInterval(() => {
148
+ sendRaw(encodePingreq());
149
+ if (!timers.pingTimeout) {
150
+ timers.pingTimeout = setTimeout(() => {
151
+ log("ping timeout, connection lost");
152
+ handleConnectionLost();
153
+ }, pingTimeoutMs);
154
+ }
155
+ }, pingIntervalMs);
156
+ };
157
+ const handlePacket = (packet) => {
158
+ log("handlePacket", packet);
159
+ // Any incoming packet proves the connection is alive, so reset the
160
+ // ping timeout (not just PINGRESP — the spec only requires PINGRESP,
161
+ // but any data is a valid liveness signal).
162
+ if (timers.pingTimeout) {
163
+ clearTimeout(timers.pingTimeout);
164
+ timers.pingTimeout = undefined;
165
+ }
81
166
  switch (packet.type) {
82
- case PacketType.CONNACK:
167
+ case PacketType.CONNACK: {
83
168
  if (packet.returnCode === 0) {
84
169
  connected = true;
170
+ retryAttempt = 0;
85
171
  startPingInterval();
86
172
  events.emit("connect", packet);
87
173
  log("connected");
88
174
  }
89
175
  else {
90
176
  const message = getConnackErrorMessage(packet.returnCode);
91
- console.error(message);
92
177
  events.emit("error", new Error(message));
93
178
  close();
94
179
  }
95
180
  break;
96
- case PacketType.PUBLISH:
181
+ }
182
+ case PacketType.PUBLISH: {
97
183
  log("received a publish packet");
98
184
  if (packet.qos === 1 && packet.messageId !== undefined) {
99
185
  send({ type: PacketType.PUBACK, messageId: packet.messageId });
100
186
  }
101
187
  events.emit("message", packet.topic, packet.payload, packet);
102
188
  break;
103
- case PacketType.PINGRESP:
189
+ }
190
+ case PacketType.PINGRESP: {
104
191
  log("PINGRESP");
105
192
  break;
106
- default:
193
+ }
194
+ default: {
107
195
  log("received a default packet");
108
196
  events.emit("packet", packet);
109
197
  break;
198
+ }
110
199
  }
111
200
  };
112
- ws.onmessage = (event) => {
201
+ try {
202
+ ws = new WebSocket(url, ["mqtt"]);
203
+ }
204
+ catch (error) {
205
+ log("WebSocket constructor threw", error);
206
+ events.emit("error", error);
207
+ scheduleRetry();
208
+ return;
209
+ }
210
+ ws.binaryType = "arraybuffer";
211
+ ws.addEventListener("open", () => {
212
+ const packet = encodeConnect({
213
+ type: PacketType.CONNECT,
214
+ clientId,
215
+ username,
216
+ password,
217
+ keepalive,
218
+ clean,
219
+ will,
220
+ });
221
+ sendRaw(packet);
222
+ }, { signal: wsSignal });
223
+ ws.addEventListener("message", (event) => {
113
224
  log("onmessage");
114
225
  const data = new Uint8Array(event.data);
115
226
  const combined = new Uint8Array(receiveBuffer.length + data.length);
@@ -120,17 +231,18 @@ export const createConnection = (options) => {
120
231
  receiveBuffer = remaining;
121
232
  log("packets to process", packets.length);
122
233
  for (const packet of packets) {
123
- log("packet", packet);
124
- handlePacket2(packet);
234
+ handlePacket(packet);
125
235
  }
126
- };
127
- ws.onerror = (event) => {
236
+ }, { signal: wsSignal });
237
+ ws.addEventListener("error", (event) => {
128
238
  events.emit("error", event);
129
- };
130
- ws.onclose = () => {
131
- log("ws:onclose");
132
- close();
133
- };
239
+ // If we were never connected, the close event may not fire (e.g. Node.js
240
+ // WebSocket on a failed connection attempt), so treat it as a disconnect.
241
+ if (!connected) {
242
+ handleDisconnect();
243
+ }
244
+ }, { signal: wsSignal });
245
+ ws.addEventListener("close", handleDisconnect, { signal: wsSignal });
134
246
  };
135
247
  signal?.addEventListener("abort", () => {
136
248
  log("abort signal received");
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./base.js";
2
+ export type { ConnectionOptions, RetryOptions } from "./connection.js";
2
3
  export type { MqttPacket, QoSLevel } from "./packets/index.js";
3
4
  export { QoS } from "./packets/index.js";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACvE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC"}
@@ -3,8 +3,8 @@ export interface DecodeResult {
3
3
  packet: IncomingPacket;
4
4
  bytesConsumed: number;
5
5
  }
6
- export declare function decode(bytes: Uint8Array): DecodeResult | null;
7
- export declare function decodeAll(bytes: Uint8Array): {
6
+ export declare const decode: (bytes: Uint8Array) => DecodeResult | null;
7
+ export declare const decodeAll: (bytes: Uint8Array) => {
8
8
  packets: IncomingPacket[];
9
9
  remaining: Uint8Array;
10
10
  };
@@ -1 +1 @@
1
- {"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/packets/decode.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,cAAc,EAKf,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,GAAG,IAAI,CAwC7D;AA2CD,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG;IAC5C,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,SAAS,EAAE,UAAU,CAAC;CACvB,CAcA"}
1
+ {"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/packets/decode.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,cAAc,EAMf,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,MAAM,GAAI,OAAO,UAAU,KAAG,YAAY,GAAG,IAqDzD,CAAC;AA6CF,eAAO,MAAM,SAAS,GAAI,OAAO,UAAU,KAAG;IAC5C,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,SAAS,EAAE,UAAU,CAAC;CAevB,CAAC"}
@@ -1,58 +1,71 @@
1
1
  import { createPacketReader } from "../utils/buffer.js";
2
2
  import { PacketType } from "./constants.js";
3
- export function decode(bytes) {
3
+ export const decode = (bytes) => {
4
+ // Preserve public API semantics: incomplete packets decode to null.
5
+ // eslint-disable-next-line unicorn/no-null
4
6
  if (bytes.length < 2)
5
7
  return null;
6
8
  const reader = createPacketReader(bytes);
7
9
  const firstByte = reader.readByte();
8
10
  const packetType = firstByte >> 4;
9
- const flags = firstByte & 0x0f;
11
+ const flags = firstByte & 0x0F;
10
12
  const remainingLength = reader.readVariableInt();
11
13
  const headerLength = reader.position;
12
14
  const totalLength = headerLength + remainingLength;
15
+ // eslint-disable-next-line unicorn/no-null
13
16
  if (bytes.length < totalLength)
14
17
  return null;
15
18
  const payload = reader.readBytes(remainingLength);
16
19
  const payloadReader = createPacketReader(payload);
17
20
  let packet;
18
21
  switch (packetType) {
19
- case PacketType.CONNACK:
22
+ case PacketType.CONNACK: {
20
23
  packet = decodeConnack(payloadReader);
21
24
  break;
22
- case PacketType.SUBACK:
25
+ }
26
+ case PacketType.SUBACK: {
23
27
  packet = decodeSuback(payloadReader);
24
28
  break;
25
- case PacketType.PUBLISH:
29
+ }
30
+ case PacketType.PUBLISH: {
26
31
  packet = decodePublish(payloadReader, flags);
27
32
  break;
28
- case PacketType.PUBACK:
33
+ }
34
+ case PacketType.PUBACK: {
29
35
  packet = decodePuback(payloadReader);
30
36
  break;
31
- case PacketType.PINGRESP:
37
+ }
38
+ case PacketType.UNSUBACK: {
39
+ packet = decodeUnsuback(payloadReader);
40
+ break;
41
+ }
42
+ case PacketType.PINGRESP: {
32
43
  packet = { type: PacketType.PINGRESP };
33
44
  break;
34
- default:
45
+ }
46
+ default: {
35
47
  throw new Error(`Unknown MQTT packet type: ${packetType}`);
48
+ }
36
49
  }
37
50
  return { packet, bytesConsumed: totalLength };
38
- }
39
- function decodeConnack(reader) {
51
+ };
52
+ const decodeConnack = (reader) => {
40
53
  const flags = reader.readByte();
41
54
  return {
42
55
  type: PacketType.CONNACK,
43
56
  sessionPresent: (flags & 0x01) === 1,
44
57
  returnCode: reader.readByte(),
45
58
  };
46
- }
47
- function decodeSuback(reader) {
59
+ };
60
+ const decodeSuback = (reader) => {
48
61
  const messageId = reader.readUint16();
49
62
  const granted = [];
50
63
  while (reader.remaining > 0) {
51
64
  granted.push(reader.readByte());
52
65
  }
53
66
  return { type: PacketType.SUBACK, messageId, granted };
54
- }
55
- function decodePublish(reader, flags) {
67
+ };
68
+ const decodePublish = (reader, flags) => {
56
69
  const topic = reader.readString();
57
70
  const qos = ((flags >> 1) & 0x03);
58
71
  const messageId = qos > 0 ? reader.readUint16() : undefined;
@@ -65,11 +78,10 @@ function decodePublish(reader, flags) {
65
78
  dup: (flags & 0x08) !== 0,
66
79
  messageId,
67
80
  };
68
- }
69
- function decodePuback(reader) {
70
- return { type: PacketType.PUBACK, messageId: reader.readUint16() };
71
- }
72
- export function decodeAll(bytes) {
81
+ };
82
+ const decodePuback = (reader) => ({ type: PacketType.PUBACK, messageId: reader.readUint16() });
83
+ const decodeUnsuback = (reader) => ({ type: PacketType.UNSUBACK, messageId: reader.readUint16() });
84
+ export const decodeAll = (bytes) => {
73
85
  const packets = [];
74
86
  let offset = 0;
75
87
  while (offset < bytes.length) {
@@ -80,4 +92,4 @@ export function decodeAll(bytes) {
80
92
  offset += result.bytesConsumed;
81
93
  }
82
94
  return { packets, remaining: bytes.subarray(offset) };
83
- }
95
+ };
@@ -1,9 +1,10 @@
1
- import type { ConnectPacket, OutgoingPacket, PubackPacket, PublishPacket, SubscribePacket } from "./types.js";
2
- export declare function encodeConnect(packet: ConnectPacket): Uint8Array;
3
- export declare function encodeSubscribe(packet: SubscribePacket): Uint8Array;
4
- export declare function encodePublish(packet: PublishPacket): Uint8Array;
5
- export declare function encodePuback(packet: PubackPacket): Uint8Array;
6
- export declare function encodePingreq(): Uint8Array;
7
- export declare function encodeDisconnect(): Uint8Array;
1
+ import type { ConnectPacket, OutgoingPacket, PubackPacket, PublishPacket, SubscribePacket, UnsubscribePacket } from "./types.js";
2
+ export declare const encodeConnect: (packet: ConnectPacket) => Uint8Array;
3
+ export declare const encodeSubscribe: (packet: SubscribePacket) => Uint8Array;
4
+ export declare const encodeUnsubscribe: (packet: UnsubscribePacket) => Uint8Array;
5
+ export declare const encodePublish: (packet: PublishPacket) => Uint8Array;
6
+ export declare const encodePuback: (packet: PubackPacket) => Uint8Array;
7
+ export declare const encodePingreq: () => Uint8Array;
8
+ export declare const encodeDisconnect: () => Uint8Array;
8
9
  export declare const encodePacket: (packet: OutgoingPacket) => Uint8Array;
9
10
  //# sourceMappingURL=encode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/packets/encode.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,eAAe,EAChB,MAAM,YAAY,CAAC;AAoBpB,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU,CAqC/D;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,UAAU,CASnE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU,CAkB/D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,UAAU,CAI7D;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED,wBAAgB,gBAAgB,IAAI,UAAU,CAE7C;AAED,eAAO,MAAM,YAAY,GAAI,QAAQ,cAAc,KAAG,UAiBrD,CAAC"}
1
+ {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/packets/encode.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,YAAY,EACZ,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAoBpB,eAAO,MAAM,aAAa,GAAI,QAAQ,aAAa,KAAG,UAoClD,CAAC;AAEL,eAAO,MAAM,eAAe,GAAI,QAAQ,eAAe,KAAG,UAQtD,CAAC;AAEL,eAAO,MAAM,iBAAiB,GAAI,QAAQ,iBAAiB,KAAG,UAO1D,CAAC;AAEL,eAAO,MAAM,aAAa,GAAI,QAAQ,aAAa,KAAG,UAkBrD,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,KAAG,UAGhD,CAAC;AAEL,eAAO,MAAM,aAAa,QAAO,UACa,CAAC;AAE/C,eAAO,MAAM,gBAAgB,QAAO,UACa,CAAC;AAElD,eAAO,MAAM,YAAY,GAAI,QAAQ,cAAc,KAAG,UAyBrD,CAAC"}
@@ -1,57 +1,59 @@
1
1
  import { createPacketWriter, toUint8Array, } from "../utils/buffer.js";
2
2
  import { ConnectFlags, DEFAULT_KEEPALIVE_SECONDS, PacketType, PROTOCOL_NAME, PROTOCOL_VERSION, } from "./constants.js";
3
- function writePacket(type, flags, writeBody) {
3
+ const writePacket = (type, flags, writeBody) => {
4
4
  const body = createPacketWriter();
5
5
  writeBody(body);
6
6
  const packet = createPacketWriter();
7
- packet.writeByte((type << 4) | (flags & 0x0f));
7
+ packet.writeByte((type << 4) | (flags & 0x0F));
8
8
  packet.writeVariableInt(body.length);
9
9
  packet.writeBytes(body.toUint8Array());
10
10
  return packet.toUint8Array();
11
- }
12
- export function encodeConnect(packet) {
13
- return writePacket(PacketType.CONNECT, 0, (writer) => {
14
- // Variable header
15
- writer.writeString(PROTOCOL_NAME);
16
- writer.writeByte(PROTOCOL_VERSION);
17
- // Connect flags
18
- let flags = 0;
19
- if (packet.clean)
20
- flags |= ConnectFlags.CLEAN_SESSION;
21
- if (packet.will) {
22
- flags |= ConnectFlags.WILL_FLAG;
23
- flags |= packet.will.qos << 3;
24
- if (packet.will.retain)
25
- flags |= ConnectFlags.WILL_RETAIN;
26
- }
27
- if (packet.password !== undefined)
28
- flags |= ConnectFlags.PASSWORD;
29
- if (packet.username !== undefined)
30
- flags |= ConnectFlags.USERNAME;
31
- writer.writeByte(flags);
32
- writer.writeUint16(packet.keepalive ?? DEFAULT_KEEPALIVE_SECONDS);
33
- // Payload
34
- writer.writeString(packet.clientId);
35
- if (packet.will) {
36
- writer.writeString(packet.will.topic);
37
- writer.writeBinary(packet.will.payload);
38
- }
39
- if (packet.username !== undefined)
40
- writer.writeString(packet.username);
41
- if (packet.password !== undefined)
42
- writer.writeString(packet.password);
43
- });
44
- }
45
- export function encodeSubscribe(packet) {
46
- return writePacket(PacketType.SUBSCRIBE, 0x02, (writer) => {
47
- writer.writeUint16(packet.messageId);
48
- for (const sub of packet.subscriptions) {
49
- writer.writeString(sub.topic);
50
- writer.writeByte(sub.qos);
51
- }
52
- });
53
- }
54
- export function encodePublish(packet) {
11
+ };
12
+ export const encodeConnect = (packet) => writePacket(PacketType.CONNECT, 0, (writer) => {
13
+ // Variable header
14
+ writer.writeString(PROTOCOL_NAME);
15
+ writer.writeByte(PROTOCOL_VERSION);
16
+ // Connect flags
17
+ let flags = 0;
18
+ if (packet.clean)
19
+ flags |= ConnectFlags.CLEAN_SESSION;
20
+ if (packet.will) {
21
+ flags |= ConnectFlags.WILL_FLAG;
22
+ flags |= packet.will.qos << 3;
23
+ if (packet.will.retain)
24
+ flags |= ConnectFlags.WILL_RETAIN;
25
+ }
26
+ if (packet.password !== undefined)
27
+ flags |= ConnectFlags.PASSWORD;
28
+ if (packet.username !== undefined)
29
+ flags |= ConnectFlags.USERNAME;
30
+ writer.writeByte(flags);
31
+ writer.writeUint16(packet.keepalive ?? DEFAULT_KEEPALIVE_SECONDS);
32
+ // Payload
33
+ writer.writeString(packet.clientId);
34
+ if (packet.will) {
35
+ writer.writeString(packet.will.topic);
36
+ writer.writeBinary(packet.will.payload);
37
+ }
38
+ if (packet.username !== undefined)
39
+ writer.writeString(packet.username);
40
+ if (packet.password !== undefined)
41
+ writer.writeString(packet.password);
42
+ });
43
+ export const encodeSubscribe = (packet) => writePacket(PacketType.SUBSCRIBE, 0x02, (writer) => {
44
+ writer.writeUint16(packet.messageId);
45
+ for (const sub of packet.subscriptions) {
46
+ writer.writeString(sub.topic);
47
+ writer.writeByte(sub.qos);
48
+ }
49
+ });
50
+ export const encodeUnsubscribe = (packet) => writePacket(PacketType.UNSUBSCRIBE, 0x02, (writer) => {
51
+ writer.writeUint16(packet.messageId);
52
+ for (const topic of packet.topics) {
53
+ writer.writeString(topic);
54
+ }
55
+ });
56
+ export const encodePublish = (packet) => {
55
57
  let flags = 0;
56
58
  if (packet.dup)
57
59
  flags |= 0x08;
@@ -65,33 +67,35 @@ export function encodePublish(packet) {
65
67
  }
66
68
  writer.writeBytes(toUint8Array(packet.payload));
67
69
  });
68
- }
69
- export function encodePuback(packet) {
70
- return writePacket(PacketType.PUBACK, 0, (writer) => {
71
- writer.writeUint16(packet.messageId);
72
- });
73
- }
74
- export function encodePingreq() {
75
- return new Uint8Array([PacketType.PINGREQ << 4, 0]);
76
- }
77
- export function encodeDisconnect() {
78
- return new Uint8Array([PacketType.DISCONNECT << 4, 0]);
79
- }
70
+ };
71
+ export const encodePuback = (packet) => writePacket(PacketType.PUBACK, 0, (writer) => {
72
+ writer.writeUint16(packet.messageId);
73
+ });
74
+ export const encodePingreq = () => new Uint8Array([PacketType.PINGREQ << 4, 0]);
75
+ export const encodeDisconnect = () => new Uint8Array([PacketType.DISCONNECT << 4, 0]);
80
76
  export const encodePacket = (packet) => {
81
77
  switch (packet.type) {
82
- case PacketType.CONNECT:
78
+ case PacketType.CONNECT: {
83
79
  return encodeConnect(packet);
84
- case PacketType.SUBSCRIBE:
80
+ }
81
+ case PacketType.SUBSCRIBE: {
85
82
  return encodeSubscribe(packet);
86
- case PacketType.PUBLISH:
83
+ }
84
+ case PacketType.UNSUBSCRIBE: {
85
+ return encodeUnsubscribe(packet);
86
+ }
87
+ case PacketType.PUBLISH: {
87
88
  return encodePublish(packet);
88
- case PacketType.PUBACK:
89
+ }
90
+ case PacketType.PUBACK: {
89
91
  return encodePuback(packet);
92
+ }
90
93
  // case PacketType.DISCONNECT:
91
94
  // return encodeDisconnect();
92
95
  // case PacketType.PINGREQ:
93
96
  // return encodePingreq();
94
- default:
97
+ default: {
95
98
  throw new Error(`Unknown packet type: ${packet["type"]}`);
99
+ }
96
100
  }
97
101
  };
@@ -47,6 +47,15 @@ export type SubackPacket = BasePacket & {
47
47
  messageId: number;
48
48
  granted: number[];
49
49
  };
50
+ export type UnsubscribePacket = BasePacket & {
51
+ type: 10;
52
+ messageId: number;
53
+ topics: string[];
54
+ };
55
+ export type UnsubackPacket = BasePacket & {
56
+ type: 11;
57
+ messageId: number;
58
+ };
50
59
  export type PingreqPacket = BasePacket & {
51
60
  type: 12;
52
61
  };
@@ -56,7 +65,7 @@ export type PingrespPacket = BasePacket & {
56
65
  export type DisconnectPacket = BasePacket & {
57
66
  type: 14;
58
67
  };
59
- export type MqttPacket = ConnectPacket | ConnackPacket | PublishPacket | PubackPacket | SubscribePacket | SubackPacket | PingreqPacket | PingrespPacket | DisconnectPacket;
60
- export type IncomingPacket = ConnackPacket | PublishPacket | PubackPacket | SubackPacket | PingrespPacket;
61
- export type OutgoingPacket = ConnectPacket | SubscribePacket | PublishPacket | PubackPacket | DisconnectPacket | PingreqPacket;
68
+ export type MqttPacket = ConnectPacket | ConnackPacket | PublishPacket | PubackPacket | SubscribePacket | SubackPacket | UnsubscribePacket | UnsubackPacket | PingreqPacket | PingrespPacket | DisconnectPacket;
69
+ export type IncomingPacket = ConnackPacket | PublishPacket | PubackPacket | SubackPacket | UnsubackPacket | PingrespPacket;
70
+ export type OutgoingPacket = ConnectPacket | SubscribePacket | UnsubscribePacket | PublishPacket | PubackPacket | DisconnectPacket | PingreqPacket;
62
71
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/packets/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAGF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG;IACvC,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,UAAU,CAAC;QACpB,GAAG,EAAE,QAAQ,CAAC;QACd,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;CACH,CAAC;AAGF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG;IACvC,IAAI,EAAE,CAAC,CAAC;IACR,cAAc,EAAE,OAAO,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG;IACvC,IAAI,EAAE,CAAC,CAAC;IACR,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,UAAU,CAAC;IACpB,GAAG,EAAE,QAAQ,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG;IACtC,IAAI,EAAE,CAAC,CAAC;IACR,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAGF,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG;IACzC,IAAI,EAAE,CAAC,CAAC;IACR,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,KAAK,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,QAAQ,CAAC;KACf,CAAC,CAAC;CACJ,CAAC;AAGF,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG;IACtC,IAAI,EAAE,CAAC,CAAC;IACR,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAGF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG;IACvC,IAAI,EAAE,EAAE,CAAC;CACV,CAAC;AAGF,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG;IACxC,IAAI,EAAE,EAAE,CAAC;CACV,CAAC;AAGF,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAC1C,IAAI,EAAE,EAAE,CAAC;CACV,CAAC;AAEF,MAAM,MAAM,UAAU,GAClB,aAAa,GACb,aAAa,GACb,aAAa,GACb,YAAY,GACZ,eAAe,GACf,YAAY,GACZ,aAAa,GACb,cAAc,GACd,gBAAgB,CAAC;AAErB,MAAM,MAAM,cAAc,GACtB,aAAa,GACb,aAAa,GACb,YAAY,GACZ,YAAY,GACZ,cAAc,CAAC;AAEnB,MAAM,MAAM,cAAc,GACtB,aAAa,GACb,eAAe,GACf,aAAa,GACb,YAAY,GACZ,gBAAgB,GAChB,aAAa,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/packets/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAGF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG;IACvC,IAAI,EAAE,CAAC,CAAC;IACR,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,UAAU,CAAC;QACpB,GAAG,EAAE,QAAQ,CAAC;QACd,MAAM,EAAE,OAAO,CAAC;KACjB,CAAC;CACH,CAAC;AAGF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG;IACvC,IAAI,EAAE,CAAC,CAAC;IACR,cAAc,EAAE,OAAO,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG;IACvC,IAAI,EAAE,CAAC,CAAC;IACR,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,UAAU,CAAC;IACpB,GAAG,EAAE,QAAQ,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG;IACtC,IAAI,EAAE,CAAC,CAAC;IACR,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAGF,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG;IACzC,IAAI,EAAE,CAAC,CAAC;IACR,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,KAAK,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,QAAQ,CAAC;KACf,CAAC,CAAC;CACJ,CAAC;AAGF,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG;IACtC,IAAI,EAAE,CAAC,CAAC;IACR,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAGF,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG;IAC3C,IAAI,EAAE,EAAE,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAGF,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG;IACxC,IAAI,EAAE,EAAE,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAGF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG;IACvC,IAAI,EAAE,EAAE,CAAC;CACV,CAAC;AAGF,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG;IACxC,IAAI,EAAE,EAAE,CAAC;CACV,CAAC;AAGF,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG;IAC1C,IAAI,EAAE,EAAE,CAAC;CACV,CAAC;AAEF,MAAM,MAAM,UAAU,GAClB,aAAa,GACb,aAAa,GACb,aAAa,GACb,YAAY,GACZ,eAAe,GACf,YAAY,GACZ,iBAAiB,GACjB,cAAc,GACd,aAAa,GACb,cAAc,GACd,gBAAgB,CAAC;AAErB,MAAM,MAAM,cAAc,GACtB,aAAa,GACb,aAAa,GACb,YAAY,GACZ,YAAY,GACZ,cAAc,GACd,cAAc,CAAC;AAEnB,MAAM,MAAM,cAAc,GACtB,aAAa,GACb,eAAe,GACf,iBAAiB,GACjB,aAAa,GACb,YAAY,GACZ,gBAAgB,GAChB,aAAa,CAAC"}
@@ -1,5 +1,5 @@
1
- export declare function toUint8Array(data: string | Uint8Array): Uint8Array;
2
- export declare function createPacketWriter(initialSize?: number): {
1
+ export declare const toUint8Array: (data: string | Uint8Array) => Uint8Array;
2
+ export declare const createPacketWriter: (initialSize?: number) => {
3
3
  writeByte(value: number): /*elided*/ any;
4
4
  writeUint16(value: number): /*elided*/ any;
5
5
  writeBytes(data: Uint8Array): /*elided*/ any;
@@ -9,7 +9,7 @@ export declare function createPacketWriter(initialSize?: number): {
9
9
  toUint8Array(): Uint8Array;
10
10
  readonly length: number;
11
11
  };
12
- export declare function createPacketReader(buffer: Uint8Array): {
12
+ export declare const createPacketReader: (buffer: Uint8Array) => {
13
13
  readonly remaining: number;
14
14
  readonly position: number;
15
15
  readByte(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../src/utils/buffer.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,UAAU,CAElE;AAED,wBAAgB,kBAAkB,CAAC,WAAW,SAAM;qBAoB/B,MAAM;uBAOJ,MAAM;qBAQR,UAAU;uBAQR,MAAM;sBASP,UAAU;4BAOJ,MAAM;oBAcd,UAAU;qBAIZ,MAAM;EAMvB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU;wBAIhC,MAAM;uBAIP,MAAM;gBAIV,MAAM;kBAIJ,MAAM;sBAQF,MAAM,GAAG,UAAU;kBAQvB,MAAM;uBAWD,MAAM;gBAcb,UAAU;EAQzB;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"buffer.d.ts","sourceRoot":"","sources":["../../src/utils/buffer.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,GAAG,UAAU,KAAG,UACK,CAAC;AAE/D,eAAO,MAAM,kBAAkB,GAAI,oBAAiB;qBAoB/B,MAAM;uBAOJ,MAAM;qBAQR,UAAU;uBAQR,MAAM;sBASP,UAAU;4BAOJ,MAAM;oBAcd,UAAU;qBAIZ,MAAM;CAMvB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,UAAU;wBAIhC,MAAM;uBAIP,MAAM;gBAIV,MAAM;kBAIJ,MAAM;sBAQF,MAAM,GAAG,UAAU;kBAQvB,MAAM;uBAWD,MAAM;gBAcb,UAAU;CAQzB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -1,9 +1,7 @@
1
1
  const textEncoder = new TextEncoder();
2
2
  const textDecoder = new TextDecoder();
3
- export function toUint8Array(data) {
4
- return typeof data === "string" ? textEncoder.encode(data) : data;
5
- }
6
- export function createPacketWriter(initialSize = 256) {
3
+ export const toUint8Array = (data) => (typeof data === "string" ? textEncoder.encode(data) : data);
4
+ export const createPacketWriter = (initialSize = 256) => {
7
5
  let buffer = new Uint8Array(initialSize);
8
6
  let position = 0;
9
7
  /**
@@ -11,14 +9,14 @@ export function createPacketWriter(initialSize = 256) {
11
9
  * If the buffer is too small, it doubles in size or expands to fit the needed bytes,
12
10
  * whichever is larger, and copies existing data to the new buffer.
13
11
  */
14
- function ensureCapacity(bytesNeeded) {
12
+ const ensureCapacity = (bytesNeeded) => {
15
13
  if (position + bytesNeeded > buffer.length) {
16
14
  const newSize = Math.max(buffer.length * 2, position + bytesNeeded);
17
15
  const newBuffer = new Uint8Array(newSize);
18
16
  newBuffer.set(buffer);
19
17
  buffer = newBuffer;
20
18
  }
21
- }
19
+ };
22
20
  const writer = {
23
21
  writeByte(value) {
24
22
  ensureCapacity(1);
@@ -27,8 +25,8 @@ export function createPacketWriter(initialSize = 256) {
27
25
  },
28
26
  writeUint16(value) {
29
27
  ensureCapacity(2);
30
- buffer[position++] = (value >> 8) & 0xff;
31
- buffer[position++] = value & 0xff;
28
+ buffer[position++] = (value >> 8) & 0xFF;
29
+ buffer[position++] = value & 0xFF;
32
30
  return writer;
33
31
  },
34
32
  writeBytes(data) {
@@ -66,8 +64,8 @@ export function createPacketWriter(initialSize = 256) {
66
64
  },
67
65
  };
68
66
  return writer;
69
- }
70
- export function createPacketReader(buffer) {
67
+ };
68
+ export const createPacketReader = (buffer) => {
71
69
  let position = 0;
72
70
  return {
73
71
  get remaining() {
@@ -102,7 +100,7 @@ export function createPacketReader(buffer) {
102
100
  let byte;
103
101
  do {
104
102
  byte = buffer[position++];
105
- value += (byte & 0x7f) * multiplier;
103
+ value += (byte & 0x7F) * multiplier;
106
104
  multiplier *= 128;
107
105
  } while (byte & 0x80);
108
106
  return value;
@@ -113,4 +111,4 @@ export function createPacketReader(buffer) {
113
111
  return data;
114
112
  },
115
113
  };
116
- }
114
+ };
@@ -14,7 +14,7 @@ export const createEventEmitter = () => {
14
14
  },
15
15
  emit(event, ...args) {
16
16
  if (listeners.has(event)) {
17
- listeners.get(event).forEach((listener) => listener(...args));
17
+ listeners.get(event).forEach(listener => listener(...args));
18
18
  }
19
19
  },
20
20
  };
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAElD,eAAO,MAAM,YAAY,GACtB,UAAU,UAAU,KAAG,MAKvB,CAAC"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;AAElD,eAAO,MAAM,YAAY,GACpB,UAAU,UAAU,KAAG,MAKvB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "websocket-mqtt",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A lightweight MQTT WebSocket client library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -23,26 +23,16 @@
23
23
  "author": "",
24
24
  "license": "LGPL-3.0",
25
25
  "devDependencies": {
26
- "@eslint/js": "^9.20.0",
27
- "@stylistic/eslint-plugin": "^5.7.1",
28
- "eslint": "^9.20.0",
29
- "eslint-config-prettier": "^10.1.8",
30
- "eslint-plugin-import": "^2.32.0",
31
- "eslint-plugin-prettier": "^5.5.5",
32
- "eslint-plugin-simple-import-sort": "^12.1.1",
33
- "eslint-plugin-sonarjs": "^3.0.5",
34
- "eslint-plugin-unicorn": "^62.0.0",
35
- "eslint-plugin-unused-imports": "^4.3.0",
26
+ "eslint": "^9",
27
+ "eslint-plugin-v3xlabs": "1.7.11",
36
28
  "mqtt": "^5.14.1",
37
- "prettier": "^3.8.1",
38
29
  "typescript": "^5.9.3",
39
- "typescript-eslint": "^8.20.0",
40
30
  "vitest": "^4.0.18"
41
31
  },
42
32
  "scripts": {
43
33
  "test": "vitest",
44
34
  "build": "tsc",
45
- "lint": "eslint .",
46
- "lint:fix": "eslint . --fix"
35
+ "lint": "eslint",
36
+ "lint:fix": "eslint --fix"
47
37
  }
48
38
  }