bridge: publish through a Publisher; devices no longer hold the broker client

src/transport.ts has the Publisher interface, MqttPublisher (the client the bridge has at the time) and
MemoryPublisher (tests without a broker); src/topics.ts names the four topics the library publishes to.
Device, AlexaStatusMessage, AlexaErrorResponse and sendSceneResponse shared three copies of the
publish-and-report code: they now call one send() that resolves the topic or "" and never rejects.
registerDevice() and addDevice() work before connect(); a send() without a connection resolves "" and the
"error" event says to call connect(). unregisterDevice() and clearDevices() take the publisher from the device.
Device.setMqttClient() is gone, the first constructor argument of Device is ignored, and the message classes
take a Publisher where they took the client. Tests: 108 -> 113.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 19:34:39 +00:00
parent 46fa06728c
commit 6789a1a077
39 changed files with 839 additions and 246 deletions

View file

@ -1,4 +1,37 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = require("events");
const AlexaErrorResponse_js_1 = require("../compat/AlexaErrorResponse.js");
@ -10,12 +43,14 @@ const Alexa_js_1 = require("../registry/interfaces/Alexa.js");
const EndpointHealth_js_1 = require("../registry/interfaces/EndpointHealth.js");
const schema_js_1 = require("../registry/schema.js");
const types_js_1 = require("../registry/types.js");
const topics = __importStar(require("../topics.js"));
const transport_js_1 = require("../transport.js");
const Capability_js_1 = require("./Capability.js");
const validate_js_1 = require("./validate.js");
class Device extends events_1.EventEmitter {
constructor(mqttClient, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") {
/** client is ignored: in 1.x it was the broker client, and a device could only be built after connect(). */
constructor(client, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") {
super();
this.mqttClient = mqttClient;
this.rootTopic = rootTopic;
this.name = name;
this.endpointId = endpointId;
@ -39,10 +74,12 @@ class Device extends events_1.EventEmitter {
*/
this.endpointHealth = false;
this.capabilities = [];
}
/** Internal (1.5.2): Alex2MQTT.connect() re-binds every registered device to its new broker client after a disconnect(). */
setMqttClient(client) {
this.mqttClient = client;
/**
* What the device and the messages it builds publish through. The bridge sets it when the device is registered
* and clears it when the device is unregistered; a MemoryPublisher here tests a device without a broker. While it
* is null every send() resolves "" and reports why.
*/
this.publisher = null;
}
getName() {
return this.name;
@ -66,12 +103,12 @@ class Device extends events_1.EventEmitter {
return this.description;
}
getErrorMessage(correlationToken) {
const msg = new AlexaErrorResponse_js_1.AlexaErrorResponse(correlationToken, this.rootTopic, this.endpointId, this.mqttClient);
const msg = new AlexaErrorResponse_js_1.AlexaErrorResponse(correlationToken, this.rootTopic, this.endpointId, this.publisher);
msg.onPublishError = this.onPublishError;
return msg;
}
getStatusMessage(correlationToken, isResponse = false, isDeferred = false) {
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse, isDeferred);
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.publisher, isResponse, isDeferred);
msg.onPublishError = this.onPublishError;
return msg;
}
@ -82,7 +119,7 @@ class Device extends events_1.EventEmitter {
* Without a changed property send() publishes nothing and resolves with "": Alex2MQTT would drop the report.
*/
getChangeReport(cause = "PHYSICAL_INTERACTION") {
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage("", this.rootTopic, this.endpointId, this.mqttClient, false, false, cause);
const msg = new AlexaStatusMessage_js_1.AlexaStatusMessage("", this.rootTopic, this.endpointId, this.publisher, false, false, cause);
msg.onPublishError = this.onPublishError;
return msg;
}
@ -91,17 +128,9 @@ class Device extends events_1.EventEmitter {
* (1.5.1). Resolves with the topic published to, or "" when the publish failed (never rejects, 1.5.2).
*/
sendSceneResponse(correlationToken, activated, cause = "VOICE_INTERACTION", sendAsync = false) {
const payload = (0, build_js_1.sceneEvent)({ endpointId: this.endpointId, correlationToken, activated, cause });
const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`;
return new Promise((resolve) => {
this.mqttClient.publish(topic, JSON.stringify(payload), (err) => {
if (!err)
return resolve(topic);
if (this.onPublishError)
this.onPublishError(err); // -> the bridge's "error" event (when somebody listens)
resolve(""); // 1.5.1 rejected here, and an un-caught send() then killed the host on any broker hiccup
});
});
const answer = sendAsync ? topics.deferred : topics.response;
const build = () => (0, build_js_1.sceneEvent)({ endpointId: this.endpointId, correlationToken, activated, cause });
return (0, transport_js_1.send)(this.publisher, answer(this.rootTopic, this.endpointId), build, this.onPublishError);
}
/** The capabilities the device declared, in the order it declared them. */
getCapabilities() {