From 2c03e7ec71db2da153e4bdeb2f2b4bc59340141d Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Mon, 28 Oct 2024 11:46:11 -0700 Subject: [PATCH 1/5] first commit --- .../azure-client/external-controller/src/view.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 8a1938ddf9c8..e2472c4997f2 100644 --- a/examples/service-clients/azure-client/external-controller/src/view.ts +++ b/examples/service-clients/azure-client/external-controller/src/view.ts @@ -171,6 +171,12 @@ function makePresenceView( const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} with id ${attendee.sessionId} joined`; addLogEntry(logContentDiv, update); }); + + presenceConfig.presence.events.on("attendeeDisconnected", (attendee) => { + const name = audience.getMembers().get(attendee.connectionId())?.name; + const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} with id ${attendee.sessionId} left`; + addLogEntry(logContentDiv, update); + }); } logDiv.append(logHeaderDiv, logContentDiv); From 0ab52c36e1357a2b369acc99782dc1315d4108d2 Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Tue, 29 Oct 2024 15:11:41 -0700 Subject: [PATCH 2/5] first commit --- .../external-controller/src/app.ts | 1 + .../external-controller/src/view.ts | 64 ++++++++++++++++++- .../external-controller/tests/index.ts | 2 +- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/examples/service-clients/azure-client/external-controller/src/app.ts b/examples/service-clients/azure-client/external-controller/src/app.ts index 1853b3c5707e..ed7d1443cf65 100644 --- a/examples/service-clients/azure-client/external-controller/src/app.ts +++ b/examples/service-clients/azure-client/external-controller/src/app.ts @@ -225,6 +225,7 @@ async function start(): Promise { const contentDiv = document.querySelector("#content") as HTMLDivElement; contentDiv.append( makeAppView( + container, [diceRollerController1, diceRollerController2], { presence, lastRoll: states.lastRoll }, services.audience, 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 e2472c4997f2..70b8d366e1a8 100644 --- a/examples/service-clients/azure-client/external-controller/src/view.ts +++ b/examples/service-clients/azure-client/external-controller/src/view.ts @@ -5,6 +5,7 @@ import type { IPresence, LatestValueManager } from "@fluid-experimental/presence"; import { AzureMember, IAzureAudience } from "@fluidframework/azure-client"; +import type { IFluidContainer } from "fluid-framework"; import { ICustomUserDetails } from "./app.js"; import { IDiceRollerController } from "./controller.js"; @@ -191,7 +192,66 @@ function makePresenceView( return presenceDiv; } +function makeAttendeeView(container: IFluidContainer, presence?: IPresence): HTMLDivElement { + const attendeeDiv = document.createElement("div"); + if (presence === undefined) { + attendeeDiv.textContent = "No presence provided"; + return attendeeDiv; + } + + attendeeDiv.style.display = "flex"; + attendeeDiv.style.justifyContent = "right"; + + createButton(container, attendeeDiv); + + presence.events.on("attendeeJoined", (attendee) => { + updateAttendees(presence, attendeeDiv); + createButton(container, attendeeDiv); + }); + presence.events.on("attendeeDisconnected", (attendee) => { + updateAttendees(presence, attendeeDiv); + createButton(container, attendeeDiv); + }); + container.on("connected", () => { + updateAttendees(presence, attendeeDiv); + createButton(container, attendeeDiv); + }); + container.on("disconnected", () => { + updateAttendees(presence, attendeeDiv); + createButton(container, attendeeDiv); + }); + + return attendeeDiv; +} + +function createButton(container: IFluidContainer, attendeeDiv: HTMLDivElement): void { + const toggleButton = document.createElement("button"); + const connected = container.connectionState === 2; + toggleButton.innerHTML = `${connected ? "Disconnect" : "Connect"}`; + toggleButton.style.marginLeft = "10px"; + + toggleButton.addEventListener("click", () => { + if (connected) { + container.disconnect(); + } else { + container.connect(); + } + }); + attendeeDiv.append(toggleButton); +} + +function updateAttendees(presence: IPresence, attendeeDiv: HTMLDivElement): void { + // Clear the existing attendees + attendeeDiv.innerHTML = ""; + const attendeeDivEntry = document.createElement("div"); + for (const attendee of presence.getAttendees()) { + attendeeDivEntry.innerHTML += `Attendee ${attendee.sessionId} is ${attendee.getStatus()}
`; + attendeeDiv.append(attendeeDivEntry); + } +} + export function makeAppView( + container: IFluidContainer, diceRollerControllers: IDiceRollerController[], // Biome insist on no semicolon - https://dev.azure.com/fluidframework/internal/_workitems/edit/9083 // eslint-disable-next-line @typescript-eslint/member-delimiter-style @@ -208,9 +268,11 @@ export function makeAppView( const audienceView = makeAudienceView(audience); + const attendeeView = makeAttendeeView(container, presenceConfig?.presence); + const presenceView = makePresenceView(presenceConfig, audience); const wrapperDiv = document.createElement("div"); - wrapperDiv.append(diceView, audienceView, presenceView); + wrapperDiv.append(diceView, audienceView, attendeeView, presenceView); return wrapperDiv; } diff --git a/examples/service-clients/azure-client/external-controller/tests/index.ts b/examples/service-clients/azure-client/external-controller/tests/index.ts index 1a77b3c41ed5..fd0e931f068a 100644 --- a/examples/service-clients/azure-client/external-controller/tests/index.ts +++ b/examples/service-clients/azure-client/external-controller/tests/index.ts @@ -140,7 +140,7 @@ async function createContainerAndRenderInElement( const diceRollerController = new DiceRollerController(sharedMap1, () => {}); const diceRollerController2 = new DiceRollerController(sharedMap2, () => {}); - element.append(makeAppView([diceRollerController, diceRollerController2])); + element.append(makeAppView(fluidContainer, [diceRollerController, diceRollerController2])); } /** From 7c343d7738c9781af5611c20a97686a6dd18a2a4 Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Tue, 29 Oct 2024 15:12:05 -0700 Subject: [PATCH 3/5] second commit --- .../presence/src/datastorePresenceManagerFactory.ts | 5 +++++ packages/framework/presence/src/presenceManager.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/framework/presence/src/datastorePresenceManagerFactory.ts b/packages/framework/presence/src/datastorePresenceManagerFactory.ts index d310cf4ea1c7..df2731dfbbeb 100644 --- a/packages/framework/presence/src/datastorePresenceManagerFactory.ts +++ b/packages/framework/presence/src/datastorePresenceManagerFactory.ts @@ -46,6 +46,11 @@ class PresenceManagerDataObject extends LoadableFluidObject { this.runtime.getAudience().on("removeMember", (clientId: string) => { manager.removeClientConnectionId(clientId); }); + this.runtime.on("disconnected", () => { + if (this.runtime.clientId !== undefined) { + manager.removeClientConnectionId(this.runtime.clientId); + } + }); this._presenceManager = manager; } return this._presenceManager; diff --git a/packages/framework/presence/src/presenceManager.ts b/packages/framework/presence/src/presenceManager.ts index 4cd2938a2715..5b8b1d61ee29 100644 --- a/packages/framework/presence/src/presenceManager.ts +++ b/packages/framework/presence/src/presenceManager.ts @@ -84,6 +84,11 @@ 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 From 0658b5e8a1121abb8112597d2aedb9ece246b3bd Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Wed, 30 Oct 2024 12:54:39 -0700 Subject: [PATCH 4/5] connectivity status --- .../external-controller/src/view.ts | 41 +++++++++++-------- .../src/datastorePresenceManagerFactory.ts | 5 --- .../framework/presence/src/presenceManager.ts | 1 + .../framework/presence/src/systemWorkspace.ts | 2 + 4 files changed, 26 insertions(+), 23 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 70b8d366e1a8..de67a6e760f6 100644 --- a/examples/service-clients/azure-client/external-controller/src/view.ts +++ b/examples/service-clients/azure-client/external-controller/src/view.ts @@ -174,9 +174,13 @@ function makePresenceView( }); presenceConfig.presence.events.on("attendeeDisconnected", (attendee) => { - const name = audience.getMembers().get(attendee.connectionId())?.name; - const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} with id ${attendee.sessionId} left`; - addLogEntry(logContentDiv, update); + // Filter for remote attendees + const self = audience.getMyself(); + if (self && attendee !== presenceConfig.presence.getAttendee(self.currentConnection)) { + const name = audience.getMembers().get(attendee.connectionId())?.name; + const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} with id ${attendee.sessionId} left`; + addLogEntry(logContentDiv, update); + } }); } logDiv.append(logHeaderDiv, logContentDiv); @@ -200,36 +204,36 @@ function makeAttendeeView(container: IFluidContainer, presence?: IPresence): HTM } attendeeDiv.style.display = "flex"; - attendeeDiv.style.justifyContent = "right"; - - createButton(container, attendeeDiv); + attendeeDiv.style.justifyContent = "center"; + attendeeDiv.style.margin = "70px"; - presence.events.on("attendeeJoined", (attendee) => { + presence.events.on("attendeeJoined", () => { updateAttendees(presence, attendeeDiv); - createButton(container, attendeeDiv); + updateButton(container, attendeeDiv); }); - presence.events.on("attendeeDisconnected", (attendee) => { + presence.events.on("attendeeDisconnected", () => { updateAttendees(presence, attendeeDiv); - createButton(container, attendeeDiv); + updateButton(container, attendeeDiv); }); container.on("connected", () => { updateAttendees(presence, attendeeDiv); - createButton(container, attendeeDiv); + updateButton(container, attendeeDiv); }); container.on("disconnected", () => { updateAttendees(presence, attendeeDiv); - createButton(container, attendeeDiv); + updateButton(container, attendeeDiv); }); return attendeeDiv; } -function createButton(container: IFluidContainer, attendeeDiv: HTMLDivElement): void { +function updateButton(container: IFluidContainer, attendeeDiv: HTMLDivElement): void { const toggleButton = document.createElement("button"); - const connected = container.connectionState === 2; - toggleButton.innerHTML = `${connected ? "Disconnect" : "Connect"}`; + toggleButton.style.marginLeft = "10px"; + const connected = container.connectionState === 2; + toggleButton.addEventListener("click", () => { if (connected) { container.disconnect(); @@ -237,6 +241,7 @@ function createButton(container: IFluidContainer, attendeeDiv: HTMLDivElement): container.connect(); } }); + toggleButton.innerHTML = `${connected ? "Disconnect" : "Connect"}`; attendeeDiv.append(toggleButton); } @@ -268,11 +273,11 @@ export function makeAppView( const audienceView = makeAudienceView(audience); - const attendeeView = makeAttendeeView(container, presenceConfig?.presence); - const presenceView = makePresenceView(presenceConfig, audience); + const attendeeView = makeAttendeeView(container, presenceConfig?.presence); + const wrapperDiv = document.createElement("div"); - wrapperDiv.append(diceView, audienceView, attendeeView, presenceView); + wrapperDiv.append(diceView, audienceView, presenceView, attendeeView); return wrapperDiv; } diff --git a/packages/framework/presence/src/datastorePresenceManagerFactory.ts b/packages/framework/presence/src/datastorePresenceManagerFactory.ts index df2731dfbbeb..d310cf4ea1c7 100644 --- a/packages/framework/presence/src/datastorePresenceManagerFactory.ts +++ b/packages/framework/presence/src/datastorePresenceManagerFactory.ts @@ -46,11 +46,6 @@ class PresenceManagerDataObject extends LoadableFluidObject { this.runtime.getAudience().on("removeMember", (clientId: string) => { manager.removeClientConnectionId(clientId); }); - this.runtime.on("disconnected", () => { - if (this.runtime.clientId !== undefined) { - manager.removeClientConnectionId(this.runtime.clientId); - } - }); this._presenceManager = manager; } return this._presenceManager; diff --git a/packages/framework/presence/src/presenceManager.ts b/packages/framework/presence/src/presenceManager.ts index 5b8b1d61ee29..6b3b0c260229 100644 --- a/packages/framework/presence/src/presenceManager.ts +++ b/packages/framework/presence/src/presenceManager.ts @@ -84,6 +84,7 @@ class PresenceManager ); runtime.on("connected", this.onConnect.bind(this)); + runtime.on("disconnected", () => { if (runtime.clientId !== undefined) { this.removeClientConnectionId(runtime.clientId); diff --git a/packages/framework/presence/src/systemWorkspace.ts b/packages/framework/presence/src/systemWorkspace.ts index 32d63aea9d92..0687d7b321ba 100644 --- a/packages/framework/presence/src/systemWorkspace.ts +++ b/packages/framework/presence/src/systemWorkspace.ts @@ -220,6 +220,8 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { // Update the order and current connection id. attendee.order = order; attendee.connectionId = connectionId; + attendee.getStatus = () => SessionClientStatus.Connected; + isNew = true; } // Always update entry for the connection id. (Okay if already set.) this.attendees.set(clientConnectionId, attendee); From f9fa087d0434c13ee53afad6b26bbc8a59bad65d Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Wed, 30 Oct 2024 13:58:28 -0700 Subject: [PATCH 5/5] test changes --- .../presence/src/test/mockEphemeralRuntime.ts | 2 + .../presence/src/test/presenceManager.spec.ts | 52 ------------------- 2 files changed, 2 insertions(+), 52 deletions(-) diff --git a/packages/framework/presence/src/test/mockEphemeralRuntime.ts b/packages/framework/presence/src/test/mockEphemeralRuntime.ts index 7cc04445dba3..5a31ad2e9292 100644 --- a/packages/framework/presence/src/test/mockEphemeralRuntime.ts +++ b/packages/framework/presence/src/test/mockEphemeralRuntime.ts @@ -51,8 +51,10 @@ export class MockEphemeralRuntime implements IEphemeralRuntime { 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; diff --git a/packages/framework/presence/src/test/presenceManager.spec.ts b/packages/framework/presence/src/test/presenceManager.spec.ts index ccf5821761cf..c89838cb86d4 100644 --- a/packages/framework/presence/src/test/presenceManager.spec.ts +++ b/packages/framework/presence/src/test/presenceManager.spec.ts @@ -217,58 +217,6 @@ describe("Presence", () => { // 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", () => { - // Setup - assert(newAttendee !== undefined, "No attendee was set in beforeEach"); - - const updatedClientConnectionId = "client5"; - clock.tick(20); - const rejoinedAttendeeSignal = generateBasicClientJoin(clock.now - 20, { - averageLatency: 20, - clientSessionId: newAttendeeSessionId, // Same session id - clientConnectionId: updatedClientConnectionId, // Different connection id - connectionOrder: 1, - updateProviders: ["client2"], - }); - rejoinedAttendeeSignal.content.data["system:presence"].clientToSessionId[ - initialAttendeeConnectionId - ] = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - initialAttendeeSignal.content.data["system:presence"].clientToSessionId[ - initialAttendeeConnectionId - ]!; - - // Act - simulate new join message from same client (without disconnect) - presence.processSignal("", rejoinedAttendeeSignal, false); - - // Verify - // Session id is unchanged - assert.equal( - newAttendee.sessionId, - newAttendeeSessionId, - "Attendee has wrong session id", - ); - // Current connection id is updated - assert( - newAttendee.connectionId() === 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, - "getAttendee returned wrong attendee for updated connection id", - ); - // Attendee is available via old connection id - const attendeeViaOriginalId = presence.getAttendee(initialAttendeeConnectionId); - assert.equal( - attendeeViaOriginalId, - newAttendee, - "getAttendee returned wrong attendee for original connection id", - ); - }); }); }); });