Deliver broadcasts to all hosts, not just the local one

An event broadcast from an instance to all hosts only reached the host
running that instance. The host side of an instance connection has the
host's own address as its source, so a broadcast aimed at hosts counts as
addressed to it, and a link that a message is addressed to handles it and
stops. The event was delivered locally and never routed onwards.

A broadcast is addressed to every link of the type it targets, so arriving
at one of them does not mean it has arrived everywhere. Keep routing such a
message after handling it locally. Only links that route are affected, as
instances and controls have no router and would otherwise throw on every
broadcast they receive.

The only links whose source type matches a broadcast target and which route
are the host and its instance connections, both matching a broadcast to
hosts, so add the missing case for that to the host router. It forwards to
the controller, which already passes broadcasts on to every other host, and
stops when the message came from the controller to begin with.

Drop the exclusion this bug had in the routing test matrix, which now
covers it along with the plugin filtering variants.

Fixes #575

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
bbassieandClaude Opus 4.8 committed 2026-07-25 17:39:25 +00:00
1 parent 5e0ab3423f
commit 09c667252c
4 files changed
+52 -2

No files matched your search

+6
View File
@@ -182,6 +182,12 @@ export class HostRouter {
if (this.host !== origin && (!plugin || this.host.serverPlugins.has(plugin))) {
this.host.connector.forward(message);
}
} else if (dst.id === lib.Address.host) {
// Sent up to the controller, which passes it on to the other
// hosts. Nothing left to do when it came from there.
if (this.host !== origin && (!plugin || this.host.serverPlugins.has(plugin))) {
this.host.connector.forward(message);
}
} else if (dst.id === lib.Address.control) {
if (this.host !== origin) {
if (!plugin || this.host.serverPlugins.has(plugin)) {
+10
View File
@@ -225,6 +225,16 @@ export class Link {
return;
}
// A broadcast is addressed to every link of the type it targets, so
// being addressed to this one does not mean it stops here.
if (
message.type === "event"
&& message.dst.type === libData.Address.broadcast
&& this.router
) {
this._routeMessage(message, entry);
}
if (message.type === "request") {
this._processRequest(
message as libData.MessageRequest,
-2
View File
@@ -235,8 +235,6 @@ describe("Integration of link routing", function() {
};
for (const dstName of dsts[dst]) {
if (srcName === dstName) { continue; }
// TODO remove when broadcasting from instance to hosts is fixed, see #575
if (srcName === "instanceA1" && dstName === "hostB") { continue; }
let called;
handle(get(dstName), Event, async () => { called = true; });
const should = canReach(dstName);
+36
View File
@@ -254,6 +254,42 @@ describe("lib/link/link", function() {
await assert.rejects(pending, { message: "Session Lost" });
});
describe("Broadcast handling", function() {
// A broadcast is addressed to every link of the type it targets,
// so a link it reaches has to pass it on as well as handle it,
// see #575.
function mockRouter(record) {
return { forwardMessage: () => { record.routed = true; return true; } };
}
it("should route a broadcast addressed to this link as well as handle it", function() {
const record = {};
let handled = false;
testLink.router = mockRouter(record);
testLink.handle(SimpleEvent, async () => { handled = true; });
testConnector.emit("message", new lib.MessageEvent(1, dst, addr("allControls"), "SimpleEvent"));
assert(record.routed, "broadcast was not routed onwards");
assert(handled, "broadcast was not handled locally");
});
it("should handle a broadcast on a link which does not route", function() {
// Instances have no router, so routing unconditionally would
// throw here and the event would never be handled.
let handled = false;
testLink.handle(SimpleEvent, async () => { handled = true; });
testConnector.emit("message", new lib.MessageEvent(1, dst, addr("allControls"), "SimpleEvent"));
assert(handled, "broadcast was not handled locally");
});
it("should not route a non broadcast addressed to this link", function() {
const record = {};
testLink.router = mockRouter(record);
testLink.handle(SimpleEvent, async () => {});
testConnector.emit("message", new lib.MessageEvent(1, dst, src, "SimpleEvent"));
assert(!record.routed, "message addressed to this link was routed onwards");
});
});
describe(".send()", function() {
it("should send request to the other side of the link", async function() {
let message = events.once(testConnector, "send");