messages: pure builders for every event the bridge sends

src/messages builds Response, StateReport, DeferredResponse, ErrorResponse, ChangeReport and the scene, doorbell
and simple events from plain values; messageId and times are parameters, so a test compares whole objects.
StateBuilder collects properties checked by the descriptors, AlexaError and AlexaErrors carry the payload fields
of an error type. AlexaStatusMessage and AlexaErrorResponse move to src/compat and are written on the builders.
On the wire, against 1.5.2: a DeferredResponse has no context key, a ChangeReport has no correlationToken, the
namespace of an ErrorResponse follows its type, and a ChangeReport without a changed property is not published:
send() resolves "" and the bridge reports an error that names the endpoint.
143 tests pass (108 before).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 19:28:05 +00:00
parent 2558e21787
commit 46fa06728c
68 changed files with 3410 additions and 1208 deletions

54
dist/cjs/compat/AlexaErrorResponse.js vendored Normal file
View file

@ -0,0 +1,54 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AlexaErrorResponse = void 0;
const crypto_1 = require("crypto");
const build_js_1 = require("../messages/build.js");
const errors_js_1 = require("../messages/errors.js");
/** An ErrorResponse as 1.x builds it: device.getErrorMessage(token), setErrorMessage(), send(). */
class AlexaErrorResponse {
constructor(correlationToken, rootTopic, endpointId, mqttClient) {
this.correlationToken = correlationToken;
this.rootTopic = rootTopic;
this.endpointId = endpointId;
this.mqttClient = mqttClient;
// Sent when setErrorMessage() was not called: 1.x published an empty payload, which is no ErrorResponse to Alexa
this.error = errors_js_1.AlexaErrors.of("INTERNAL_ERROR", "the device answered with an error and did not say which");
// One id for the message, however often toJSON() is called
this.messageId = (0, crypto_1.randomUUID)();
}
/**
* The error to answer with. otherParams are the fields the type adds to the payload (validRange). The header
* carries the namespace the type is documented under, Alexa.ThermostatController for THERMOSTAT_IS_OFF;
* options.namespace names another.
*/
setErrorMessage(type, message, otherParams = {}, options = {}) {
this.error = errors_js_1.AlexaErrors.of(type, message, otherParams);
this.namespace = options.namespace;
}
toJSON() {
const { type, alexaMessage, extra } = this.error;
return (0, build_js_1.errorResponse)({
endpointId: this.endpointId,
messageId: this.messageId,
correlationToken: this.correlationToken,
type,
message: alexaMessage,
extra,
namespace: this.namespace ?? this.error.namespace,
});
}
/** Resolves with the topic published to, or "" when the publish failed. Never rejects. */
send(sendAsync = false) {
const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; // "alexaResponce" is how Alex2MQTT and Alex2ESP spell the topic
return new Promise((resolve) => {
this.mqttClient.publish(topic, JSON.stringify(this.toJSON()), (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
});
});
}
}
exports.AlexaErrorResponse = AlexaErrorResponse;