fix: make presentation sync disconnect lifecycle-safe
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import type { ServerSyncMessage } from "@lda/presentation-sync";
|
import type { ServerSyncMessage } from "@lda/presentation-sync";
|
||||||
import type { PresentationPeer } from "./rooms.js";
|
import type { PresentationPeer, PresentationRoomService } from "./rooms.js";
|
||||||
import {
|
import {
|
||||||
createPresentationRoomService,
|
createPresentationRoomService,
|
||||||
EMPTY_ROOM_GRACE_MS,
|
EMPTY_ROOM_GRACE_MS,
|
||||||
@@ -35,8 +35,7 @@ const makeService = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectedRoom = () => {
|
const connectRoom = (service: PresentationRoomService) => {
|
||||||
const { service, advance } = makeService();
|
|
||||||
const created = service.create({
|
const created = service.create({
|
||||||
role: "presenter",
|
role: "presenter",
|
||||||
initialHash: "#scene/thesis/title",
|
initialHash: "#scene/thesis/title",
|
||||||
@@ -52,7 +51,6 @@ const connectedRoom = () => {
|
|||||||
audience.close.mockClear();
|
audience.close.mockClear();
|
||||||
return {
|
return {
|
||||||
service,
|
service,
|
||||||
advance,
|
|
||||||
presenter,
|
presenter,
|
||||||
audience,
|
audience,
|
||||||
presenterToken: created.connectionToken,
|
presenterToken: created.connectionToken,
|
||||||
@@ -60,6 +58,11 @@ const connectedRoom = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connectedRoom = () => {
|
||||||
|
const { service, advance } = makeService();
|
||||||
|
return { ...connectRoom(service), advance };
|
||||||
|
};
|
||||||
|
|
||||||
describe("createPresentationRoomService", () => {
|
describe("createPresentationRoomService", () => {
|
||||||
it("creates a room at revision zero and joins the opposite role", () => {
|
it("creates a room at revision zero and joins the opposite role", () => {
|
||||||
const { service } = makeService();
|
const { service } = makeService();
|
||||||
@@ -131,6 +134,30 @@ describe("createPresentationRoomService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not disconnect a replacement when the old peer closes", () => {
|
||||||
|
const { service } = makeService();
|
||||||
|
const created = service.create({ role: "presenter", initialHash: "#scene/one" });
|
||||||
|
const oldPeer = peer();
|
||||||
|
const newPeer = peer();
|
||||||
|
service.connect(created.connectionToken, oldPeer);
|
||||||
|
service.connect(created.connectionToken, newPeer);
|
||||||
|
newPeer.send.mockClear();
|
||||||
|
|
||||||
|
service.disconnect(created.connectionToken, oldPeer);
|
||||||
|
|
||||||
|
expect(service.publish(created.connectionToken, {
|
||||||
|
type: "location.publish",
|
||||||
|
hash: "#scene/replacement-survives",
|
||||||
|
baseRevision: 0,
|
||||||
|
messageId: "replacement-1",
|
||||||
|
}).kind).toBe("accepted");
|
||||||
|
expect(newPeer.send).toHaveBeenCalledWith({
|
||||||
|
type: "location.snapshot",
|
||||||
|
snapshot: { hash: "#scene/replacement-survives", revision: 1 },
|
||||||
|
originatingMessageId: "replacement-1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts one publish and rejects a stale competing publish", () => {
|
it("accepts one publish and rejects a stale competing publish", () => {
|
||||||
const { service, presenter, audience, presenterToken, audienceToken } =
|
const { service, presenter, audience, presenterToken, audienceToken } =
|
||||||
connectedRoom();
|
connectedRoom();
|
||||||
@@ -171,7 +198,7 @@ describe("createPresentationRoomService", () => {
|
|||||||
const { service, presenter, audience, presenterToken, audienceToken } =
|
const { service, presenter, audience, presenterToken, audienceToken } =
|
||||||
connectedRoom();
|
connectedRoom();
|
||||||
|
|
||||||
service.disconnect(audienceToken);
|
service.disconnect(audienceToken, audience);
|
||||||
|
|
||||||
expect(presenter.send).toHaveBeenCalledWith({
|
expect(presenter.send).toHaveBeenCalledWith({
|
||||||
type: "presence.snapshot",
|
type: "presence.snapshot",
|
||||||
@@ -193,10 +220,11 @@ describe("createPresentationRoomService", () => {
|
|||||||
presenter,
|
presenter,
|
||||||
presenterToken,
|
presenterToken,
|
||||||
audienceToken,
|
audienceToken,
|
||||||
|
audience,
|
||||||
} = connectedRoom();
|
} = connectedRoom();
|
||||||
|
|
||||||
service.disconnect(presenterToken);
|
service.disconnect(presenterToken, presenter);
|
||||||
service.disconnect(audienceToken);
|
service.disconnect(audienceToken, audience);
|
||||||
advance(EMPTY_ROOM_GRACE_MS - 1);
|
advance(EMPTY_ROOM_GRACE_MS - 1);
|
||||||
expect(service.sweepExpired()).toBe(0);
|
expect(service.sweepExpired()).toBe(0);
|
||||||
|
|
||||||
@@ -206,10 +234,17 @@ describe("createPresentationRoomService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("expires an empty room after the ten-minute grace", () => {
|
it("expires an empty room after the ten-minute grace", () => {
|
||||||
const { service, advance, presenterToken, audienceToken } = connectedRoom();
|
const {
|
||||||
|
service,
|
||||||
|
advance,
|
||||||
|
presenterToken,
|
||||||
|
presenter,
|
||||||
|
audienceToken,
|
||||||
|
audience,
|
||||||
|
} = connectedRoom();
|
||||||
|
|
||||||
service.disconnect(presenterToken);
|
service.disconnect(presenterToken, presenter);
|
||||||
service.disconnect(audienceToken);
|
service.disconnect(audienceToken, audience);
|
||||||
advance(EMPTY_ROOM_GRACE_MS);
|
advance(EMPTY_ROOM_GRACE_MS);
|
||||||
|
|
||||||
expect(service.sweepExpired()).toBe(1);
|
expect(service.sweepExpired()).toBe(1);
|
||||||
@@ -281,10 +316,11 @@ describe("createPresentationRoomService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does not broadcast location changes between rooms", () => {
|
it("does not broadcast location changes between rooms", () => {
|
||||||
const first = connectedRoom();
|
const { service } = makeService();
|
||||||
const second = connectedRoom();
|
const first = connectRoom(service);
|
||||||
|
const second = connectRoom(service);
|
||||||
|
|
||||||
expect(first.service.publish(first.presenterToken, {
|
expect(service.publish(first.presenterToken, {
|
||||||
type: "location.publish",
|
type: "location.publish",
|
||||||
hash: "#scene/first-room",
|
hash: "#scene/first-room",
|
||||||
baseRevision: 0,
|
baseRevision: 0,
|
||||||
|
|||||||
@@ -245,14 +245,15 @@ export const createPresentationRoomService = (options: {
|
|||||||
return { kind: "connected", snapshot: room.snapshot, presence };
|
return { kind: "connected", snapshot: room.snapshot, presence };
|
||||||
},
|
},
|
||||||
|
|
||||||
disconnect(token: string): void {
|
disconnect(token: string, peer: PresentationPeer): void {
|
||||||
const membership = membershipByToken.get(token);
|
const membership = membershipByToken.get(token);
|
||||||
const room = membership?.room;
|
const room = membership?.room;
|
||||||
if (
|
if (
|
||||||
membership === undefined ||
|
membership === undefined ||
|
||||||
room === undefined ||
|
room === undefined ||
|
||||||
!roomsById.has(room.id) ||
|
!roomsById.has(room.id) ||
|
||||||
membership.peer === null
|
membership.peer === null ||
|
||||||
|
membership.peer !== peer
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user