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)
This commit is contained in:
Родитель
aa2718b106
Коммит
7f8f204cda
|
@ -168,14 +168,25 @@ function makePresenceView(
|
||||||
if (audience !== undefined) {
|
if (audience !== undefined) {
|
||||||
presenceConfig.presence.events.on("attendeeJoined", (attendee) => {
|
presenceConfig.presence.events.on("attendeeJoined", (attendee) => {
|
||||||
const name = audience.getMembers().get(attendee.getConnectionId())?.name;
|
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);
|
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);
|
logDiv.append(logHeaderDiv, logContentDiv);
|
||||||
|
|
||||||
presenceConfig.lastRoll.events.on("updated", (update) => {
|
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);
|
addLogEntry(logContentDiv, updateText);
|
||||||
|
|
||||||
makeDiceValuesView(statesContentDiv, presenceConfig.lastRoll);
|
makeDiceValuesView(statesContentDiv, presenceConfig.lastRoll);
|
||||||
|
|
|
@ -43,7 +43,7 @@ export const brandedObjectEntries = Object.entries as <K extends string, T>(
|
||||||
*/
|
*/
|
||||||
export type IEphemeralRuntime = Pick<
|
export type IEphemeralRuntime = Pick<
|
||||||
(IContainerRuntime & IRuntimeInternal) | IFluidDataStoreRuntime,
|
(IContainerRuntime & IRuntimeInternal) | IFluidDataStoreRuntime,
|
||||||
"clientId" | "connected" | "getQuorum" | "off" | "on" | "submitSignal"
|
"clientId" | "connected" | "getAudience" | "getQuorum" | "off" | "on" | "submitSignal"
|
||||||
> &
|
> &
|
||||||
Partial<Pick<IFluidDataStoreRuntime, "logger">>;
|
Partial<Pick<IFluidDataStoreRuntime, "logger">>;
|
||||||
|
|
||||||
|
|
|
@ -267,7 +267,12 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager {
|
||||||
// Direct to the appropriate Presence Workspace, if present.
|
// Direct to the appropriate Presence Workspace, if present.
|
||||||
const workspace = this.workspaces.get(workspaceAddress);
|
const workspace = this.workspaces.get(workspaceAddress);
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
workspace.internal.processUpdate(received, timeModifier, remoteDatastore);
|
workspace.internal.processUpdate(
|
||||||
|
received,
|
||||||
|
timeModifier,
|
||||||
|
remoteDatastore,
|
||||||
|
message.clientId,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// All broadcast state is kept even if not currently registered, unless a value
|
// All broadcast state is kept even if not currently registered, unless a value
|
||||||
// notes itself to be ignored.
|
// notes itself to be ignored.
|
||||||
|
|
|
@ -85,6 +85,12 @@ class PresenceManager
|
||||||
|
|
||||||
runtime.on("connected", this.onConnect.bind(this));
|
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.
|
// Check if already connected at the time of construction.
|
||||||
// If constructed during data store load, the runtime may already be connected
|
// If constructed during data store load, the runtime may already be connected
|
||||||
// and the "connected" event will be raised during completion. With construction
|
// and the "connected" event will be raised during completion. With construction
|
||||||
|
@ -167,6 +173,7 @@ function setupSubComponents(
|
||||||
clientSessionId,
|
clientSessionId,
|
||||||
systemWorkspaceDatastore,
|
systemWorkspaceDatastore,
|
||||||
events,
|
events,
|
||||||
|
runtime.getAudience(),
|
||||||
);
|
);
|
||||||
const datastoreManager = new PresenceDatastoreManagerImpl(
|
const datastoreManager = new PresenceDatastoreManagerImpl(
|
||||||
clientSessionId,
|
clientSessionId,
|
||||||
|
|
|
@ -104,6 +104,7 @@ export interface PresenceStatesInternal {
|
||||||
received: number,
|
received: number,
|
||||||
timeModifier: number,
|
timeModifier: number,
|
||||||
remoteDatastore: ValueUpdateRecord,
|
remoteDatastore: ValueUpdateRecord,
|
||||||
|
senderConnectionId: ClientConnectionId,
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* Licensed under the MIT License.
|
* Licensed under the MIT License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { IAudience } from "@fluidframework/container-definitions";
|
||||||
import { assert } from "@fluidframework/core-utils/internal";
|
import { assert } from "@fluidframework/core-utils/internal";
|
||||||
|
|
||||||
import type { ClientConnectionId } from "./baseTypes.js";
|
import type { ClientConnectionId } from "./baseTypes.js";
|
||||||
|
@ -60,14 +61,9 @@ class SessionClient implements ISessionClient {
|
||||||
return this.connectionStatus;
|
return this.connectionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setConnectionId(
|
public setConnectionId(connectionId: ClientConnectionId): void {
|
||||||
connectionId: ClientConnectionId,
|
|
||||||
updateStatus: boolean = true,
|
|
||||||
): void {
|
|
||||||
this.connectionId = connectionId;
|
this.connectionId = connectionId;
|
||||||
if (updateStatus) {
|
this.connectionStatus = SessionClientStatus.Connected;
|
||||||
this.connectionStatus = SessionClientStatus.Connected;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setDisconnected(): void {
|
public setDisconnected(): void {
|
||||||
|
@ -114,6 +110,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace {
|
||||||
private readonly events: IEmitter<
|
private readonly events: IEmitter<
|
||||||
Pick<PresenceEvents, "attendeeJoined" | "attendeeDisconnected">
|
Pick<PresenceEvents, "attendeeJoined" | "attendeeDisconnected">
|
||||||
>,
|
>,
|
||||||
|
private readonly audience: IAudience,
|
||||||
) {
|
) {
|
||||||
this.selfAttendee = new SessionClient(clientSessionId);
|
this.selfAttendee = new SessionClient(clientSessionId);
|
||||||
this.attendees.set(clientSessionId, this.selfAttendee);
|
this.attendees.set(clientSessionId, this.selfAttendee);
|
||||||
|
@ -137,8 +134,11 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
senderConnectionId: ClientConnectionId,
|
||||||
): void {
|
): void {
|
||||||
const postUpdateActions: (() => void)[] = [];
|
const postUpdateActions: (() => void)[] = [];
|
||||||
|
const audienceMembers = this.audience.getMembers();
|
||||||
|
const connectedAttendees = new Set<SessionClient>();
|
||||||
for (const [clientConnectionId, value] of Object.entries(
|
for (const [clientConnectionId, value] of Object.entries(
|
||||||
remoteDatastore.clientToSessionId,
|
remoteDatastore.clientToSessionId,
|
||||||
)) {
|
)) {
|
||||||
|
@ -148,9 +148,26 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace {
|
||||||
clientConnectionId,
|
clientConnectionId,
|
||||||
/* order */ value.rev,
|
/* 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<ClientSessionId> | undefined =
|
const knownSessionId: InternalTypes.ValueRequiredState<ClientSessionId> | undefined =
|
||||||
this.datastore.clientToSessionId[clientConnectionId];
|
this.datastore.clientToSessionId[clientConnectionId];
|
||||||
if (knownSessionId === undefined) {
|
if (knownSessionId === undefined) {
|
||||||
|
@ -159,6 +176,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace {
|
||||||
assert(knownSessionId.value === value.value, 0xa5a /* Mismatched SessionId */);
|
assert(knownSessionId.value === value.value, 0xa5a /* Mismatched SessionId */);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: reorganize processUpdate and caller to process actions after all updates are processed.
|
// TODO: reorganize processUpdate and caller to process actions after all updates are processed.
|
||||||
for (const action of postUpdateActions) {
|
for (const action of postUpdateActions) {
|
||||||
action();
|
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,
|
// 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.
|
// therefore we should not change the attendee connection status or emit a disconnect event.
|
||||||
const attendeeReconnected = attendee.getConnectionId() !== clientConnectionId;
|
const attendeeReconnected = attendee.getConnectionId() !== clientConnectionId;
|
||||||
if (!attendeeReconnected) {
|
const connected = attendee.getConnectionStatus() === SessionClientStatus.Connected;
|
||||||
|
if (!attendeeReconnected && connected) {
|
||||||
attendee.setDisconnected();
|
attendee.setDisconnected();
|
||||||
this.events.emit("attendeeDisconnected", attendee);
|
this.events.emit("attendeeDisconnected", attendee);
|
||||||
}
|
}
|
||||||
|
@ -224,9 +243,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace {
|
||||||
): { attendee: SessionClient; isNew: boolean } {
|
): { attendee: SessionClient; isNew: boolean } {
|
||||||
let attendee = this.attendees.get(clientSessionId);
|
let attendee = this.attendees.get(clientSessionId);
|
||||||
let isNew = false;
|
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) {
|
if (attendee === undefined) {
|
||||||
// New attendee. Create SessionClient and add session ID based
|
// New attendee. Create SessionClient and add session ID based
|
||||||
// entry to map.
|
// entry to map.
|
||||||
|
@ -237,10 +254,12 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace {
|
||||||
// The given association is newer than the one we have.
|
// The given association is newer than the one we have.
|
||||||
// Update the order and current connection ID.
|
// Update the order and current connection ID.
|
||||||
attendee.order = order;
|
attendee.order = order;
|
||||||
attendee.setConnectionId(clientConnectionId, /* updateStatus */ false);
|
attendee.setConnectionId(clientConnectionId);
|
||||||
|
isNew = true;
|
||||||
}
|
}
|
||||||
// Always update entry for the connection ID. (Okay if already set.)
|
// Always update entry for the connection ID. (Okay if already set.)
|
||||||
this.attendees.set(clientConnectionId, attendee);
|
this.attendees.set(clientConnectionId, attendee);
|
||||||
|
|
||||||
return { attendee, isNew };
|
return { attendee, isNew };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -254,6 +273,7 @@ export function createSystemWorkspace(
|
||||||
clientSessionId: ClientSessionId,
|
clientSessionId: ClientSessionId,
|
||||||
datastore: SystemWorkspaceDatastore,
|
datastore: SystemWorkspaceDatastore,
|
||||||
events: IEmitter<Pick<PresenceEvents, "attendeeJoined">>,
|
events: IEmitter<Pick<PresenceEvents, "attendeeJoined">>,
|
||||||
|
audience: IAudience,
|
||||||
): {
|
): {
|
||||||
workspace: SystemWorkspace;
|
workspace: SystemWorkspace;
|
||||||
statesEntry: {
|
statesEntry: {
|
||||||
|
@ -261,7 +281,7 @@ export function createSystemWorkspace(
|
||||||
public: PresenceStates<PresenceStatesSchema>;
|
public: PresenceStates<PresenceStatesSchema>;
|
||||||
};
|
};
|
||||||
} {
|
} {
|
||||||
const workspace = new SystemWorkspaceImpl(clientSessionId, datastore, events);
|
const workspace = new SystemWorkspaceImpl(clientSessionId, datastore, events, audience);
|
||||||
return {
|
return {
|
||||||
workspace,
|
workspace,
|
||||||
statesEntry: {
|
statesEntry: {
|
||||||
|
|
|
@ -5,13 +5,48 @@
|
||||||
|
|
||||||
import { strict as assert } from "node:assert";
|
import { strict as assert } from "node:assert";
|
||||||
|
|
||||||
|
import type { IAudience } from "@fluidframework/container-definitions";
|
||||||
import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
|
import type { ITelemetryBaseLogger } from "@fluidframework/core-interfaces";
|
||||||
import type { IQuorumClients, ISequencedClient } from "@fluidframework/driver-definitions";
|
import type {
|
||||||
import { MockQuorumClients } from "@fluidframework/test-runtime-utils/internal";
|
IClient,
|
||||||
|
IQuorumClients,
|
||||||
|
ISequencedClient,
|
||||||
|
} from "@fluidframework/driver-definitions";
|
||||||
|
import { MockAudience, MockQuorumClients } from "@fluidframework/test-runtime-utils/internal";
|
||||||
|
|
||||||
import type { ClientConnectionId } from "../baseTypes.js";
|
import type { ClientConnectionId } from "../baseTypes.js";
|
||||||
import type { IEphemeralRuntime } from "../internalTypes.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<string, IClient>();
|
||||||
|
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.
|
* 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 {
|
export class MockEphemeralRuntime implements IEphemeralRuntime {
|
||||||
public logger?: ITelemetryBaseLogger;
|
public logger?: ITelemetryBaseLogger;
|
||||||
public readonly quorum: IQuorumClients;
|
public readonly quorum: IQuorumClients;
|
||||||
|
public readonly audience: IAudience;
|
||||||
|
|
||||||
public readonly listeners: {
|
public readonly listeners: {
|
||||||
connected: ((clientId: ClientConnectionId) => void)[];
|
connected: ((clientId: ClientConnectionId) => void)[];
|
||||||
|
disconnected: (() => void)[];
|
||||||
} = {
|
} = {
|
||||||
connected: [],
|
connected: [],
|
||||||
|
disconnected: [],
|
||||||
};
|
};
|
||||||
private isSupportedEvent(event: string): event is keyof typeof this.listeners {
|
private isSupportedEvent(event: string): event is keyof typeof this.listeners {
|
||||||
return event in this.listeners;
|
return event in this.listeners;
|
||||||
|
@ -65,14 +103,12 @@ export class MockEphemeralRuntime implements IEphemeralRuntime {
|
||||||
if (logger !== undefined) {
|
if (logger !== undefined) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
const quorum = makeMockQuorum([
|
|
||||||
"client0",
|
const clients = ["client0", "client1", "client2", "client3", "client4", "client5"];
|
||||||
"client1",
|
const quorum = makeMockQuorum(clients);
|
||||||
"client2",
|
const audience: IAudience = makeMockAudience(clients);
|
||||||
"client3",
|
this.audience = audience;
|
||||||
"client4",
|
this.getAudience = () => audience;
|
||||||
"client5",
|
|
||||||
]);
|
|
||||||
this.quorum = quorum;
|
this.quorum = quorum;
|
||||||
this.getQuorum = () => quorum;
|
this.getQuorum = () => quorum;
|
||||||
this.on = (
|
this.on = (
|
||||||
|
@ -126,6 +162,8 @@ export class MockEphemeralRuntime implements IEphemeralRuntime {
|
||||||
throw new Error("IEphemeralRuntime.off method not implemented.");
|
throw new Error("IEphemeralRuntime.off method not implemented.");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getAudience: () => ReturnType<IEphemeralRuntime["getAudience"]>;
|
||||||
|
|
||||||
public getQuorum: () => ReturnType<IEphemeralRuntime["getQuorum"]>;
|
public getQuorum: () => ReturnType<IEphemeralRuntime["getQuorum"]>;
|
||||||
|
|
||||||
public submitSignal: IEphemeralRuntime["submitSignal"] = (
|
public submitSignal: IEphemeralRuntime["submitSignal"] = (
|
||||||
|
|
|
@ -92,19 +92,10 @@ describe("Presence", () => {
|
||||||
describe("attendee", () => {
|
describe("attendee", () => {
|
||||||
const newAttendeeSessionId = "sessionId-4";
|
const newAttendeeSessionId = "sessionId-4";
|
||||||
const initialAttendeeConnectionId = "client4";
|
const initialAttendeeConnectionId = "client4";
|
||||||
let newAttendee: ISessionClient | undefined;
|
|
||||||
let initialAttendeeSignal: ReturnType<typeof generateBasicClientJoin>;
|
let initialAttendeeSignal: ReturnType<typeof generateBasicClientJoin>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
runtime.submitSignal = () => {};
|
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, {
|
initialAttendeeSignal = generateBasicClientJoin(clock.now - 50, {
|
||||||
averageLatency: 50,
|
averageLatency: 50,
|
||||||
clientSessionId: newAttendeeSessionId,
|
clientSessionId: newAttendeeSessionId,
|
||||||
|
@ -113,114 +104,233 @@ describe("Presence", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is announced via `attendeeJoined` when new", () => {
|
it("is not announced via `attendeeDisconnected` when unknown connection is removed", () => {
|
||||||
// Act - simulate join message from client
|
// Setup
|
||||||
presence.processSignal("", initialAttendeeSignal, false);
|
presence.events.on("attendeeDisconnected", () => {
|
||||||
|
assert.fail("ateendeeDisconnected should not be emitted for unknown connection.");
|
||||||
|
});
|
||||||
|
|
||||||
// Verify
|
// Act & Verify - remove unknown connection id
|
||||||
assert(newAttendee !== undefined, "No attendee was announced");
|
presence.removeClientConnectionId("unknownConnectionId");
|
||||||
assert.equal(
|
|
||||||
newAttendee.sessionId,
|
|
||||||
newAttendeeSessionId,
|
|
||||||
"Attendee has wrong session id",
|
|
||||||
);
|
|
||||||
assert.equal(
|
|
||||||
newAttendee.getConnectionId(),
|
|
||||||
initialAttendeeConnectionId,
|
|
||||||
"Attendee has wrong client connection id",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("disconnects", () => {
|
describe("that is joining", () => {
|
||||||
let disconnectedAttendee: ISessionClient | undefined;
|
it('is announced via `attendeeJoined` with status "Connected" when new', () => {
|
||||||
beforeEach(() => {
|
// Setup
|
||||||
disconnectedAttendee = undefined;
|
let newAttendee: ISessionClient | undefined;
|
||||||
afterCleanUp.push(
|
afterCleanUp.push(
|
||||||
presence.events.on("attendeeDisconnected", (attendee) => {
|
presence.events.on("attendeeJoined", (attendee) => {
|
||||||
assert(
|
assert(newAttendee === undefined, "Only one attendee should be announced");
|
||||||
disconnectedAttendee === undefined,
|
newAttendee = attendee;
|
||||||
"Only one attendee should be disconnected",
|
}),
|
||||||
);
|
);
|
||||||
disconnectedAttendee = 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
|
// Setup - simulate join message from client
|
||||||
presence.processSignal("", initialAttendeeSignal, false);
|
presence.processSignal("", initialAttendeeSignal, false);
|
||||||
|
assert(knownAttendee !== undefined, "No attendee was announced in setup");
|
||||||
// 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");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [desc, id] of [
|
for (const [desc, id] of [
|
||||||
["connection id", initialAttendeeConnectionId] as const,
|
["connection id", initialAttendeeConnectionId] as const,
|
||||||
["session id", newAttendeeSessionId] as const,
|
["session id", newAttendeeSessionId] as const,
|
||||||
]) {
|
]) {
|
||||||
it(`is available from \`getAttendee\` by ${desc}`, () => {
|
describe(`is available from \`getAttendee\` by ${desc}`, () => {
|
||||||
// Act
|
it('with status "Connected"', () => {
|
||||||
const attendee = presence.getAttendee(id);
|
// Act
|
||||||
// Verify
|
const attendee = presence.getAttendee(id);
|
||||||
assert.equal(attendee, newAttendee, "getAttendee returned wrong attendee");
|
// 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`", () => {
|
describe("is available from `getAttendees`", () => {
|
||||||
// Setup
|
it('with status "Connected"', () => {
|
||||||
assert(newAttendee !== undefined, "No attendee was set in beforeEach");
|
// Setup
|
||||||
|
assert(knownAttendee !== undefined, "No attendee was set in beforeEach");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const attendees = presence.getAttendees();
|
const attendees = presence.getAttendees();
|
||||||
assert(attendees.has(newAttendee), "getAttendees set does not contain attendee");
|
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)", () => {
|
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);
|
clock.tick(10);
|
||||||
|
|
||||||
// Act & Verify - simulate duplicate join message from client
|
// Act & Verify - simulate duplicate join message from client
|
||||||
presence.processSignal("", initialAttendeeSignal, false);
|
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
|
// Setup
|
||||||
assert(newAttendee !== undefined, "No attendee was set in beforeEach");
|
assert(priorAttendee !== undefined, "No attendee was set in beforeEach");
|
||||||
|
|
||||||
const updatedClientConnectionId = "client5";
|
const updatedClientConnectionId = "client5";
|
||||||
clock.tick(20);
|
clock.tick(20);
|
||||||
|
@ -245,27 +355,27 @@ describe("Presence", () => {
|
||||||
// Verify
|
// Verify
|
||||||
// Session id is unchanged
|
// Session id is unchanged
|
||||||
assert.equal(
|
assert.equal(
|
||||||
newAttendee.sessionId,
|
priorAttendee.sessionId,
|
||||||
newAttendeeSessionId,
|
newAttendeeSessionId,
|
||||||
"Attendee has wrong session id",
|
"Attendee has wrong session id",
|
||||||
);
|
);
|
||||||
// Current connection id is updated
|
// Current connection id is updated
|
||||||
assert(
|
assert(
|
||||||
newAttendee.getConnectionId() === updatedClientConnectionId,
|
priorAttendee.getConnectionId() === updatedClientConnectionId,
|
||||||
"Attendee does not have updated client connection id",
|
"Attendee does not have updated client connection id",
|
||||||
);
|
);
|
||||||
// Attendee is available via new connection id
|
// Attendee is available via new connection id
|
||||||
const attendeeViaUpdatedId = presence.getAttendee(updatedClientConnectionId);
|
const attendeeViaUpdatedId = presence.getAttendee(updatedClientConnectionId);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
attendeeViaUpdatedId,
|
attendeeViaUpdatedId,
|
||||||
newAttendee,
|
priorAttendee,
|
||||||
"getAttendee returned wrong attendee for updated connection id",
|
"getAttendee returned wrong attendee for updated connection id",
|
||||||
);
|
);
|
||||||
// Attendee is available via old connection id
|
// Attendee is available via old connection id
|
||||||
const attendeeViaOriginalId = presence.getAttendee(initialAttendeeConnectionId);
|
const attendeeViaOriginalId = presence.getAttendee(initialAttendeeConnectionId);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
attendeeViaOriginalId,
|
attendeeViaOriginalId,
|
||||||
newAttendee,
|
priorAttendee,
|
||||||
"getAttendee returned wrong attendee for original connection id",
|
"getAttendee returned wrong attendee for original connection id",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
Загрузка…
Ссылка в новой задаче