bridge: a discovery request that arrives twice is answered once

A broker that mirrors another delivers every message twice, and the bridge
answered both copies of a discovery request. A request is now remembered
by its messageId for 60 s, as a directive is, and its copy is dropped with
a log line. The messageId is read from the header of the Discover
directive or from the request itself when that is the header alone.
A request without a messageId is answered every time, as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 21:16:50 +00:00
parent d12767f9a7
commit 29ee1a315e
9 changed files with 147 additions and 21 deletions

19
dist/esm/Alex2Node.js vendored
View file

@ -14,6 +14,20 @@ const DESCRIBED = [
"description", "manufacturerName", "manufacturer", "model", "serialNumber", "firmwareVersion", "softwareVersion",
"customIdentifier", "cookie",
];
// The messageId of a discovery request: in the header of the Discover directive, or beside the namespace and the
// name when the request is the header alone. undefined for a request without one, which is answered every time.
function requestId(text) {
let request;
try {
request = JSON.parse(text);
}
catch {
return undefined;
}
const header = request?.directive?.header ?? request?.header ?? request;
const { messageId } = (typeof header === "object" && header !== null ? header : {});
return typeof messageId === "string" ? messageId : undefined;
}
/**
* The Alexa-to-MQTT bridge: one broker connection for a user's root topic, a set of registered devices, discovery and
* directive dispatch.
@ -118,6 +132,11 @@ class Alex2MQTT extends EventEmitter {
this.log(`MQTT Message Received`, { topic, payload: text });
try {
if (topic === topics.discover(this.rootTopic)) {
const messageId = requestId(text);
if (messageId !== undefined && this.dispatcher.arrivedBefore("", messageId)) {
this.log(`the discovery request ${messageId} is dropped: it arrived before`);
return;
}
await this.answerDiscovery();
return;
}

View file

@ -93,6 +93,11 @@ export declare class Dispatcher {
send(topic: string, message: object): Promise<PublishResult>;
/** defer() for an interface Amazon documents no DeferredResponse for: said once, the DeferredResponse is sent. */
noteDeferral(namespace: string): void;
/**
* true for a message that came before, within REMEMBERED_MS: a broker that mirrors another delivers each message
* twice. endpointId is "" for a message to the root, a discovery request.
*/
arrivedBefore(endpointId: string, messageId: string, now?: number): boolean;
/** The text of a message on the directive topic of endpointId. Resolves when the handlers are done. */
dispatch(endpointId: string, text: string): Promise<void>;
private route;

View file

@ -127,6 +127,18 @@ export class Dispatcher {
this.deferralNoted.add(namespace);
this.options.log(`warning: Amazon documents no DeferredResponse for ${namespace}, Alexa may not wait for the answer`);
}
/**
* true for a message that came before, within REMEMBERED_MS: a broker that mirrors another delivers each message
* twice. endpointId is "" for a message to the root, a discovery request.
*/
arrivedBefore(endpointId, messageId, now = Date.now()) {
this.forget(now);
const id = key(endpointId, messageId);
if (this.arrived.has(id))
return true;
this.arrived.set(id, now);
return false;
}
/** The text of a message on the directive topic of endpointId. Resolves when the handlers are done. */
async dispatch(endpointId, text) {
const { log, emit } = this.options;
@ -146,13 +158,9 @@ export class Dispatcher {
const { namespace, name, messageId } = raw.header;
const correlationToken = typeof raw.header.correlationToken === "string" ? raw.header.correlationToken : "";
const receivedAt = Date.now();
this.forget(receivedAt);
if (typeof messageId === "string") {
if (this.arrived.has(key(endpointId, messageId))) {
log(`the directive ${messageId} for ${endpointId} is dropped: it arrived before`);
return;
}
this.arrived.set(key(endpointId, messageId), receivedAt);
if (typeof messageId === "string" && this.arrivedBefore(endpointId, messageId, receivedAt)) {
log(`the directive ${messageId} for ${endpointId} is dropped: it arrived before`);
return;
}
const device = this.options.getDevice(endpointId);
if (!device) {