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

486 lines (479 loc) 12.8 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { BetterMQTT: () => BetterMQTT, Subscription: () => Subscription, binaryParser: () => binaryParser, jsonParser: () => jsonParser, stringParser: () => stringParser }); module.exports = __toCommonJS(src_exports); var import_ee_ts2 = require("ee-ts"); var import_mqtt = __toESM(require("mqtt"), 1); // 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 var import_ee_ts = require("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 import_ee_ts.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 import_ee_ts2.EventEmitter { 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 import_mqtt.default.connectAsync(...args); return new _BetterMQTT(client); } static connect(...args) { const client = import_mqtt.default.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"); } }; //# sourceMappingURL=index.cjs.map