import type { MqttClient } from "mqtt"; /** What became of a publish. A failure is a value: publish() never rejects. */ export type PublishResult = | { ok: true; topic: string } | { ok: false; topic: string; error: Error }; /** Where the bridge, its devices and their messages publish. The message is sent as JSON. */ export interface Publisher { publish(topic: string, message: object): Promise; } const asError = (err: unknown): Error => (err instanceof Error ? err : new Error(String(err))); /** * Publishes to the broker through the client the bridge has at the time: a new one after disconnect() and connect(), * none before connect() and after disconnect(). Without a client the publish is refused. With a client that lost * its connection mqtt.js keeps the message and sends it after the reconnect, and the publish resolves then. */ export class MqttPublisher implements Publisher { constructor(private readonly client: () => MqttClient | null) {} publish(topic: string, message: object): Promise { const client = this.client(); return new Promise((resolve) => { const failed = (err: unknown): void => resolve({ ok: false, topic, error: asError(err) }); if (!client) { failed(new Error(`nothing was published to ${topic}: the bridge is not connected, call connect() first`)); return; } try { client.publish(topic, JSON.stringify(message), (err) => (err ? failed(err) : resolve({ ok: true, topic }))); } catch (err) { failed(err); } }); } } /** * Keeps what it is asked to publish, for tests and dry runs without a broker: * * const sent = new MemoryPublisher(); * device.publisher = sent; * await device.getStatusMessage(token, true).addPowerControllerProp(PowerController.ON).send(); * sent.published[0] // { topic: "//alexaResponce", message: { event, context } } */ export class MemoryPublisher implements Publisher { /** Oldest first. The message is what a subscriber gets after JSON.parse. */ public readonly published: Array<{ topic: string; message: any }> = []; /** Set it and every publish fails with this error, as a publish to a broker that is gone does. */ public failWith: Error | null = null; publish(topic: string, message: object): Promise { if (this.failWith) return Promise.resolve({ ok: false, topic, error: this.failWith }); try { this.published.push({ topic, message: JSON.parse(JSON.stringify(message)) }); } catch (err) { return Promise.resolve({ ok: false, topic, error: asError(err) }); } return Promise.resolve({ ok: true, topic }); } } /** * send() as 1.x promises it: resolves with the topic, or with "" when nothing was published, and never rejects * (1.5.1 rejected, and a send() nobody caught killed the host on any broker hiccup). What went wrong goes to report: * the message cannot be built, the device is on no bridge, or the publish failed. */ export async function send( publisher: Publisher | null, topic: string, build: () => object, report?: (err: Error) => void ): Promise { let result: PublishResult; try { const message = build(); if (!publisher) { throw new Error(`nothing was published to ${topic}: the device is on no bridge, register it with addDevice() or registerDevice()`); } result = await publisher.publish(topic, message); } catch (err) { result = { ok: false, topic, error: asError(err) }; } if (result.ok) return topic; if (report) report(result.error); return ""; }