From d12767f9a7141072a0a41b794fd4eeb09083a701 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 28 Sep 2026 21:15:47 +0000 Subject: [PATCH] bridge: the discovery answer is published through send() The answer on /discover_r called the publisher directly; a publisher that threw or rejected was only caught by the handler meant for listeners of "discover". It now takes the path of every device message: send() resolves, and a failure goes to the "error" event when somebody listens. A test covers a publish that fails, throws and rejects, with and without an "error" listener: receive() resolves each time and "discover" is not emitted. Co-Authored-By: Claude Fable 5.1 --- dist/cjs/Alex2Node.js | 9 ++++----- dist/esm/Alex2Node.js | 11 +++++------ src/Alex2Node.ts | 9 +++++---- test/transport.test.js | 26 ++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/dist/cjs/Alex2Node.js b/dist/cjs/Alex2Node.js index bf0f8cd..be918c3 100644 --- a/dist/cjs/Alex2Node.js +++ b/dist/cjs/Alex2Node.js @@ -174,12 +174,11 @@ class Alex2MQTT extends events_1.EventEmitter { this.log("Discovery request received, getting device json..."); const deviceArray = this.describeDevices(); this.lastDiscoveryAt = new Date().toISOString(); - const result = await this.publisher.publish(topics.discoverReply(this.rootTopic), deviceArray); - if (!result.ok) { - this.fail(result.error); + // send(), as for every message of a device: a publisher that throws or rejects is reported like one that fails + const topic = await (0, transport_js_1.send)(this.publisher, topics.discoverReply(this.rootTopic), () => deviceArray, (err) => this.fail(err)); + if (topic === "") return; - } - this.log(`Discovery payloads published to ${result.topic}`, deviceArray); + this.log(`Discovery payloads published to ${topic}`, deviceArray); this.emit("discover", deviceArray.length); } // The endpoint objects of a discovery answer. What check() says about a device is logged, each line once. A device diff --git a/dist/esm/Alex2Node.js b/dist/esm/Alex2Node.js index 1f5ee6e..3d8f175 100644 --- a/dist/esm/Alex2Node.js +++ b/dist/esm/Alex2Node.js @@ -7,7 +7,7 @@ import { DisplayCategory } from "./compat/enums.js"; import { DeclarationError } from "./registry/types.js"; import { Dispatcher } from "./dispatcher.js"; import * as topics from "./topics.js"; -import { asError, MqttPublisher } from "./transport.js"; +import { asError, MqttPublisher, send } from "./transport.js"; export const DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883"; // What addDevice() copies from the definition to the device const DESCRIBED = [ @@ -135,12 +135,11 @@ class Alex2MQTT extends EventEmitter { this.log("Discovery request received, getting device json..."); const deviceArray = this.describeDevices(); this.lastDiscoveryAt = new Date().toISOString(); - const result = await this.publisher.publish(topics.discoverReply(this.rootTopic), deviceArray); - if (!result.ok) { - this.fail(result.error); + // send(), as for every message of a device: a publisher that throws or rejects is reported like one that fails + const topic = await send(this.publisher, topics.discoverReply(this.rootTopic), () => deviceArray, (err) => this.fail(err)); + if (topic === "") return; - } - this.log(`Discovery payloads published to ${result.topic}`, deviceArray); + this.log(`Discovery payloads published to ${topic}`, deviceArray); this.emit("discover", deviceArray.length); } // The endpoint objects of a discovery answer. What check() says about a device is logged, each line once. A device diff --git a/src/Alex2Node.ts b/src/Alex2Node.ts index 129cbab..c7ced76 100644 --- a/src/Alex2Node.ts +++ b/src/Alex2Node.ts @@ -10,7 +10,7 @@ import { DeclarationError } from "./registry/types.js"; import { Dispatcher } from "./dispatcher.js"; import type { Timers } from "./dispatcher.js"; import * as topics from "./topics.js"; -import { asError, MqttPublisher } from "./transport.js"; +import { asError, MqttPublisher, send } from "./transport.js"; import type { Publisher } from "./transport.js"; /** Optional settings for the bridge (1.5.1). Everything has the 1.4.0 behaviour as its default. */ @@ -176,9 +176,10 @@ class Alex2MQTT extends EventEmitter { this.log("Discovery request received, getting device json..."); const deviceArray = this.describeDevices(); this.lastDiscoveryAt = new Date().toISOString(); - const result = await this.publisher.publish(topics.discoverReply(this.rootTopic), deviceArray); - if (!result.ok) { this.fail(result.error); return; } - this.log(`Discovery payloads published to ${result.topic}`, deviceArray); + // send(), as for every message of a device: a publisher that throws or rejects is reported like one that fails + const topic = await send(this.publisher, topics.discoverReply(this.rootTopic), () => deviceArray, (err) => this.fail(err)); + if (topic === "") return; + this.log(`Discovery payloads published to ${topic}`, deviceArray); this.emit("discover", deviceArray.length); } diff --git a/test/transport.test.js b/test/transport.test.js index 852b202..1e1bb92 100644 --- a/test/transport.test.js +++ b/test/transport.test.js @@ -63,3 +63,29 @@ test("unregisterDevice and clearDevices take the publisher from the device", () bridge.clearDevices(); assert.equal(plug.publisher, null); }); + +test("a discovery answer that cannot be published: receive() resolves and the \"error\" event says why", async () => { + const gone = new MemoryPublisher(); + gone.failWith = new Error("broker gone"); + const publishers = { + "fails": gone, + "throws": { publish() { throw new Error("publisher threw"); } }, + "rejects": { publish: () => Promise.reject(new Error("publisher rejected")) }, + }; + const expected = { "fails": "broker gone", "throws": "publisher threw", "rejects": "publisher rejected" }; + for (const [how, publisher] of Object.entries(publishers)) { + const bridge = new Alex2MQTT("user", "password", "root", false, { publisher }); + bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] }).add(PowerController); + const errors = []; + const discovered = []; + bridge.on("error", (err) => errors.push(err.message)); + bridge.on("discover", (n) => discovered.push(n)); + await bridge.receive("root/discover", ""); + assert.deepEqual(errors, [expected[how]], how); + assert.deepEqual(discovered, [], how); + + // Without a listener the error has nowhere to go, and still nothing is thrown or rejected + bridge.removeAllListeners("error"); + await bridge.receive("root/discover", ""); + } +});