dispatch: typed handlers, respond/defer/error, automatic ErrorResponse, watchdog

src/dispatcher.ts routes a directive to capability.on(name | "*"), device.onDirective() or
device.onReportState(), with the payload checked by the descriptor of the interface. The
DirectiveContext answers with respond/report/defer/error; respond() and report() start from
device.state(). A handler or 1.x listener that throws or rejects is answered with INTERNAL_ERROR
(an AlexaError with itself) and reported through "error", never as an unhandled rejection.
Undeclared interface, unknown directive, AdjustMode on an unordered mode and a missing handler
get INVALID_DIRECTIVE, a bad payload INVALID_VALUE; a device with an "Event" or "ReportState"
listener keeps the answer to itself. Every answer passes the dispatcher: the first one per
correlationToken is published, a second is refused. No answer within answerWithinMs (6500,
0 = off, unref'd timer) sends INTERNAL_ERROR and emits "unanswered". A messageId that arrived
in the last 60 s is dropped. New: "unknownEndpoint", options answerUnknownEndpoints, publisher,
timers, and bridge.receive() to run a bridge without a broker. 168 tests pass (18 new).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 19:49:41 +00:00
parent e0fe9d2812
commit feff828ec1
40 changed files with 2149 additions and 136 deletions

View file

@ -3,6 +3,8 @@ import Device from "./device/Device.js";
import type { EndpointDefinition } from "./device/Device.js";
import { EventEmitter } from "events";
import { DisplayCategory } from "./compat/enums.js";
import type { Timers } from "./dispatcher.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. */
export interface Alex2MQTTOptions {
/** The broker URL. Default: the public Alex2MQTT broker, mqtt://Alex2MQTT.stormysdream.club:1883. */
@ -16,6 +18,20 @@ export interface Alex2MQTTOptions {
* { type: "AlexaInterface", interface: "Alexa", version: "3" }, which Amazon requires and 1.x left out.
*/
alexaInterface?: boolean;
/**
* How long a directive may wait for its answer, in milliseconds. After that the bridge answers INTERNAL_ERROR and
* emits "unanswered". Default: 6500, before Alex2MQTT gives up at 7000. 0: the bridge never answers for a handler.
*/
answerWithinMs?: number;
/**
* true: a directive for an endpoint the bridge does not have is answered with NO_SUCH_ENDPOINT. Default: it is
* left alone, because the endpoint may belong to another bridge on the same root topic.
*/
answerUnknownEndpoints?: boolean;
/** Publish here and not to the broker: a MemoryPublisher and receive() run a bridge that never connects. */
publisher?: Publisher;
/** The timer of answerWithinMs, for a test that does not wait. Default: setTimeout, unref'd. */
timers?: Timers;
}
export declare const DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883";
/**
@ -31,6 +47,9 @@ export declare const DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883";
* unconditionally, and Node kills a process that has an unhandled "error" event)
* "discover" (n) a discovery request was answered with n devices
* "directive" (info) a directive was dispatched to a device: { endpointId, namespace, name }
* "unknownEndpoint" (info) a directive came for an endpoint that is not registered: { endpointId, namespace, name }
* "unanswered" (info) nothing answered a directive in time and the bridge answered INTERNAL_ERROR:
* { endpointId, namespace, name, correlationToken }
*/
declare class Alex2MQTT extends EventEmitter {
private username;
@ -39,6 +58,7 @@ declare class Alex2MQTT extends EventEmitter {
private debugLogging;
private client;
private readonly publisher;
private readonly dispatcher;
private devices;
private MqttHost;
private options;
@ -52,6 +72,15 @@ declare class Alex2MQTT extends EventEmitter {
/** Emit "error" only when somebody listens: an unhandled "error" event would crash the host process. */
private fail;
connect(): void;
/**
* What the bridge does with a message of the broker: it answers a discovery request, and it hands a directive to
* the handlers of its device. Resolves when they are done and never rejects. With the publisher option a test
* calls it in place of the broker:
*
* await bridge.receive("root/lamp-1/alexaDirective", JSON.stringify(directive));
*/
receive(topic: string, payload: Buffer | string): Promise<void>;
private answerDiscovery;
private describeDevices;
/** Close the broker connection (resolves once closed). The devices stay registered; connect() again reuses them. */
disconnect(): Promise<void>;