@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
262 lines (259 loc) • 6.83 kB
JavaScript
// src/index.ts
import { EventEmitter } from "ee-ts";
import mqtt from "mqtt";
// 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) {
console.warn("Attempted to push to a closed generator.");
return;
}
queue.push({ type: "value", value });
resolve();
promise = new Promise((res) => {
resolve = res;
});
};
const throwError = (error) => {
if (closed) {
console.warn("Attempted to throw error to a closed generator.");
return;
}
queue.push({ type: "error", error });
resolve();
promise = new Promise((res) => {
resolve = res;
});
};
const end = () => {
if (closed) {
console.warn("Attempted to end a closed generator.");
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") {
throw item.error;
} else if (item.type === "done") {
break;
}
} else {
await promise;
}
}
}
return { push, throwError, end, generator: generator() };
}
// src/match.ts
function matchTopic(topic, pattern) {
if (!(topic && pattern)) {
return null;
}
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 processedPattern = patternSegments.join("/");
if (processedPattern.includes("#") && processedPattern.indexOf("#") !== processedPattern.length - 1) {
return null;
}
const regexString = `^${processedPattern.replace(/\+/g, "([^/]+)").replace(/#/g, "(.*)")}$`;
const regex = new RegExp(regexString);
const match = regex.exec(topic);
if (!match) {
return null;
}
return { params: match.slice(1) };
}
// 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 EventEmitter {
client;
error = null;
get status() {
return this.client.connected ? "online" : "offline";
}
sharedMqttSubscriptions = /* @__PURE__ */ new Map();
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.on("message", (topic, message) => {
const subscriptions = [];
for (const [
pattern,
set
] of this.sharedMqttSubscriptions.entries()) {
const match = matchTopic(topic, pattern);
if (match) {
subscriptions.push([set, match.params]);
}
}
for (const [set, params] of subscriptions) {
for (const sub of set) {
sub.handleMessage(message, topic, params);
}
}
});
}
publish(topic, message, opts) {
this.client.publish(topic, message, { qos: opts?.qos ?? 2 });
}
async publishAsync(topic, message) {
this.client.publishAsync(topic, message);
}
publishJson(topic, message) {
this.publish(topic, JSON.stringify(message));
}
async publishJsonAsync(topic, message) {
await this.publishAsync(topic, JSON.stringify(message));
}
unsubscribe(sub) {
const set = this.sharedMqttSubscriptions.get(sub.topic);
if (set) {
sub.emit("end");
set.delete(sub);
if (set.size === 0) {
this.sharedMqttSubscriptions.delete(sub.topic);
this.client.unsubscribe(sub.topic);
}
}
}
subscribe(topic, parser) {
const sub = new Subscription({ mqtt: this, topic, parser });
const set = this.sharedMqttSubscriptions.get(topic);
if (set) {
set.add(sub);
} else {
this.sharedMqttSubscriptions.set(topic, /* @__PURE__ */ new Set([sub]));
this.client.subscribe(topic, { qos: 2, rh: 2 });
}
return sub;
}
subscribeString(topic) {
return this.subscribe(topic, stringParser);
}
subscribeJson(topic) {
return this.subscribe(topic, jsonParser);
}
// TODO: Subscribe zod
subscribeBinary(topic) {
return this.subscribe(topic, binaryParser);
}
async subscribeAsync(topic, parser) {
const sub = new Subscription({ mqtt: this, topic, parser });
const set = this.sharedMqttSubscriptions.get(topic);
if (set) {
set.add(sub);
} else {
this.sharedMqttSubscriptions.set(topic, /* @__PURE__ */ new Set([sub]));
await this.client.subscribeAsync(topic);
}
return sub;
}
async subscribeStringAsync(topic) {
return this.subscribeAsync(topic, stringParser);
}
async subscribeJsonAsync(topic) {
return this.subscribeAsync(topic, jsonParser);
}
async subscribeBinaryAsync(topic) {
return this.subscribeAsync(topic, binaryParser);
}
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() {
this.client.end();
this.emit("end");
}
};
var Subscription = class extends EventEmitter {
mqtt;
generator;
topic;
parser;
constructor(opts) {
super();
this.mqtt = opts.mqtt;
this.topic = opts.topic;
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);
}
};
export {
BetterMQTT,
Subscription,
binaryParser,
jsonParser,
stringParser
};
//# sourceMappingURL=index.js.map