Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/bridge-mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export function createBridgeMeshSync(
store.roomVerbHandlers,
undefined,
() => store.selfStatus,
undefined,
() => store.hostedRooms,
),
);
const tool = new CommsTool(store, store.discovery);
Expand Down
17 changes: 17 additions & 0 deletions src/core/mesh-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ConnectionApproval } from "./connection-approval.js";
import { StaleAgentChecker } from "./stale-agent-checker.js";
import { PeerLifecycle } from "./peer-lifecycle.js";
import type { RoomVerbHandler } from "./room-router.js";
import type { HostedRoomAdvert } from "./wire-mesh-transport.js";
import type {
MeshStatePatch,
PeerInfo,
Expand Down Expand Up @@ -107,6 +108,22 @@ export class MeshStore implements CommsStore {
return this.agents.get(this.peerId)?.status;
}

/** This store's own currently-hosted public/private rooms, synchronously, in the lightweight gossip-safe shape WireMeshTransport's own hosted-rooms re-advertisement timer reads on every tick -- the write half of P3.8's room-discovery replacement for createRoom's own broadcastPatch (agent-comms#48). Secret rooms are never included (never worth advertising at all), and a room this store has merely replicated via the legacy room_upsert broadcast, rather than owns, is excluded too -- "hosted" means "this device is the one a joiner should actually reach", which only its own owner is. */
get hostedRooms(): readonly HostedRoomAdvert[] {
const result: HostedRoomAdvert[] = [];
for (const room of this.rooms.values()) {
if (room.owner !== this.peerId) continue;
if (room.type !== "public" && room.type !== "private") continue;
result.push({
path: room.id,
name: room.name,
type: room.type,
description: room.description,
});
}
return result;
}

/** Whether the mesh has a live coordinator connection. */
get connected(): boolean {
return (
Expand Down
85 changes: 85 additions & 0 deletions src/test/mesh-store-orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MeshStore } from "../core/mesh-store.js";
import type { ConnectionHandle, MeshTransport } from "../core/transport.js";
import type { ManageOutcome } from "wire-mesh-core/domain/mesh-session";
import type { PeerInfo } from "../core/wire-protocol.js";
import type { Room } from "../core/types.js";

/** A device-id is a 64-character lowercase hex SHA-256 digest; room-path.ts's assertDeviceIdHex rejects anything shorter. */
const DEVICE_ID_HEX_LENGTH = 64;
Expand Down Expand Up @@ -95,6 +96,90 @@ describe("MeshStore — connected getter", () => {
});
});

describe("MeshStore — hostedRooms getter", () => {
function room(overrides: Partial<Room> = {}): Room {
return {
id: "owner-device/general",
version: 1,
name: "general",
type: "public",
owner: "owner-device",
createdAt: "2026-01-01T00:00:00.000Z",
description: "chat",
members: ["owner-device"],
invited: [],
memberJoins: { "owner-device": 1 },
memberLeaves: {},
invitedJoins: {},
invitedLeaves: {},
...overrides,
};
}

it("includes only this store's own public/private rooms, mapped to the gossip-safe shape", () => {
const store = new MeshStore();
store.peerId = "owner-device";
const rooms = collaborator(store, "rooms") as Map<string, Room>;
rooms.set(
"owner-device/general",
room({ id: "owner-device/general", name: "general", type: "public" }),
);
rooms.set(
"owner-device/team",
room({
id: "owner-device/team",
name: "team",
type: "private",
description: "private chat",
}),
);

expect(store.hostedRooms).toEqual([
{
path: "owner-device/general",
name: "general",
type: "public",
description: "chat",
},
{
path: "owner-device/team",
name: "team",
type: "private",
description: "private chat",
},
]);
});

it("excludes secret rooms, even when owned by this store", () => {
const store = new MeshStore();
store.peerId = "owner-device";
const rooms = collaborator(store, "rooms") as Map<string, Room>;
rooms.set(
"owner-device/_hidden",
room({ id: "owner-device/_hidden", name: "_hidden", type: "secret" }),
);

expect(store.hostedRooms).toEqual([]);
});

it("excludes rooms owned by a different device, even public/private ones this store has replicated", () => {
const store = new MeshStore();
store.peerId = "owner-device";
const rooms = collaborator(store, "rooms") as Map<string, Room>;
rooms.set(
"other-device/general",
room({
id: "other-device/general",
owner: "other-device",
name: "general",
type: "public",
}),
);

expect(store.hostedRooms).toEqual([]);
});
});

describe("MeshStore — init()", () => {
it("starts the data server, records its own peer info, connects to an existing coordinator, and unrefs -- without becoming coordinator", async () => {
const store = new MeshStore();
Expand Down