From 7f8f204cda31c2b9b1800038db6e971d4e888d87 Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:49:58 -0800 Subject: [PATCH] improvement(client-presence): Use audience for session client connection status (#23006) ## Description This PR updates SystemWorkspace to use audience for session client connection status. We also add audience support to Mock Ephemeral Runtime w/ additional reworking of unit tests Presence + Audience test cases to add: - Unknown attendee joins that is in audience (=> emits attendeeJoined) - Unknown attendee joins that is not in audience (=> does not emit attendeeJoined) - Known attendee rejoins that is in audience w/ same connection (duplicate signal) (=> does not emit attendeeJoined) - Known attendee rejoins that is in audience w/ different connection (=> emits attendeeJoined) - Known attendee rejoins that is not in audience w/ same connection (=> does not emit attendeeJoined) - Known attendee rejoins that is not in audience w/ different connection (=> does not emit attendeeJoined) --- .../external-controller/src/view.ts | 15 +- .../framework/presence/src/internalTypes.ts | 2 +- .../presence/src/presenceDatastoreManager.ts | 7 +- .../framework/presence/src/presenceManager.ts | 7 + .../framework/presence/src/presenceStates.ts | 1 + .../framework/presence/src/systemWorkspace.ts | 50 ++- .../presence/src/test/mockEphemeralRuntime.ts | 58 +++- .../presence/src/test/presenceManager.spec.ts | 296 ++++++++++++------ 8 files changed, 314 insertions(+), 122 deletions(-) diff --git a/examples/service-clients/azure-client/external-controller/src/view.ts b/examples/service-clients/azure-client/external-controller/src/view.ts index c3cb1aea37e..c9dded15015 100644 --- a/examples/service-clients/azure-client/external-controller/src/view.ts +++ b/examples/service-clients/azure-client/external-controller/src/view.ts @@ -168,14 +168,25 @@ function makePresenceView( if (audience !== undefined) { presenceConfig.presence.events.on("attendeeJoined", (attendee) => { const name = audience.getMembers().get(attendee.getConnectionId())?.name; - const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} with id ${attendee.sessionId} joined`; + const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} 🔗 with id ${attendee.sessionId} joined`; addLogEntry(logContentDiv, update); }); + + presenceConfig.presence.events.on("attendeeDisconnected", (attendee) => { + // Filter for remote attendees + const self = audience.getMyself(); + if (self && attendee !== presenceConfig.presence.getAttendee(self.currentConnection)) { + const name = audience.getMembers().get(attendee.getConnectionId())?.name; + const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} ⛓️‍💥 with id ${attendee.sessionId} left`; + addLogEntry(logContentDiv, update); + } + }); } logDiv.append(logHeaderDiv, logContentDiv); presenceConfig.lastRoll.events.on("updated", (update) => { - const updateText = `updated ${update.client.sessionId.slice(0, 8)}'s last rolls to ${JSON.stringify(update.value)}`; + const connected = update.client.getConnectionStatus() === "Connected" ? "🔗" : "⛓️‍💥"; + const updateText = `updated ${update.client.sessionId.slice(0, 8)}'s ${connected} last rolls to ${JSON.stringify(update.value)}`; addLogEntry(logContentDiv, updateText); makeDiceValuesView(statesContentDiv, presenceConfig.lastRoll); diff --git a/packages/framework/presence/src/internalTypes.ts b/packages/framework/presence/src/internalTypes.ts index 767366ac50e..8315db837d6 100644 --- a/packages/framework/presence/src/internalTypes.ts +++ b/packages/framework/presence/src/internalTypes.ts @@ -43,7 +43,7 @@ export const brandedObjectEntries = Object.entries as ( */ export type IEphemeralRuntime = Pick< (IContainerRuntime & IRuntimeInternal) | IFluidDataStoreRuntime, - "clientId" | "connected" | "getQuorum" | "off" | "on" | "submitSignal" + "clientId" | "connected" | "getAudience" | "getQuorum" | "off" | "on" | "submitSignal" > & Partial>; diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index 0857b96e8c6..071a5fc7145 100644 --- a/packages/framework/presence/src/presenceDatastoreManager.ts +++ b/packages/framework/presence/src/presenceDatastoreManager.ts @@ -267,7 +267,12 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { // Direct to the appropriate Presence Workspace, if present. const workspace = this.workspaces.get(workspaceAddress); if (workspace) { - workspace.internal.processUpdate(received, timeModifier, remoteDatastore); + workspace.internal.processUpdate( + received, + timeModifier, + remoteDatastore, + message.clientId, + ); } else { // All broadcast state is kept even if not currently registered, unless a value // notes itself to be ignored. diff --git a/packages/framework/presence/src/presenceManager.ts b/packages/framework/presence/src/presenceManager.ts index 92bcb3eaa6b..9a2a7ffa606 100644 --- a/packages/framework/presence/src/presenceManager.ts +++ b/packages/framework/presence/src/presenceManager.ts @@ -85,6 +85,12 @@ class PresenceManager runtime.on("connected", this.onConnect.bind(this)); + runtime.on("disconnected", () => { + if (runtime.clientId !== undefined) { + this.removeClientConnectionId(runtime.clientId); + } + }); + // Check if already connected at the time of construction. // If constructed during data store load, the runtime may already be connected // and the "connected" event will be raised during completion. With construction @@ -167,6 +173,7 @@ function setupSubComponents( clientSessionId, systemWorkspaceDatastore, events, + runtime.getAudience(), ); const datastoreManager = new PresenceDatastoreManagerImpl( clientSessionId, diff --git a/packages/framework/presence/src/presenceStates.ts b/packages/framework/presence/src/presenceStates.ts index a15c9236d1c..51aad05b4d0 100644 --- a/packages/framework/presence/src/presenceStates.ts +++ b/packages/framework/presence/src/presenceStates.ts @@ -104,6 +104,7 @@ export interface PresenceStatesInternal { received: number, timeModifier: number, remoteDatastore: ValueUpdateRecord, + senderConnectionId: ClientConnectionId, ): void; } diff --git a/packages/framework/presence/src/systemWorkspace.ts b/packages/framework/presence/src/systemWorkspace.ts index 01a693e81ca..9f5dc938dbe 100644 --- a/packages/framework/presence/src/systemWorkspace.ts +++ b/packages/framework/presence/src/systemWorkspace.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import type { IAudience } from "@fluidframework/container-definitions"; import { assert } from "@fluidframework/core-utils/internal"; import type { ClientConnectionId } from "./baseTypes.js"; @@ -60,14 +61,9 @@ class SessionClient implements ISessionClient { return this.connectionStatus; } - public setConnectionId( - connectionId: ClientConnectionId, - updateStatus: boolean = true, - ): void { + public setConnectionId(connectionId: ClientConnectionId): void { this.connectionId = connectionId; - if (updateStatus) { - this.connectionStatus = SessionClientStatus.Connected; - } + this.connectionStatus = SessionClientStatus.Connected; } public setDisconnected(): void { @@ -114,6 +110,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { private readonly events: IEmitter< Pick >, + private readonly audience: IAudience, ) { this.selfAttendee = new SessionClient(clientSessionId); this.attendees.set(clientSessionId, this.selfAttendee); @@ -137,8 +134,11 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { }; }; }, + senderConnectionId: ClientConnectionId, ): void { const postUpdateActions: (() => void)[] = []; + const audienceMembers = this.audience.getMembers(); + const connectedAttendees = new Set(); for (const [clientConnectionId, value] of Object.entries( remoteDatastore.clientToSessionId, )) { @@ -148,9 +148,26 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { clientConnectionId, /* order */ value.rev, ); - if (isNew) { - postUpdateActions.push(() => this.events.emit("attendeeJoined", attendee)); + + // Check new attendee against audience to see if they're currently connected + const isAttendeeConnected = audienceMembers.has(clientConnectionId); + + if (isAttendeeConnected) { + connectedAttendees.add(attendee); + if (attendee.getConnectionStatus() === SessionClientStatus.Disconnected) { + attendee.setConnectionId(clientConnectionId); + } + if (isNew) { + // If the attendee is both new and in audience (i.e. currently connected), emit an attendeeJoined event. + postUpdateActions.push(() => this.events.emit("attendeeJoined", attendee)); + } } + + // If the attendee is not in the audience, they are considered disconnected. + if (!connectedAttendees.has(attendee)) { + attendee.setDisconnected(); + } + const knownSessionId: InternalTypes.ValueRequiredState | undefined = this.datastore.clientToSessionId[clientConnectionId]; if (knownSessionId === undefined) { @@ -159,6 +176,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { assert(knownSessionId.value === value.value, 0xa5a /* Mismatched SessionId */); } } + // TODO: reorganize processUpdate and caller to process actions after all updates are processed. for (const action of postUpdateActions) { action(); @@ -185,7 +203,8 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { // If the last known connectionID is different from the connection ID being removed, the attendee has reconnected, // therefore we should not change the attendee connection status or emit a disconnect event. const attendeeReconnected = attendee.getConnectionId() !== clientConnectionId; - if (!attendeeReconnected) { + const connected = attendee.getConnectionStatus() === SessionClientStatus.Connected; + if (!attendeeReconnected && connected) { attendee.setDisconnected(); this.events.emit("attendeeDisconnected", attendee); } @@ -224,9 +243,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { ): { attendee: SessionClient; isNew: boolean } { let attendee = this.attendees.get(clientSessionId); let isNew = false; - // TODO #22616: Check for a current connection to determine best status. - // For now, always leave existing state as was last determined and - // assume new client is connected. + if (attendee === undefined) { // New attendee. Create SessionClient and add session ID based // entry to map. @@ -237,10 +254,12 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { // The given association is newer than the one we have. // Update the order and current connection ID. attendee.order = order; - attendee.setConnectionId(clientConnectionId, /* updateStatus */ false); + attendee.setConnectionId(clientConnectionId); + isNew = true; } // Always update entry for the connection ID. (Okay if already set.) this.attendees.set(clientConnectionId, attendee); + return { attendee, isNew }; } } @@ -254,6 +273,7 @@ export function createSystemWorkspace( clientSessionId: ClientSessionId, datastore: SystemWorkspaceDatastore, events: IEmitter>, + audience: IAudience, ): { workspace: SystemWorkspace; statesEntry: { @@ -261,7 +281,7 @@ export function createSystemWorkspace( public: PresenceStates; }; } { - const workspace = new SystemWorkspaceImpl(clientSessionId, datastore, events); + const workspace = new SystemWorkspaceImpl(clientSessionId, datastore, events, audience); return { workspace, statesEntry: { diff --git a/packages/framework/presence/src/test/mockEphemeralRuntime.ts b/packages/framework/presence/src/test/mockEphemeralRuntime.ts index 7cc04445dba..35dae908ad0 100644 --- a/packages/framework/presence/src/test/mockEphemeralRuntime.ts +++ b/packages/framework/presence/src/test/mockEphemeralRuntime.ts @@ -5,13 +5,48 @@ import { strict as assert } from "node:assert"; +import type { IAudience } from "@fluidframework/container-definitions"; import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces"; -import type { IQuorumClients, ISequencedClient } from "@fluidframework/driver-definitions"; -import { MockQuorumClients } from "@fluidframework/test-runtime-utils/internal"; +import type { + IClient, + IQuorumClients, + ISequencedClient, +} from "@fluidframework/driver-definitions"; +import { MockAudience, MockQuorumClients } from "@fluidframework/test-runtime-utils/internal"; import type { ClientConnectionId } from "../baseTypes.js"; import type { IEphemeralRuntime } from "../internalTypes.js"; +/** + * Creates a mock {@link @fluidframework/container-definitions#IAudience} for testing. + */ +export function makeMockAudience(clientIds: string[]): IAudience { + const clients = new Map(); + for (const [index, clientId] of clientIds.entries()) { + // eslint-disable-next-line unicorn/prefer-code-point + const stringId = String.fromCharCode(index + 65); + const name = stringId.repeat(10); + const userId = `${name}@microsoft.com`; + const user = { + id: userId, + }; + clients.set(clientId, { + mode: index % 2 === 0 ? "write" : "read", + details: { capabilities: { interactive: true } }, + permission: [], + user, + scopes: [], + }); + } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const audience = new MockAudience(); + for (const [clientId, client] of clients.entries()) { + audience.addMember(clientId, client); + } + + return audience as IAudience; +} + /** * Creates a mock {@link @fluidframework/protocol-definitions#IQuorumClients} for testing. */ @@ -48,11 +83,14 @@ export function makeMockQuorum(clientIds: string[]): IQuorumClients { export class MockEphemeralRuntime implements IEphemeralRuntime { public logger?: ITelemetryBaseLogger; public readonly quorum: IQuorumClients; + public readonly audience: IAudience; public readonly listeners: { connected: ((clientId: ClientConnectionId) => void)[]; + disconnected: (() => void)[]; } = { connected: [], + disconnected: [], }; private isSupportedEvent(event: string): event is keyof typeof this.listeners { return event in this.listeners; @@ -65,14 +103,12 @@ export class MockEphemeralRuntime implements IEphemeralRuntime { if (logger !== undefined) { this.logger = logger; } - const quorum = makeMockQuorum([ - "client0", - "client1", - "client2", - "client3", - "client4", - "client5", - ]); + + const clients = ["client0", "client1", "client2", "client3", "client4", "client5"]; + const quorum = makeMockQuorum(clients); + const audience: IAudience = makeMockAudience(clients); + this.audience = audience; + this.getAudience = () => audience; this.quorum = quorum; this.getQuorum = () => quorum; this.on = ( @@ -126,6 +162,8 @@ export class MockEphemeralRuntime implements IEphemeralRuntime { throw new Error("IEphemeralRuntime.off method not implemented."); }; + public getAudience: () => ReturnType; + public getQuorum: () => ReturnType; public submitSignal: IEphemeralRuntime["submitSignal"] = ( diff --git a/packages/framework/presence/src/test/presenceManager.spec.ts b/packages/framework/presence/src/test/presenceManager.spec.ts index 5fb8c4c62b2..ce97838dc05 100644 --- a/packages/framework/presence/src/test/presenceManager.spec.ts +++ b/packages/framework/presence/src/test/presenceManager.spec.ts @@ -92,19 +92,10 @@ describe("Presence", () => { describe("attendee", () => { const newAttendeeSessionId = "sessionId-4"; const initialAttendeeConnectionId = "client4"; - let newAttendee: ISessionClient | undefined; let initialAttendeeSignal: ReturnType; beforeEach(() => { runtime.submitSignal = () => {}; - newAttendee = undefined; - afterCleanUp.push( - presence.events.on("attendeeJoined", (attendee) => { - assert(newAttendee === undefined, "Only one attendee should be announced"); - newAttendee = attendee; - }), - ); - initialAttendeeSignal = generateBasicClientJoin(clock.now - 50, { averageLatency: 50, clientSessionId: newAttendeeSessionId, @@ -113,114 +104,233 @@ describe("Presence", () => { }); }); - it("is announced via `attendeeJoined` when new", () => { - // Act - simulate join message from client - presence.processSignal("", initialAttendeeSignal, false); + it("is not announced via `attendeeDisconnected` when unknown connection is removed", () => { + // Setup + presence.events.on("attendeeDisconnected", () => { + assert.fail("ateendeeDisconnected should not be emitted for unknown connection."); + }); - // Verify - assert(newAttendee !== undefined, "No attendee was announced"); - assert.equal( - newAttendee.sessionId, - newAttendeeSessionId, - "Attendee has wrong session id", - ); - assert.equal( - newAttendee.getConnectionId(), - initialAttendeeConnectionId, - "Attendee has wrong client connection id", - ); + // Act & Verify - remove unknown connection id + presence.removeClientConnectionId("unknownConnectionId"); }); - describe("disconnects", () => { - let disconnectedAttendee: ISessionClient | undefined; - beforeEach(() => { - disconnectedAttendee = undefined; + describe("that is joining", () => { + it('is announced via `attendeeJoined` with status "Connected" when new', () => { + // Setup + let newAttendee: ISessionClient | undefined; afterCleanUp.push( - presence.events.on("attendeeDisconnected", (attendee) => { - assert( - disconnectedAttendee === undefined, - "Only one attendee should be disconnected", - ); - disconnectedAttendee = attendee; + presence.events.on("attendeeJoined", (attendee) => { + assert(newAttendee === undefined, "Only one attendee should be announced"); + newAttendee = attendee; + }), + ); + + // Act - simulate join message from client + presence.processSignal("", initialAttendeeSignal, false); + + // Verify + assert(newAttendee !== undefined, "No attendee was announced"); + assert.equal( + newAttendee.sessionId, + newAttendeeSessionId, + "Attendee has wrong session id", + ); + assert.equal( + newAttendee.getConnectionId(), + initialAttendeeConnectionId, + "Attendee has wrong client connection id", + ); + assert.equal( + newAttendee.getConnectionStatus(), + SessionClientStatus.Connected, + "Attendee connection status is not Connected", + ); + }); + }); + + describe("that is already known", () => { + let knownAttendee: ISessionClient | undefined; + beforeEach(() => { + afterCleanUp.push( + presence.events.on("attendeeJoined", (attendee) => { + knownAttendee = attendee; }), ); // Setup - simulate join message from client presence.processSignal("", initialAttendeeSignal, false); - - // Act - remove client connection id - presence.removeClientConnectionId(initialAttendeeConnectionId); - }); - - it("is announced via `attendeeDisconnected` when audience member leaves", () => { - // Verify - assert( - disconnectedAttendee !== undefined, - "No attendee was disconnected in beforeEach", - ); - assert.equal( - disconnectedAttendee.sessionId, - newAttendeeSessionId, - "Disconnected attendee has wrong session id", - ); - assert.equal( - disconnectedAttendee.getConnectionId(), - initialAttendeeConnectionId, - "Disconnected attendee has wrong client connection id", - ); - }); - - it("changes the session client status to `Disconnected`", () => { - // Verify - assert( - disconnectedAttendee !== undefined, - "No attendee was disconnected in beforeEach", - ); - assert.equal( - disconnectedAttendee.getConnectionStatus(), - SessionClientStatus.Disconnected, - "Disconnected attendee has wrong status", - ); - }); - }); - - describe("already known", () => { - beforeEach(() => { - // Setup - simulate join message from client - presence.processSignal("", initialAttendeeSignal, false); - assert(newAttendee !== undefined, "No attendee was announced in setup"); + assert(knownAttendee !== undefined, "No attendee was announced in setup"); }); for (const [desc, id] of [ ["connection id", initialAttendeeConnectionId] as const, ["session id", newAttendeeSessionId] as const, ]) { - it(`is available from \`getAttendee\` by ${desc}`, () => { - // Act - const attendee = presence.getAttendee(id); - // Verify - assert.equal(attendee, newAttendee, "getAttendee returned wrong attendee"); + describe(`is available from \`getAttendee\` by ${desc}`, () => { + it('with status "Connected"', () => { + // Act + const attendee = presence.getAttendee(id); + // Verify + assert.equal(attendee, knownAttendee, "getAttendee returned wrong attendee"); + assert.equal( + attendee.getConnectionStatus(), + SessionClientStatus.Connected, + "getAttendee returned attendee with wrong status", + ); + }); + + it('with status "Disconnected" after disconnect', () => { + // Act - remove client connection id + presence.removeClientConnectionId(initialAttendeeConnectionId); + const attendee = presence.getAttendee(id); + + // Verify + assert.equal(attendee, knownAttendee, "getAttendee returned wrong attendee"); + assert.equal( + attendee.getConnectionStatus(), + SessionClientStatus.Disconnected, + "getAttendee returned attendee with wrong status", + ); + }); }); } - it("is available from `getAttendees`", () => { - // Setup - assert(newAttendee !== undefined, "No attendee was set in beforeEach"); + describe("is available from `getAttendees`", () => { + it('with status "Connected"', () => { + // Setup + assert(knownAttendee !== undefined, "No attendee was set in beforeEach"); - // Act - const attendees = presence.getAttendees(); - assert(attendees.has(newAttendee), "getAttendees set does not contain attendee"); + // Act + const attendees = presence.getAttendees(); + assert( + attendees.has(knownAttendee), + "getAttendees set does not contain attendee", + ); + assert.equal( + knownAttendee.getConnectionStatus(), + SessionClientStatus.Connected, + "getAttendees set contains attendee with wrong status", + ); + }); + + it('with status "Disconnected"', () => { + // Setup + assert(knownAttendee !== undefined, "No attendee was set in beforeEach"); + + // Act - remove client connection id + presence.removeClientConnectionId(initialAttendeeConnectionId); + + // Verify + const attendees = presence.getAttendees(); + assert( + attendees.has(knownAttendee), + "getAttendees set does not contain attendee", + ); + assert.equal( + knownAttendee.getConnectionStatus(), + SessionClientStatus.Disconnected, + "getAttendees set contains attendee with wrong status", + ); + }); + }); + + it('is not announced via `attendeeJoined` when already "Connected"', () => { + // Setup + afterCleanUp.push( + presence.events.on("attendeeJoined", () => { + assert.fail("No attendee should be announced in beforeEach"); + }), + ); + + // Act - simulate join message from client + presence.processSignal("", initialAttendeeSignal, false); + }); + + describe("and has their connection removed", () => { + let disconnectedAttendee: ISessionClient | undefined; + beforeEach(() => { + disconnectedAttendee = undefined; + afterCleanUp.push( + presence.events.on("attendeeDisconnected", (attendee) => { + assert( + disconnectedAttendee === undefined, + "Only one attendee should be disconnected", + ); + disconnectedAttendee = attendee; + }), + ); + + // Act - remove client connection id + presence.removeClientConnectionId(initialAttendeeConnectionId); + }); + + it("is announced via `attendeeDisconnected`", () => { + // + assert(knownAttendee !== undefined, "No attendee was set in beforeEach"); + assert( + disconnectedAttendee !== undefined, + "No attendee was disconnected in removeClientConnectionId", + ); + assert.equal( + disconnectedAttendee.sessionId, + knownAttendee.sessionId, + "Disconnected attendee has wrong session id", + ); + assert.equal( + disconnectedAttendee.getConnectionId(), + initialAttendeeConnectionId, + "Disconnected attendee has wrong client connection id", + ); + assert.equal( + disconnectedAttendee.getConnectionStatus(), + SessionClientStatus.Disconnected, + "Disconnected attendee has wrong status", + ); + }); + + it('is not announced via `attendeeDisconnected` when already "Disconnected"', () => { + assert( + disconnectedAttendee !== undefined, + "No attendee was disconnected in removeClientConnectionId", + ); + + // Act & Verify - remove client connection id again + presence.removeClientConnectionId(initialAttendeeConnectionId); + }); + }); + }); + + describe("that is rejoining", () => { + let priorAttendee: ISessionClient | undefined; + beforeEach(() => { + afterCleanUp.push( + presence.events.on("attendeeJoined", (attendee) => { + priorAttendee = attendee; + }), + ); + + // Setup - simulate join message from client + presence.processSignal("", initialAttendeeSignal, false); + assert(priorAttendee !== undefined, "No attendee was announced in setup"); }); it("is NOT announced when rejoined with same connection (duplicate signal)", () => { + afterCleanUp.push( + presence.events.on("attendeeJoined", (attendee) => { + assert.fail( + "Attendee should not be announced when rejoining with same connection", + ); + }), + ); + clock.tick(10); // Act & Verify - simulate duplicate join message from client presence.processSignal("", initialAttendeeSignal, false); }); - it("is NOT announced when rejoined with different connection and current information is updated", () => { + it("is announced when rejoined with different connection and current information is updated", () => { // Setup - assert(newAttendee !== undefined, "No attendee was set in beforeEach"); + assert(priorAttendee !== undefined, "No attendee was set in beforeEach"); const updatedClientConnectionId = "client5"; clock.tick(20); @@ -245,27 +355,27 @@ describe("Presence", () => { // Verify // Session id is unchanged assert.equal( - newAttendee.sessionId, + priorAttendee.sessionId, newAttendeeSessionId, "Attendee has wrong session id", ); // Current connection id is updated assert( - newAttendee.getConnectionId() === updatedClientConnectionId, + priorAttendee.getConnectionId() === updatedClientConnectionId, "Attendee does not have updated client connection id", ); // Attendee is available via new connection id const attendeeViaUpdatedId = presence.getAttendee(updatedClientConnectionId); assert.equal( attendeeViaUpdatedId, - newAttendee, + priorAttendee, "getAttendee returned wrong attendee for updated connection id", ); // Attendee is available via old connection id const attendeeViaOriginalId = presence.getAttendee(initialAttendeeConnectionId); assert.equal( attendeeViaOriginalId, - newAttendee, + priorAttendee, "getAttendee returned wrong attendee for original connection id", ); });