@robot.com/better-mqtt
Version:
A modern, TypeScript-first MQTT client library that provides a better developer experience with async iterators, shared subscriptions, and React hooks. Better MQTT is a wrapper around the excellent [mqtt.js](https://github.com/mqttjs/MQTT.js) library, enh
455 lines (449 loc) • 11.2 kB
JavaScript
// src/index.ts
import { EventEmitter as EventEmitter2 } from "ee-ts";
import mqtt from "mqtt";
// src/match.ts
function matchTopic(topic, pattern) {
if (!topic || !pattern) {
return null;
}
const topicSegments = topic.split("/");
const patternSegments = pattern.split("/");
if (patternSegments[0] === "$share") {
if (patternSegments.length < 3) {
return null;
}
patternSegments.splice(0, 2);
} else if (patternSegments[0] === "$queue") {
if (patternSegments.length < 2) {
return null;
}
patternSegments.shift();
}
const params = [];
const patternLen = patternSegments.length;
const topicLen = topicSegments.length;
for (let i = 0; i < patternLen; i++) {
const patternSegment = patternSegments[i];
if (patternSegment === "#") {
if (i !== patternLen - 1) {
return null;
}
params.push(topicSegments.slice(i).join("/"));
return { params };
}
if (i >= topicLen) {
return null;
}
const topicSegment = topicSegments[i];
if (patternSegment === "+") {
params.push(topicSegment);
} else if (patternSegment !== topicSegment) {
return null;
}
}
if (topicLen > patternLen) {
return null;
}
return { params };
}
// src/subs-manager.ts
var SubscriptionGroup = class {
topic;
id;
subs;
options;
retainedMessage = null;
constructor(topic, id, options) {
this.topic = topic;
this.id = id;
this.subs = /* @__PURE__ */ new Set();
this.options = options;
}
handleMessage(topic, message, _packet, params) {
const match = params ? { params } : matchTopic(topic, this.topic);
if (!match) {
return;
}
if (this.options.rh < 2) {
this.retainedMessage = {
topic,
content: message,
params: match.params
};
}
for (const sub of this.subs) {
sub.handleMessage(message, topic, match.params);
}
}
add(sub) {
this.subs.add(sub);
if (this.retainedMessage) {
sub.handleMessage(
this.retainedMessage.content,
this.retainedMessage.topic,
this.retainedMessage.params
);
}
if (sub.options.qos !== this.options.qos) {
return true;
}
if (sub.options.rh !== this.options.rh) {
return true;
}
if (sub.options.rap !== this.options.rap) {
return true;
}
if (sub.options.nl !== this.options.nl) {
return true;
}
return false;
}
remove(sub) {
this.subs.delete(sub);
}
isEmpty() {
return this.subs.size === 0;
}
};
var SubscriptionManager = class {
nextSubIdentifier = 0;
/**
* Subscription groups by subscription identifier
*/
subsById = /* @__PURE__ */ new Map();
/**
* Subscription groups by topic
*/
subsByTopic = /* @__PURE__ */ new Map();
nextId() {
return ++this.nextSubIdentifier;
}
add(sub) {
const entry = this.subsByTopic.get(sub.topic);
if (entry) {
return {
resubscribe: entry.add(sub),
group: entry
};
}
const id = this.nextId();
const group = new SubscriptionGroup(sub.topic, id, sub.options);
this.subsById.set(id, group);
this.subsByTopic.set(sub.topic, group);
group.add(sub);
return {
resubscribe: true,
group
};
}
remove(sub) {
const group = this.subsByTopic.get(sub.topic);
if (!group) {
return null;
}
group.remove(sub);
if (group.isEmpty()) {
this.subsById.delete(group.id);
this.subsByTopic.delete(group.topic);
return group;
}
return group;
}
handleMessage(topic, message, packet) {
const subId = packet.properties?.subscriptionIdentifier;
if (!subId) {
for (const [subTopic, group] of this.subsByTopic.entries()) {
const match = matchTopic(topic, subTopic);
if (!match) {
continue;
}
group.handleMessage(topic, message, packet);
}
return;
}
const subsIds = Array.isArray(subId) ? subId : [subId];
for (const id of subsIds) {
const group = this.subsById.get(id);
if (!group) {
continue;
}
group.handleMessage(topic, message, packet);
}
}
all() {
const subs = [];
for (const group of this.subsById.values()) {
for (const sub of group.subs) {
subs.push(sub);
}
}
return subs;
}
};
// src/subscription.ts
import { EventEmitter } from "ee-ts";
// src/generator.ts
function createAsyncGenerator() {
let resolve = () => void 0;
let promise = new Promise((res) => {
resolve = res;
});
const queue = [];
let closed = false;
const push = (value) => {
if (closed) {
return;
}
queue.push({ type: "value", value });
resolve();
promise = new Promise((res) => {
resolve = res;
});
};
const throwError = (error) => {
if (closed) {
return;
}
queue.push({ type: "error", error });
resolve();
promise = new Promise((res) => {
resolve = res;
});
};
const end = () => {
if (closed) {
return;
}
closed = true;
queue.push({ type: "done" });
resolve();
};
async function* generator() {
while (true) {
if (queue.length > 0) {
const item = queue.shift();
if (item.type === "value") {
yield item.value;
} else if (item.type === "error") {
closed = true;
throw item.error;
} else if (item.type === "done") {
break;
}
} else {
await promise;
}
}
closed = true;
}
return { push, throwError, end, generator: generator() };
}
// src/subscription.ts
var Subscription = class extends EventEmitter {
mqtt;
generator;
topic;
/** Subscription options */
options;
parser;
constructor(opts) {
super();
this.mqtt = opts.mqtt;
this.topic = opts.topic;
this.options = {
qos: opts.options?.qos ?? 2,
rh: opts.options?.rh ?? 2,
rap: opts.options?.rap ?? false,
nl: opts.options?.nl ?? false
};
this.parser = opts.parser;
const { generator, push, end, throwError } = createAsyncGenerator();
this.on("message", (message) => {
push(message);
});
this.on("end", () => {
end();
});
this.on("error", (error) => {
throwError(error);
});
this.generator = generator;
}
handleMessage(message, topic, params) {
const parsedMessage = this.parser(message);
this.emit("message", { topic, content: parsedMessage, params });
}
// The method that makes the class async iterable
[Symbol.asyncIterator]() {
return this.generator;
}
end() {
this.mqtt.unsubscribe(this);
}
};
// src/index.ts
function stringParser(message) {
return message.toString("utf8");
}
function jsonParser(message) {
return JSON.parse(message.toString("utf8"));
}
function binaryParser(message) {
return message;
}
var BetterMQTT = class _BetterMQTT extends EventEmitter2 {
client;
error = null;
get status() {
return this.client.connected ? "online" : "offline";
}
subscriptions = new SubscriptionManager();
constructor(client) {
super();
this.client = client;
this.client.on("offline", () => {
this.emit("status", "offline");
});
this.client.on("connect", () => {
this.emit("status", "online");
});
this.client.on("connect", () => {
this.emit("status", "online");
});
this.client.on("error", (error) => {
this.error = error;
this.emit("error", error);
});
this.client.off("end", () => {
this.end(false);
});
this.client.on("message", (topic, message, packet) => {
this.subscriptions.handleMessage(topic, message, packet);
});
}
publish(topic, message, opts) {
this.client.publish(topic, message, {
qos: opts?.qos ?? 2,
dup: opts?.dup,
retain: opts?.retain
});
}
async publishAsync(topic, message, opts) {
await this.client.publishAsync(topic, message, {
qos: opts?.qos ?? 2,
dup: opts?.dup,
retain: opts?.retain
});
}
publishJson(topic, message) {
this.publish(topic, JSON.stringify(message));
}
async publishJsonAsync(topic, message) {
await this.publishAsync(topic, JSON.stringify(message));
}
unsubscribe(sub) {
sub.emit("end");
const group = this.subscriptions.remove(sub);
if (group?.isEmpty()) {
this.client.unsubscribe(group.topic);
}
}
subscribe(topic, parser, options) {
const sub = new Subscription({ mqtt: this, topic, parser, options });
const { resubscribe, group } = this.subscriptions.add(sub);
if (resubscribe) {
this.client.subscribe(sub.topic, {
qos: sub.options.qos,
rh: sub.options.rh,
rap: sub.options.rap,
nl: sub.options.nl,
properties: {
subscriptionIdentifier: group.id
}
});
}
return sub;
}
subscribeString(topic, options) {
return this.subscribe(topic, stringParser, options);
}
subscribeJson(topic, options) {
return this.subscribe(topic, jsonParser, options);
}
// TODO: Subscribe zod
subscribeBinary(topic, options) {
return this.subscribe(topic, binaryParser, options);
}
async unsubscribeAsync(sub) {
sub.emit("end");
const group = this.subscriptions.remove(sub);
if (group?.isEmpty()) {
await this.client.unsubscribeAsync(group.topic);
}
}
async subscribeAsync(topic, parser, options) {
const sub = new Subscription({ mqtt: this, topic, parser, options });
const { resubscribe, group } = this.subscriptions.add(sub);
if (resubscribe) {
await this.client.subscribeAsync(sub.topic, {
qos: sub.options.qos,
rh: sub.options.rh,
rap: sub.options.rap,
nl: sub.options.nl,
properties: {
subscriptionIdentifier: group.id
}
});
}
return sub;
}
async subscribeStringAsync(topic, options) {
return this.subscribeAsync(topic, stringParser, options);
}
async subscribeJsonAsync(topic, options) {
return this.subscribeAsync(topic, jsonParser, options);
}
async subscribeBinaryAsync(topic, options) {
return this.subscribeAsync(topic, binaryParser, options);
}
static async connectAsync(...args) {
const client = await mqtt.connectAsync(...args);
return new _BetterMQTT(client);
}
static connect(...args) {
const client = mqtt.connect(...args);
return new _BetterMQTT(client);
}
end(endClient = true) {
const subs = this.subscriptions.all();
for (const sub of subs) {
sub.emit("end");
if (endClient) {
this.unsubscribe(sub);
}
}
if (endClient) {
this.client.end();
}
this.emit("end");
}
async endAsync(endClient = true) {
const subs = this.subscriptions.all();
for (const sub of subs) {
sub.emit("end");
}
if (endClient) {
await Promise.all(subs.map((sub) => this.unsubscribeAsync(sub)));
await this.client.endAsync();
}
this.emit("end");
}
};
export {
BetterMQTT,
Subscription,
binaryParser,
jsonParser,
stringParser
};
//# sourceMappingURL=index.js.map