UNPKG

@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
// 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