diff --git a/src/server/hybrid-media-router.ts b/src/server/hybrid-media-router.ts index 1da9f63..dcc7d24 100644 --- a/src/server/hybrid-media-router.ts +++ b/src/server/hybrid-media-router.ts @@ -88,8 +88,6 @@ interface RoomRuntime { pump?: Promise; deadlineTimer?: NodeJS.Timeout; sfuRefreshesInFlight: Set; - debugOrdinalByPeerId: Map; - nextDebugOrdinal: number; } interface PreparedCandidate { @@ -201,7 +199,6 @@ export class HybridMediaRouter { room.advertisedCapacityByViewer.get(input.peerId) ?? 0, ); } - this.debugPeer(input.roomId, input.peerId); if (room.controller) { this.releaseResources( room.controller.upsertParticipant({ @@ -932,7 +929,6 @@ export class HybridMediaRouter { removeViewer(roomId: string, peerId: string): void { const room = this.rooms.get(roomId); room?.advertisedCapacityByViewer.delete(peerId); - room?.debugOrdinalByPeerId.delete(peerId); this.options.deleteConnectionId(roomId, peerId); if (room?.controller?.confirmDeparture(peerId, this.now())) { this.requestPump(roomId); @@ -965,8 +961,6 @@ export class HybridMediaRouter { advertisedCapacityByViewer: new Map(), requested: false, sfuRefreshesInFlight: new Set(), - debugOrdinalByPeerId: new Map(), - nextDebugOrdinal: 1, }; this.rooms.set(roomId, room); } @@ -1952,14 +1946,9 @@ export class HybridMediaRouter { private debugPeer(roomId: string, peerId: string): string { const room = this.rooms.get(roomId); if (peerId === room?.hostPeerId) return "host"; - if (!room) return "viewer-unknown"; - let ordinal = room.debugOrdinalByPeerId.get(peerId); - if (ordinal === undefined) { - ordinal = room.nextDebugOrdinal; - room.nextDebugOrdinal += 1; - room.debugOrdinalByPeerId.set(peerId, ordinal); - } - return `viewer-${ordinal}`; + return ( + room?.controller?.diagnosticParticipantLabel(peerId) ?? "viewer-unknown" + ); } private debugTuple(roomId: string, tuple: CandidateTuple): string { diff --git a/src/server/room-route-controller.ts b/src/server/room-route-controller.ts index 679fa6c..88984c1 100644 --- a/src/server/room-route-controller.ts +++ b/src/server/room-route-controller.ts @@ -375,6 +375,10 @@ export class RoomRouteController { }); } + diagnosticParticipantLabel(peerId: string): string { + return this.debugPeer(peerId); + } + snapshot(): RouteSnapshot { return { revision: this.revision, diff --git a/tests/hybrid-media-router.test.ts b/tests/hybrid-media-router.test.ts index 9a799e5..9975b96 100644 --- a/tests/hybrid-media-router.test.ts +++ b/tests/hybrid-media-router.test.ts @@ -234,6 +234,124 @@ async function establishSfuRoom( } describe("HybridMediaRouter v9 runtime", () => { + it("keeps the controller diagnostic label until a departed relay is pruned", async () => { + const { store, sent, router } = harness(2); + try { + const room = await store.createRoom(); + const host = connectHost(store, room); + complete(router, host); + const parent = connectViewer(store, room, "diagnostic-label-parent"); + complete(router, parent); + const parentPrepare = await vi.waitFor(() => { + const prepared = preparedFor(sent, parent.sessionId); + expect(prepared).toBeDefined(); + return prepared!; + }); + router.handleRouteReady(parent, { + type: "route-ready", + revision: parentPrepare.revision, + phase: "prepare", + }); + router.setViewerRelayCapacity(parent, 1); + + const temporaryRoot = connectViewer( + store, + room, + "diagnostic-label-temporary-root", + ); + complete(router, temporaryRoot); + const temporaryPrepare = await vi.waitFor(() => { + const prepared = preparedFor(sent, temporaryRoot.sessionId); + expect(prepared?.assignment.upstream).toEqual({ + kind: "peer", + peerId: host.peerId, + }); + return prepared!; + }); + router.handleRouteReady(temporaryRoot, { + type: "route-ready", + revision: temporaryPrepare.revision, + phase: "prepare", + }); + + const child = connectViewer(store, room, "diagnostic-label-child"); + complete(router, child); + const childPrepare = await vi.waitFor(() => { + const prepared = preparedFor(sent, child.sessionId); + expect(prepared?.assignment.upstream).toEqual({ + kind: "peer", + peerId: parent.peerId, + }); + return prepared!; + }); + router.handleRouteReady(child, { + type: "route-ready", + revision: childPrepare.revision, + phase: "prepare", + }); + + store.disconnectParticipant( + room.roomId, + temporaryRoot.peerId, + temporaryRoot.sessionId, + ); + router.disconnectParticipant( + room.roomId, + temporaryRoot.peerId, + temporaryRoot.sessionId, + ); + router.removeViewer(room.roomId, temporaryRoot.peerId); + await vi.waitFor(() => + expect( + router.resolveActiveViewerMediaEdge( + room.roomId, + temporaryRoot.peerId, + ), + ).toBeUndefined(), + ); + + const internal = router as unknown as { + debugPeer(roomId: string, peerId: string): string; + }; + const label = internal.debugPeer(room.roomId, parent.peerId); + + store.disconnectParticipant( + room.roomId, + parent.peerId, + parent.sessionId, + ); + router.disconnectParticipant( + room.roomId, + parent.peerId, + parent.sessionId, + ); + router.removeViewer(room.roomId, parent.peerId); + expect(internal.debugPeer(room.roomId, parent.peerId)).toBe(label); + + const recovery = await vi.waitFor(() => { + const prepared = preparedFor(sent, child.sessionId); + expect(prepared?.revision).toBeGreaterThan(childPrepare.revision); + expect(prepared?.assignment.upstream).toEqual({ + kind: "peer", + peerId: host.peerId, + }); + return prepared!; + }); + router.handleRouteReady(child, { + type: "route-ready", + revision: recovery.revision, + phase: "prepare", + }); + await vi.waitFor(() => + expect(internal.debugPeer(room.roomId, parent.peerId)).toBe( + "viewer-unknown", + ), + ); + } finally { + await router.close(); + } + }); + it("maps an exact Host active failure to its committed direct edge", async () => { const { store, sent, router } = harness(2); try { diff --git a/tests/room-route-controller.test.ts b/tests/room-route-controller.test.ts index 9ebc56d..d0fa9aa 100644 --- a/tests/room-route-controller.test.ts +++ b/tests/room-route-controller.test.ts @@ -353,6 +353,41 @@ function qualityMetrics( } describe("RoomRouteController", () => { + it("keeps one diagnostic label until a departed relay is finally pruned", () => { + const routes = controller(2); + addViewer(routes, A, 1); + addViewer(routes, B, 0); + routes.hydrateEdge(A, peerEdge(HOST, "a_from_host")); + routes.hydrateEdge(B, peerEdge(A, "b_from_a")); + const label = routes.diagnosticParticipantLabel(A); + expect(label).toMatch(/^viewer-\d+$/); + + routes.upsertParticipant({ + peerId: A, + role: "viewer", + sessionId: "a_replacement_session", + effectiveDownstreamCapacity: 1, + }); + expect(routes.diagnosticParticipantLabel(A)).toBe(label); + expect(routes.disconnectSession(A, "a_replacement_session")).toBe(true); + expect(routes.diagnosticParticipantLabel(A)).toBe(label); + expect(routes.confirmDeparture(A, 0)).toBe(true); + expect(routes.diagnosticParticipantLabel(A)).toBe(label); + + const repair = routes.reconcile(1).operation!; + expect(repair).toMatchObject({ + childPeerId: B, + reason: "parent-departed", + }); + commitCurrent(routes, 2, "b_from_host"); + const pruned = routes.reconcile(4); + expect(pruned.removedPeerIds).toContain(A); + expect(routes.diagnosticParticipantLabel(A)).toBe("viewer-unknown"); + + addViewer(routes, C, 0); + expect(routes.diagnosticParticipantLabel(C)).not.toBe(label); + }); + it("keeps isolated limitation windows diagnostic", () => { const routes = controller(2, { qualityConvergenceEnabled: true }); addViewer(routes, A, 2);