From 86c78931d9ebdbc726cd517f9b9dad0a61f94b54 Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Fri, 11 Apr 2025 16:21:14 +0200 Subject: [PATCH 1/6] refactor(client-presence): Basic APIs renames (#24295) --- .changeset/sour-mirrors-wave.md | 45 ++++++ docs/docs/build/presence.md | 20 +-- examples/apps/ai-collab/src/app/page.tsx | 4 +- examples/apps/ai-collab/src/app/presence.ts | 32 ++-- .../apps/presence-tracker/src/FocusTracker.ts | 24 +-- .../apps/presence-tracker/src/MouseTracker.ts | 22 +-- examples/apps/presence-tracker/src/app.ts | 12 +- .../apps/presence-tracker/src/reactions.ts | 6 +- examples/apps/presence-tracker/src/view.ts | 4 +- .../external-controller/src/app.ts | 6 +- .../external-controller/src/presence.ts | 12 +- .../external-controller/src/view.ts | 16 +- packages/framework/presence/README.md | 18 +-- .../presence/api-report/presence.alpha.api.md | 142 +++++++++--------- packages/framework/presence/src/baseTypes.ts | 6 +- .../src/datastorePresenceManagerFactory.ts | 18 +-- .../presence/src/experimentalAccess.ts | 12 +- packages/framework/presence/src/index.ts | 26 ++-- .../framework/presence/src/internalTypes.ts | 6 +- .../presence/src/latestMapValueManager.ts | 58 +++---- .../presence/src/latestValueManager.ts | 36 ++--- .../presence/src/latestValueTypes.ts | 6 +- .../presence/src/notificationsManager.ts | 32 ++-- packages/framework/presence/src/presence.ts | 95 ++++++------ .../presence/src/presenceDatastoreManager.ts | 53 +++---- .../framework/presence/src/presenceManager.ts | 49 +++--- .../framework/presence/src/presenceStates.ts | 78 +++++----- .../framework/presence/src/stateDatastore.ts | 10 +- .../framework/presence/src/systemWorkspace.ts | 83 +++++----- .../presence/src/test/batching.spec.ts | 88 +++++------ .../src/test/broadcastControlsTests.ts | 4 +- .../presence/src/test/eventing.spec.ts | 62 ++++---- .../src/test/latestMapValueManager.spec.ts | 30 ++-- .../src/test/latestValueManager.spec.ts | 24 +-- .../src/test/notificationsManager.spec.ts | 64 ++++---- .../src/test/presenceDatastoreManager.spec.ts | 18 +-- .../presence/src/test/presenceManager.spec.ts | 60 ++++---- .../presence/src/test/presenceStates.spec.ts | 6 +- .../framework/presence/src/test/testUtils.ts | 16 +- packages/framework/presence/src/types.ts | 60 ++++---- .../src/assertionShortCodesMap.ts | 2 +- .../src/test/multiprocess/childClient.ts | 24 +-- .../src/test/multiprocess/messageTypes.ts | 10 +- .../test/multiprocess/presenceTest.spec.ts | 7 +- 44 files changed, 710 insertions(+), 696 deletions(-) create mode 100644 .changeset/sour-mirrors-wave.md diff --git a/.changeset/sour-mirrors-wave.md b/.changeset/sour-mirrors-wave.md new file mode 100644 index 000000000000..e9f54649af2d --- /dev/null +++ b/.changeset/sour-mirrors-wave.md @@ -0,0 +1,45 @@ +--- +"@fluidframework/presence": minor +--- +--- +"section": other +--- + +Presence API renames + +The following API changes have been made to improve clarity and consistency: + +| Original | New | +|----------|-----| +| `PresenceNotifications` | `NotificationsWorkspace` | +| `PresenceNotificationsSchema` | `NotificationsWorkspaceSchema` | +| `PresenceStates` | `StatesWorkspace` | +| `PresenceStatesEntries` | `StatesWorkspaceEntries` | +| `PresenceStatesSchema` | `StatesWorkspaceSchema` | +| `PresenceWorkspaceAddress` | `WorkspaceAddress` | +| `PresenceWorkspaceEntry` | `StatesWorkspaceEntry` | +| `ClientSessionId` | `AttendeeId` | +| `IPresence` | `Presence` | +| `ISessionClient` | `Attendee` | +| `SessionClientStatus` | `AttendeeStatus` | +| `acquirePresence` | `getPresence` | +| `acquirePresenceViaDataObject` | `getPresenceViaDataObject` | + +```json +{ + "PresenceNotifications": "NotificationsWorkspace", + "PresenceNotificationsSchema": "NotificationsWorkspaceSchema", + "PresenceStates": "StatesWorkspace", + "PresenceStatesEntries": "StatesWorkspaceEntries", + "PresenceStatesSchema": "StatesWorkspaceSchema", + "PresenceWorkspaceAddress": "WorkspaceAddress", + "PresenceWorkspaceEntry": "StatesWorkspaceEntry", + "ClientSessionId": "AttendeeId", + "IPresence": "Presence", + "ISessionClient": "Attendee", + "SessionClientStatus": "AttendeeStatus", + "acquirePresence": "getPresence", + "acquirePresenceViaDataObject": "getPresenceViaDataObject" +} +``` +The JSON table above can be used to automate most of these replacements in your codebase. You can implement a simple script that reads this JSON and performs the necessary replacements in your files. diff --git a/docs/docs/build/presence.md b/docs/docs/build/presence.md index 1fbb7100dbcb..4d118afc94fc 100644 --- a/docs/docs/build/presence.md +++ b/docs/docs/build/presence.md @@ -19,13 +19,13 @@ The key scenarios that the new Presence APIs are suitable for includes: ## Concepts -A session is a period of time when one or more clients are connected to a Fluid service. Session data and messages may be exchanged among clients, but will disappear once the no clients remain. (More specifically once no clients remain that have acquired the session `IPresence` interface.) Once fully implemented, no client will require container write permissions to use Presence features. +A session is a period of time when one or more clients are connected to a Fluid service. Session data and messages may be exchanged among clients, but will disappear once the no clients remain. (More specifically once no clients remain that have acquired the session `Presence` interface.) Once fully implemented, no client will require container write permissions to use Presence features. ### Attendees -For the lifetime of a session, each client connecting will be established as a unique and stable `ISessionClient`. The representation is stable because it will remain the same `ISessionClient` instance independent of connection drops and reconnections. +For the lifetime of a session, each client connecting will be established as a unique and stable `Attendee`. The representation is stable because it will remain the same `Attendee` instance independent of connection drops and reconnections. -Client Ids maintained by `ISessionClient` may be used to associate `ISessionClient` with quorum, audience, and service audience members. +Client Ids maintained by `Attendee` may be used to associate `Attendee` with quorum, audience, and service audience members. ### Workspaces @@ -35,25 +35,25 @@ There are two types of workspaces: States and Notifications. #### States Workspace -A states workspace, `PresenceStates`, allows sharing of simple data across attendees where each attendee maintains their own data values that others may read, but not change. This is distinct from a Fluid DDS where data values might be manipulated by multiple clients and one ultimate value is derived. Shared, independent values are maintained by value managers that specialize in incrementality and history of values. +A `StatesWorkspace`, allows sharing of simple data across attendees where each attendee maintains their own data values that others may read, but not change. This is distinct from a Fluid DDS where data values might be manipulated by multiple clients and one ultimate value is derived. Shared, independent values are maintained by value managers that specialize in incrementality and history of values. #### Notifications Workspace -A notifications workspace, `PresenceNotifications`, is similar to states workspace, but is dedicated to notification use-cases via `NotificationsManager`. +A `NotificationsWorkspace`, is similar to states workspace, but is dedicated to notification use-cases via `NotificationsManager`. ### Value Managers #### LatestValueManager -Latest value manager retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `PresenceStates` workspace. +Latest value manager retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `StatesWorkspace`. #### LatestMapValueManager -Latest map value manager retains the most recent atomic value each attendee has shared under arbitrary keys. Values associated with a key may be nullified (appears as deleted). Use `LatestMap` to add one to `PresenceStates` workspace. +Latest map value manager retains the most recent atomic value each attendee has shared under arbitrary keys. Values associated with a key may be nullified (appears as deleted). Use `LatestMap` to add one to `StatesWorkspace`. #### NotificationsManager -Notifications value managers are special case where no data is retained during a session and all interactions appear as events that are sent and received. Notifications value managers may be mixed into a `PresenceStates` workspace for convenience. They are the only type of value managers permitted in a `PresenceNotifications` workspace. Use `Notifications` to add one to `PresenceNotifications` or `PresenceStates` workspace. +Notifications value managers are special case where no data is retained during a session and all interactions appear as events that are sent and received. Notifications value managers may be mixed into a `StatesWorkspace` for convenience. They are the only type of value managers permitted in a `NotificationsWorkspace`. Use `Notifications` to add one to `NotificationsWorkspace` or `StatesWorkspace`. ## Onboarding @@ -61,7 +61,7 @@ While this package is developing as experimental and other Fluid Framework inter ```typescript import { - acquirePresenceViaDataObject, + getPresenceViaDataObject, ExperimentalPresenceManager, } from "@fluidframework/presence/alpha"; @@ -71,7 +71,7 @@ const containerSchema = { }, } satisfies ContainerSchema; -const presence = await acquirePresenceViaDataObject(container.initialObjects.presence); +const presence = await getPresenceViaDataObject(container.initialObjects.presence); ``` ## Limitations diff --git a/examples/apps/ai-collab/src/app/page.tsx b/examples/apps/ai-collab/src/app/page.tsx index 60cd11d80e60..7fe2e53e98fb 100644 --- a/examples/apps/ai-collab/src/app/page.tsx +++ b/examples/apps/ai-collab/src/app/page.tsx @@ -5,7 +5,7 @@ "use client"; -import { acquirePresenceViaDataObject } from "@fluidframework/presence/alpha"; +import { getPresenceViaDataObject } from "@fluidframework/presence/alpha"; import { Box, Button, @@ -63,7 +63,7 @@ export default function TasksListPage(): JSX.Element { const _treeView = fluidContainer.initialObjects.appState.viewWith(TREE_CONFIGURATION); setTreeView(_treeView); - const presence = acquirePresenceViaDataObject(fluidContainer.initialObjects.presence); + const presence = getPresenceViaDataObject(fluidContainer.initialObjects.presence); setPresenceManagerContext(new PresenceManager(presence)); return { sharedTree: _treeView }; }, diff --git a/examples/apps/ai-collab/src/app/presence.ts b/examples/apps/ai-collab/src/app/presence.ts index ddc5c3889911..9c2e768e2482 100644 --- a/examples/apps/ai-collab/src/app/presence.ts +++ b/examples/apps/ai-collab/src/app/presence.ts @@ -4,11 +4,11 @@ */ import { - IPresence, + Presence, Latest, - type ISessionClient, - type PresenceStates, - type PresenceStatesSchema, + type Attendee, + type StatesWorkspace, + type StatesWorkspaceSchema, } from "@fluidframework/presence/alpha"; import { getProfilePhoto } from "@/infra/authHelper"; @@ -19,12 +19,12 @@ export interface User { const statesSchema = { onlineUsers: Latest({ photo: "" } satisfies User), -} satisfies PresenceStatesSchema; +} satisfies StatesWorkspaceSchema; -export type UserPresence = PresenceStates; +export type UserPresence = StatesWorkspace; // Takes a presence object and returns the user presence object that contains the shared object states -export function buildUserPresence(presence: IPresence): UserPresence { +export function buildUserPresence(presence: Presence): UserPresence { const states = presence.getStates(`name:user-avatar-states`, statesSchema); return states; } @@ -33,11 +33,11 @@ export class PresenceManager { // A PresenceState object to manage the presence of users within the app private readonly usersState: UserPresence; // A map of SessionClient to UserInfo, where users can share their info with other users - private readonly userInfoMap: Map = new Map(); + private readonly userInfoMap: Map = new Map(); // A callback method to get updates when remote UserInfo changes - private userInfoCallback: (userInfoMap: Map) => void = () => {}; + private userInfoCallback: (userInfoMap: Map) => void = () => {}; - constructor(private readonly presence: IPresence) { + constructor(private readonly presence: Presence) { // Address for the presence state, this is used to organize the presence states and avoid conflicts const appSelectionWorkspaceAddress = "aiCollab:workspace"; @@ -50,7 +50,7 @@ export class PresenceManager { // Listen for updates to the userInfo property in the presence state this.usersState.props.onlineUsers.events.on("updated", (update) => { // The remote client that updated the userInfo property - const remoteSessionClient = update.client; + const remoteSessionClient = update.attendee; // The new value of the userInfo property const remoteUserInfo = update.value; @@ -82,17 +82,17 @@ export class PresenceManager { } // Returns the presence object - getPresence(): IPresence { + getPresence(): Presence { return this.presence; } // Allows the app to listen for updates to the userInfoMap - setUserInfoUpdateListener(callback: (userInfoMap: Map) => void): void { + setUserInfoUpdateListener(callback: (userInfoMap: Map) => void): void { this.userInfoCallback = callback; } // Returns the UserInfo of given session clients - getUserInfo(sessionList: ISessionClient[]): User[] { + getUserInfo(sessionList: Attendee[]): User[] { const userInfoList: User[] = []; for (const sessionClient of sessionList) { @@ -100,7 +100,7 @@ export class PresenceManager { try { const userInfo = this.usersState.props.onlineUsers.clientValue(sessionClient).value; // If the user is local user, then add it to the beginning of the list - if (sessionClient.sessionId === this.presence.getMyself().sessionId) { + if (sessionClient.attendeeId === this.presence.getMyself().attendeeId) { userInfoList.push(userInfo); } else { // If the user is remote user, then add it to the end of the list @@ -108,7 +108,7 @@ export class PresenceManager { } } catch (error) { console.error( - `Error: ${error} when getting user info for session client: ${sessionClient.sessionId}`, + `Error: ${error} when getting user info for session client: ${sessionClient.attendeeId}`, ); } } diff --git a/examples/apps/presence-tracker/src/FocusTracker.ts b/examples/apps/presence-tracker/src/FocusTracker.ts index 55de55d7c231..7f55768fc7a7 100644 --- a/examples/apps/presence-tracker/src/FocusTracker.ts +++ b/examples/apps/presence-tracker/src/FocusTracker.ts @@ -6,12 +6,12 @@ import { TypedEventEmitter } from "@fluid-internal/client-utils"; import { IEvent } from "@fluidframework/core-interfaces"; import type { - IPresence, - ISessionClient, + Attendee, LatestValueManager, - PresenceStates, + Presence, + StatesWorkspace, } from "@fluidframework/presence/alpha"; -import { Latest, SessionClientStatus } from "@fluidframework/presence/alpha"; +import { Latest, AttendeeStatus } from "@fluidframework/presence/alpha"; /** * IFocusState is the data that individual session clients share via presence. @@ -43,13 +43,13 @@ export class FocusTracker extends TypedEventEmitter { private readonly focus: LatestValueManager; constructor( - private readonly presence: IPresence, + private readonly presence: Presence, /** * A states workspace that the FocusTracker will use to share focus states with other session clients. */ // eslint-disable-next-line @typescript-eslint/ban-types -- empty object is the correct typing - readonly statesWorkspace: PresenceStates<{}>, + readonly statesWorkspace: StatesWorkspace<{}>, ) { super(); @@ -64,7 +64,7 @@ export class FocusTracker extends TypedEventEmitter { this.focus = statesWorkspace.props.focus; // When the focus value manager is updated, the FocusTracker should emit the focusChanged event. - this.focus.events.on("updated", ({ client, value }) => { + this.focus.events.on("updated", ({ attendee, value }) => { this.emit("focusChanged", this.focus.local); }); @@ -99,18 +99,18 @@ export class FocusTracker extends TypedEventEmitter { /** * A map of session clients to focus status. */ - public getFocusPresences(): Map { - const statuses: Map = new Map(); + public getFocusPresences(): Map { + const statuses: Map = new Map(); // Include the local client in the map because this is used to render a // dashboard of all connected clients. const currentClient = this.presence.getMyself(); statuses.set(currentClient, this.focus.local.hasFocus); - for (const { client, value } of this.focus.clientValues()) { - if (client.getConnectionStatus() === SessionClientStatus.Connected) { + for (const { attendee, value } of this.focus.clientValues()) { + if (attendee.getConnectionStatus() === AttendeeStatus.Connected) { const { hasFocus } = value; - statuses.set(client, hasFocus); + statuses.set(attendee, hasFocus); } } diff --git a/examples/apps/presence-tracker/src/MouseTracker.ts b/examples/apps/presence-tracker/src/MouseTracker.ts index 3106329f15d9..f11e0339a22a 100644 --- a/examples/apps/presence-tracker/src/MouseTracker.ts +++ b/examples/apps/presence-tracker/src/MouseTracker.ts @@ -6,12 +6,12 @@ import { TypedEventEmitter } from "@fluid-internal/client-utils"; import type { IEvent } from "@fluidframework/core-interfaces"; import type { - IPresence, - ISessionClient, + Presence, + Attendee, LatestValueManager, - PresenceStates, + StatesWorkspace, } from "@fluidframework/presence/alpha"; -import { Latest, SessionClientStatus } from "@fluidframework/presence/alpha"; +import { Latest, AttendeeStatus } from "@fluidframework/presence/alpha"; /** * IMousePosition is the data that individual session clients share via presence. @@ -44,13 +44,13 @@ export class MouseTracker extends TypedEventEmitter { private readonly cursor: LatestValueManager; constructor( - private readonly presence: IPresence, + private readonly presence: Presence, /** * A states workspace that the MouseTracker will use to share mouse positions with other session clients. */ // eslint-disable-next-line @typescript-eslint/ban-types -- empty object is the correct typing - readonly statesWorkspace: PresenceStates<{}>, + readonly statesWorkspace: StatesWorkspace<{}>, ) { super(); @@ -85,12 +85,12 @@ export class MouseTracker extends TypedEventEmitter { /** * A map of session clients to mouse positions. */ - public getMousePresences(): Map { - const statuses: Map = new Map(); + public getMousePresences(): Map { + const statuses: Map = new Map(); - for (const { client, value } of this.cursor.clientValues()) { - if (client.getConnectionStatus() === SessionClientStatus.Connected) { - statuses.set(client, value); + for (const { attendee, value } of this.cursor.clientValues()) { + if (attendee.getConnectionStatus() === AttendeeStatus.Connected) { + statuses.set(attendee, value); } } return statuses; diff --git a/examples/apps/presence-tracker/src/app.ts b/examples/apps/presence-tracker/src/app.ts index 042bb09a1251..5f838b612a7e 100644 --- a/examples/apps/presence-tracker/src/app.ts +++ b/examples/apps/presence-tracker/src/app.ts @@ -4,7 +4,7 @@ */ import { - acquirePresenceViaDataObject, + getPresenceViaDataObject, ExperimentalPresenceManager, } from "@fluidframework/presence/alpha"; import { TinyliciousClient } from "@fluidframework/tinylicious-client"; @@ -57,7 +57,7 @@ async function start() { } // Retrieve a reference to the presence APIs via the data object. - const presence = acquirePresenceViaDataObject(container.initialObjects.presence); + const presence = getPresenceViaDataObject(container.initialObjects.presence); // Get the states workspace for the tracker data. This workspace will be created if it doesn't exist. // We create it with no states; we will pass the workspace to the Mouse and Focus trackers, and they will create value @@ -86,7 +86,7 @@ async function start() { // Setting "fluid*" and these helpers are just for our test automation const buildAttendeeMap = () => { return [...presence.getAttendees()].reduce((map, a) => { - map[a.sessionId] = a.getConnectionStatus(); + map[a.attendeeId] = a.getConnectionStatus(); return map; }, {}); }; @@ -109,18 +109,18 @@ async function start() { window["fluidSessionAttendees"] = buildAttendeeMap(); window["fluidSessionAttendeeCount"] = presence.getAttendees().size; presence.events.on("attendeeJoined", (attendee) => { - console.log(`Attendee joined: ${attendee.sessionId}`); + console.log(`Attendee joined: ${attendee.attendeeId}`); window["fluidSessionAttendees"] = buildAttendeeMap(); window["fluidSessionAttendeeCount"] = presence.getAttendees().size; window["fluidAttendeeJoinedCalled"] = true; }); presence.events.on("attendeeDisconnected", (attendee) => { - console.log(`Attendee left: ${attendee.sessionId}`); + console.log(`Attendee left: ${attendee.attendeeId}`); window["fluidSessionAttendees"] = buildAttendeeMap(); window["fluidSessionAttendeeCount"] = presence.getAttendees().size; window["fluidAttendeeDisconnectedCalled"] = true; }); - window["fluidSessionId"] = presence.getMyself().sessionId; + window["fluidSessionId"] = presence.getMyself().attendeeId; // Always set last as it is used as fence for load completion window["fluidContainerId"] = id; /* eslint-enable @typescript-eslint/dot-notation */ diff --git a/examples/apps/presence-tracker/src/reactions.ts b/examples/apps/presence-tracker/src/reactions.ts index fac05ac4e060..f3c7e8717bc1 100644 --- a/examples/apps/presence-tracker/src/reactions.ts +++ b/examples/apps/presence-tracker/src/reactions.ts @@ -4,7 +4,7 @@ */ import { Notifications } from "@fluidframework/presence/alpha"; -import type { IPresence, ISessionClient } from "@fluidframework/presence/alpha"; +import type { Attendee, Presence } from "@fluidframework/presence/alpha"; import type { IMousePosition, MouseTracker } from "./MouseTracker.js"; @@ -13,7 +13,7 @@ import type { IMousePosition, MouseTracker } from "./MouseTracker.js"; * relevant event handlers. Reaction elements are added to the DOM in response to incoming notifications. These DOM * elements are automatically removed after a timeout. */ -export function initializeReactions(presence: IPresence, mouseTracker: MouseTracker) { +export function initializeReactions(presence: Presence, mouseTracker: MouseTracker) { // Create a notifications workspace to send reactions-related notifications. This workspace will be created if it // doesn't exist. We also create a Notifications value manager. You can also // add value managers to the workspace later. @@ -59,7 +59,7 @@ export function initializeReactions(presence: IPresence, mouseTracker: MouseTrac /** * Renders reactions to the window using absolute positioning. */ -function onReaction(client: ISessionClient, position: IMousePosition, value: string): void { +function onReaction(client: Attendee, position: IMousePosition, value: string): void { const reactionDiv = document.createElement("div"); reactionDiv.className = "reaction"; reactionDiv.style.position = "absolute"; diff --git a/examples/apps/presence-tracker/src/view.ts b/examples/apps/presence-tracker/src/view.ts index 7b7ef9a83ecd..0b9094f5732f 100644 --- a/examples/apps/presence-tracker/src/view.ts +++ b/examples/apps/presence-tracker/src/view.ts @@ -35,7 +35,7 @@ function getFocusPresencesString( const focusString: string[] = []; focusTracker.getFocusPresences().forEach((hasFocus, sessionClient) => { - const prefix = `User session ${sessionClient.sessionId}:`; + const prefix = `User session ${sessionClient.attendeeId}:`; if (hasFocus) { focusString.push(`${prefix} has focus`); } else { @@ -56,7 +56,7 @@ export function renderMousePresence( for (const [sessionClient, mousePosition] of mouseTracker.getMousePresences()) { if (focusTracker.getFocusPresences().get(sessionClient) === true) { const posDiv = document.createElement("div"); - posDiv.textContent = `/${sessionClient.sessionId}`; + posDiv.textContent = `/${sessionClient.attendeeId}`; posDiv.style.position = "absolute"; posDiv.style.left = `${mousePosition.x}px`; posDiv.style.top = `${mousePosition.y - 6}px`; 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 d3dd129d6b88..c3347449fe7d 100644 --- a/examples/service-clients/azure-client/external-controller/src/app.ts +++ b/examples/service-clients/azure-client/external-controller/src/app.ts @@ -12,7 +12,7 @@ import { import { createDevtoolsLogger, initializeDevtools } from "@fluidframework/devtools/beta"; import { ISharedMap, IValueChanged, SharedMap } from "@fluidframework/map/legacy"; import { - acquirePresenceViaDataObject, + getPresenceViaDataObject, ExperimentalPresenceManager, } from "@fluidframework/presence/alpha"; import { createChildLogger } from "@fluidframework/telemetry-utils/legacy"; @@ -182,7 +182,7 @@ async function start(): Promise { // Biome insist on no semicolon - https://dev.azure.com/fluidframework/internal/_workitems/edit/9083 const lastRoll: { die1?: DieValue; die2?: DieValue } = {}; - const presence = acquirePresenceViaDataObject(container.initialObjects.presence); + const presence = getPresenceViaDataObject(container.initialObjects.presence); const states = buildDicePresence(presence).props; // Initialize Devtools @@ -218,7 +218,7 @@ async function start(): Promise { // Its updates are only logged to the console. states.lastDiceRolls.events.on("itemUpdated", (update) => { console.log( - `Client ${update.client.sessionId.slice(0, 8)}'s ${update.key} rolled to ${update.value.value}`, + `Client ${update.attendee.attendeeId.slice(0, 8)}'s ${update.key} rolled to ${update.value.value}`, ); }); diff --git a/examples/service-clients/azure-client/external-controller/src/presence.ts b/examples/service-clients/azure-client/external-controller/src/presence.ts index 4662316d78de..7b337fcbd0fd 100644 --- a/examples/service-clients/azure-client/external-controller/src/presence.ts +++ b/examples/service-clients/azure-client/external-controller/src/presence.ts @@ -4,11 +4,11 @@ */ import { - IPresence, + Presence, Latest, LatestMap, - type PresenceStates, - type PresenceStatesSchema, + type StatesWorkspace, + type StatesWorkspaceSchema, } from "@fluidframework/presence/alpha"; import type { DieValue } from "./controller.js"; @@ -41,11 +41,11 @@ const statesSchema = { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions lastRoll: Latest({} as DiceValues), lastDiceRolls: LatestMap<{ value: DieValue }, `die${number}`>(), -} satisfies PresenceStatesSchema; +} satisfies StatesWorkspaceSchema; -export type DicePresence = PresenceStates; +export type DicePresence = StatesWorkspace; -export function buildDicePresence(presence: IPresence): DicePresence { +export function buildDicePresence(presence: Presence): DicePresence { const states = presence.getStates("name:app-client-states", statesSchema); return states; } 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 d4bc50037e65..2eb568d883f5 100644 --- a/examples/service-clients/azure-client/external-controller/src/view.ts +++ b/examples/service-clients/azure-client/external-controller/src/view.ts @@ -4,7 +4,7 @@ */ import { AzureMember, IAzureAudience } from "@fluidframework/azure-client"; -import type { IPresence, LatestValueManager } from "@fluidframework/presence/alpha"; +import type { Presence, LatestValueManager } from "@fluidframework/presence/alpha"; import { ICustomUserDetails } from "./app.js"; import { IDiceRollerController } from "./controller.js"; @@ -112,7 +112,7 @@ export function makeDiceValuesView( ): void { const children = makeDiceHeaderElement(); for (const clientValue of lastRoll.clientValues()) { - children.push(...makeDiceValueElement(clientValue.client.sessionId, clientValue.value)); + children.push(...makeDiceValueElement(clientValue.attendee.attendeeId, clientValue.value)); } target.replaceChildren(...children); } @@ -125,7 +125,7 @@ function addLogEntry(logDiv: HTMLDivElement, entry: string): void { function makePresenceView( // Biome insist on no semicolon - https://dev.azure.com/fluidframework/internal/_workitems/edit/9083 - presenceConfig?: { presence: IPresence; lastRoll: LatestValueManager }, + presenceConfig?: { presence: Presence; lastRoll: LatestValueManager }, audience?: IAzureAudience, ): HTMLDivElement { const presenceDiv = document.createElement("div"); @@ -167,7 +167,7 @@ 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.attendeeId} joined`; addLogEntry(logContentDiv, update); }); @@ -176,7 +176,7 @@ function makePresenceView( 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`; + const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} ⛓️‍💥 with id ${attendee.attendeeId} left`; addLogEntry(logContentDiv, update); } }); @@ -184,8 +184,8 @@ function makePresenceView( logDiv.append(logHeaderDiv, logContentDiv); presenceConfig.lastRoll.events.on("updated", (update) => { - const connected = update.client.getConnectionStatus() === "Connected" ? "🔗" : "⛓️‍💥"; - const updateText = `updated ${update.client.sessionId.slice(0, 8)}'s ${connected} last rolls to ${JSON.stringify(update.value)}`; + const connected = update.attendee.getConnectionStatus() === "Connected" ? "🔗" : "⛓️‍💥"; + const updateText = `updated ${update.attendee.attendeeId.slice(0, 8)}'s ${connected} last rolls to ${JSON.stringify(update.value)}`; addLogEntry(logContentDiv, updateText); makeDiceValuesView(statesContentDiv, presenceConfig.lastRoll); @@ -198,7 +198,7 @@ function makePresenceView( export function makeAppView( diceRollerControllers: IDiceRollerController[], // Biome insist on no semicolon - https://dev.azure.com/fluidframework/internal/_workitems/edit/9083 - presenceConfig?: { presence: IPresence; lastRoll: LatestValueManager }, + presenceConfig?: { presence: Presence; lastRoll: LatestValueManager }, audience?: IAzureAudience, ): HTMLDivElement { const diceRollerViews = diceRollerControllers.map((controller) => diff --git a/packages/framework/presence/README.md b/packages/framework/presence/README.md index 9bcf163883ae..926e887cfabb 100644 --- a/packages/framework/presence/README.md +++ b/packages/framework/presence/README.md @@ -44,9 +44,9 @@ API documentation for **@fluidframework/presence** is available at { + readonly attendeeId: SpecificAttendeeId; + getConnectionId(): ClientConnectionId; + getConnectionStatus(): AttendeeStatus; +} + +// @alpha +export type AttendeeId = SessionId & { + readonly AttendeeId: "AttendeeId"; +}; + // @alpha -export function acquirePresence(fluidContainer: IFluidContainer): IPresence; +export const AttendeeStatus: { + readonly Connected: "Connected"; + readonly Disconnected: "Disconnected"; +}; // @alpha -export function acquirePresenceViaDataObject(fluidLoadable: ExperimentalPresenceDO): IPresence; +export type AttendeeStatus = (typeof AttendeeStatus)[keyof typeof AttendeeStatus]; // @alpha @sealed export interface BroadcastControls { @@ -23,11 +38,6 @@ export interface BroadcastControlSettings { // @alpha export type ClientConnectionId = string; -// @alpha -export type ClientSessionId = SessionId & { - readonly ClientSessionId: "ClientSessionId"; -}; - // @alpha @sealed export class ExperimentalPresenceDO { } @@ -35,6 +45,12 @@ export class ExperimentalPresenceDO { // @alpha export const ExperimentalPresenceManager: SharedObjectKind; +// @alpha +export function getPresence(fluidContainer: IFluidContainer): Presence; + +// @alpha +export function getPresenceViaDataObject(fluidLoadable: ExperimentalPresenceDO): Presence; + // @alpha export namespace InternalTypes { export type ManagerFactory, TManager> = { @@ -111,23 +127,6 @@ export namespace InternalUtilityTypes { }; } -// @alpha @sealed -export interface IPresence { - readonly events: Listenable; - getAttendee(clientId: ClientConnectionId | ClientSessionId): ISessionClient; - getAttendees(): ReadonlySet; - getMyself(): ISessionClient; - getNotifications(notificationsId: PresenceWorkspaceAddress, requestedContent: NotificationsSchema): PresenceNotifications; - getStates(workspaceAddress: PresenceWorkspaceAddress, requestedContent: StatesSchema, controls?: BroadcastControlSettings): PresenceStates; -} - -// @alpha @sealed -export interface ISessionClient { - getConnectionId(): ClientConnectionId; - getConnectionStatus(): SessionClientStatus; - readonly sessionId: SpecificSessionClientId; -} - // @alpha export function Latest(initialValue: JsonSerializable & JsonDeserialized & object, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, LatestValueManager>; @@ -139,7 +138,7 @@ export function LatestMap { // (undocumented) - client: ISessionClient; + attendee: Attendee; // (undocumented) key: K; // (undocumented) @@ -153,16 +152,16 @@ export interface LatestMapItemValueClientData exte } // @alpha @sealed -export interface LatestMapValueClientData { - client: ISessionClient; +export interface LatestMapValueClientData { + attendee: Attendee; // (undocumented) items: ReadonlyMap>; } // @alpha @sealed export interface LatestMapValueManager { - clients(): ISessionClient[]; - clientValue(client: ISessionClient): ReadonlyMap>; + clients(): Attendee[]; + clientValue(attendee: Attendee): ReadonlyMap>; clientValues(): IterableIterator>; readonly controls: BroadcastControls; readonly events: Listenable>; @@ -191,7 +190,7 @@ export interface LatestMapValueManagerEvents { // @alpha @sealed export interface LatestValueClientData extends LatestValueData { // (undocumented) - client: ISessionClient; + attendee: Attendee; } // @alpha @sealed @@ -204,8 +203,8 @@ export interface LatestValueData { // @alpha @sealed export interface LatestValueManager { - clients(): ISessionClient[]; - clientValue(client: ISessionClient): LatestValueData; + clients(): Attendee[]; + clientValue(attendee: Attendee): LatestValueData; clientValues(): IterableIterator>; readonly controls: BroadcastControls; readonly events: Listenable>; @@ -232,13 +231,13 @@ export interface LatestValueMetadata { // @alpha @sealed export interface NotificationEmitter> { broadcast>(notificationName: K, ...args: Parameters): void; - unicast>(notificationName: K, targetClient: ISessionClient, ...args: Parameters): void; + unicast>(notificationName: K, targetAttendee: Attendee, ...args: Parameters): void; } // @alpha @sealed export interface NotificationListenable> { - off>(notificationName: K, listener: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void): void; - on>(notificationName: K, listener: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void): Off; + off>(notificationName: K, listener: (sender: Attendee, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void): void; + on>(notificationName: K, listener: (sender: Attendee, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void): Off; } // @alpha @@ -254,44 +253,54 @@ export interface NotificationsManager void; + unattendedNotification: (name: string, sender: Attendee, ...content: unknown[]) => void; } // @alpha @sealed export type NotificationSubscriptions> = { - [K in string & keyof InternalUtilityTypes.NotificationListeners]: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void; + [K in string & keyof InternalUtilityTypes.NotificationListeners]: (sender: Attendee, ...args: InternalUtilityTypes.JsonDeserializedParameters) => void; }; -// @alpha @sealed (undocumented) -export interface PresenceEvents { - // @eventProperty - attendeeDisconnected: (attendee: ISessionClient) => void; - // @eventProperty - attendeeJoined: (attendee: ISessionClient) => void; - workspaceActivated: (workspaceAddress: PresenceWorkspaceAddress, type: "States" | "Notifications" | "Unknown") => void; -} - // @alpha @sealed -export interface PresenceNotifications { - add, TManager extends NotificationsManager>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is PresenceNotifications>>; - readonly props: PresenceStatesEntries; +export interface NotificationsWorkspace { + add, TManager extends NotificationsManager>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is NotificationsWorkspace>>; + readonly props: StatesWorkspaceEntries; } // @alpha -export interface PresenceNotificationsSchema { +export interface NotificationsWorkspaceSchema { // (undocumented) [key: string]: InternalTypes.ManagerFactory, NotificationsManager>; } // @alpha @sealed -export interface PresenceStates { - add, TManager extends TManagerConstraints>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is PresenceStates>, TManagerConstraints>; +export interface Presence { + readonly events: Listenable; + getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee; + getAttendees(): ReadonlySet; + getMyself(): Attendee; + getNotifications(notificationsId: WorkspaceAddress, requestedContent: NotificationsSchema): NotificationsWorkspace; + getStates(workspaceAddress: WorkspaceAddress, requestedContent: StatesSchema, controls?: BroadcastControlSettings): StatesWorkspace; +} + +// @alpha @sealed (undocumented) +export interface PresenceEvents { + // @eventProperty + attendeeDisconnected: (attendee: Attendee) => void; + // @eventProperty + attendeeJoined: (attendee: Attendee) => void; + workspaceActivated: (workspaceAddress: WorkspaceAddress, type: "States" | "Notifications" | "Unknown") => void; +} + +// @alpha @sealed +export interface StatesWorkspace { + add, TManager extends TManagerConstraints>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is StatesWorkspace>, TManagerConstraints>; readonly controls: BroadcastControls; - readonly props: PresenceStatesEntries; + readonly props: StatesWorkspaceEntries; } // @alpha @sealed -export type PresenceStatesEntries = { +export type StatesWorkspaceEntries = { /** * Registered `Value Manager`s */ @@ -299,25 +308,13 @@ export type PresenceStatesEntries = { }; // @alpha -export interface PresenceStatesSchema { - // (undocumented) - [key: string]: PresenceWorkspaceEntry>; -} - -// @alpha -export type PresenceWorkspaceAddress = `${string}:${string}`; - -// @alpha -export type PresenceWorkspaceEntry, TManager = unknown> = InternalTypes.ManagerFactory; - -// @alpha -export const SessionClientStatus: { - readonly Connected: "Connected"; - readonly Disconnected: "Disconnected"; -}; +export type StatesWorkspaceEntry, TManager = unknown> = InternalTypes.ManagerFactory; // @alpha -export type SessionClientStatus = (typeof SessionClientStatus)[keyof typeof SessionClientStatus]; +export interface StatesWorkspaceSchema { + // (undocumented) + [key: string]: StatesWorkspaceEntry>; +} // @alpha @sealed export interface ValueMap { @@ -334,4 +331,7 @@ export interface ValueMap { readonly size: number; } +// @alpha +export type WorkspaceAddress = `${string}:${string}`; + ``` diff --git a/packages/framework/presence/src/baseTypes.ts b/packages/framework/presence/src/baseTypes.ts index 6af16c338573..7e1972decbe3 100644 --- a/packages/framework/presence/src/baseTypes.ts +++ b/packages/framework/presence/src/baseTypes.ts @@ -9,9 +9,9 @@ * @remarks * Each client connection is given a unique identifier for the duration of the * connection. If a client disconnects and reconnects, it will be given a new - * identifier. Prefer use of {@link ISessionClient} as a way to identify clients - * in a session. {@link ISessionClient.getConnectionId} will provide the current - * connection identifier for a logical session client. + * identifier. Prefer use of {@link Attendee} as a way to identify clients + * in a session. {@link Attendee.getConnectionId} will provide the current + * connection identifier for a logical attendee. * * @privateRemarks * This represents what is commonly `clientId` in Fluid code. Ideally this is diff --git a/packages/framework/presence/src/datastorePresenceManagerFactory.ts b/packages/framework/presence/src/datastorePresenceManagerFactory.ts index 462146fe545a..8309199e3f56 100644 --- a/packages/framework/presence/src/datastorePresenceManagerFactory.ts +++ b/packages/framework/presence/src/datastorePresenceManagerFactory.ts @@ -13,7 +13,7 @@ import type { IInboundSignalMessage } from "@fluidframework/runtime-definitions/ import type { SharedObjectKind } from "@fluidframework/shared-object-base"; import { BasicDataStoreFactory, LoadableFluidObject } from "./datastoreSupport.js"; -import type { IPresence } from "./presence.js"; +import type { Presence } from "./presence.js"; import { createPresenceManager } from "./presenceManager.js"; import type { IExtensionMessage } from "@fluidframework/presence/internal/container-definitions/internal"; @@ -32,9 +32,9 @@ function assertSignalMessageIsValid( class PresenceManagerDataObject extends LoadableFluidObject { // Creation of presence manager is deferred until first acquisition to avoid // instantiations and stand-up by Summarizer that has no actual use. - private _presenceManager: IPresence | undefined; + private _presenceManager: Presence | undefined; - public presenceManager(): IPresence { + public presenceManager(): Presence { if (!this._presenceManager) { // TODO: investigate if ContainerExtensionStore (path-based address routing for // Signals) is readily detectable here and use that presence manager directly. @@ -50,7 +50,7 @@ class PresenceManagerDataObject extends LoadableFluidObject { } /** - * Factory class to create {@link IPresence} in own data store. + * Factory class to create {@link Presence} in own data store. */ class PresenceManagerFactory { public is(value: IFluidLoadable | ExperimentalPresenceDO): value is ExperimentalPresenceDO { @@ -67,7 +67,7 @@ class PresenceManagerFactory { * Brand for Experimental Presence Data Object. * * @remarks - * See {@link acquirePresenceViaDataObject} for example usage. + * See {@link getPresenceViaDataObject} for example usage. * * @sealed * @alpha @@ -88,7 +88,7 @@ export const ExperimentalPresenceManager = >; /** - * Acquire IPresence from a DataStore based Presence Manager + * Acquire Presence from a DataStore based Presence Manager * * @example * ```typescript @@ -100,16 +100,14 @@ export const ExperimentalPresenceManager = * ``` * then * ```typescript - * const presence = acquirePresenceViaDataObject( + * const presence = getPresenceViaDataObject( * container.initialObjects.experimentalPresence, * ); * ``` * * @alpha */ -export function acquirePresenceViaDataObject( - fluidLoadable: ExperimentalPresenceDO, -): IPresence { +export function getPresenceViaDataObject(fluidLoadable: ExperimentalPresenceDO): Presence { if (fluidLoadable instanceof PresenceManagerDataObject) { return fluidLoadable.presenceManager(); } diff --git a/packages/framework/presence/src/experimentalAccess.ts b/packages/framework/presence/src/experimentalAccess.ts index f99ceee0904d..e478dc50e8e0 100644 --- a/packages/framework/presence/src/experimentalAccess.ts +++ b/packages/framework/presence/src/experimentalAccess.ts @@ -10,7 +10,7 @@ import { isInternalFluidContainer } from "@fluidframework/fluid-static/internal" import type { IContainerRuntimeBase } from "@fluidframework/runtime-definitions/internal"; import type { IEphemeralRuntime } from "./internalTypes.js"; -import type { IPresence } from "./presence.js"; +import type { Presence } from "./presence.js"; import type { PresenceExtensionInterface } from "./presenceManager.js"; import { createPresenceManager } from "./presenceManager.js"; @@ -31,7 +31,7 @@ function isContainerExtensionStore( * Common Presence manager for a container */ class ContainerPresenceManager implements IContainerExtension { - public readonly interface: IPresence; + public readonly interface: Presence; public readonly extension = this; private readonly manager: PresenceExtensionInterface; @@ -54,13 +54,13 @@ class ContainerPresenceManager implements IContainerExtension { } /** - * Acquire an IPresence from a Fluid Container + * Acquire an Presence from a Fluid Container * @param fluidContainer - Fluid Container to acquire the map from - * @returns the IPresence + * @returns the Presence * * @alpha */ -export function acquirePresence(fluidContainer: IFluidContainer): IPresence { +export function getPresence(fluidContainer: IFluidContainer): Presence { assert( isInternalFluidContainer(fluidContainer), 0xa2f /* IFluidContainer was not recognized. Only Containers generated by the Fluid Framework are supported. */, @@ -69,7 +69,7 @@ export function acquirePresence(fluidContainer: IFluidContainer): IPresence { assert( isContainerExtensionStore(innerContainer), - 0xa39 /* Container does not support extensions. Use acquirePresenceViaDataObject. */, + 0xa39 /* Container does not support extensions. Use getPresenceViaDataObject. */, ); const presence = innerContainer.acquireExtension( diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 39cac83c0011..bbd668094b6b 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -14,21 +14,21 @@ export type { ClientConnectionId } from "./baseTypes.js"; export type { - PresenceNotifications, - PresenceNotificationsSchema, - PresenceStates, - PresenceStatesEntries, - PresenceStatesSchema, - PresenceWorkspaceAddress, - PresenceWorkspaceEntry, + NotificationsWorkspace, + NotificationsWorkspaceSchema, + StatesWorkspace, + StatesWorkspaceEntries, + StatesWorkspaceSchema, + StatesWorkspaceEntry, + WorkspaceAddress, } from "./types.js"; export { - type ClientSessionId, - type IPresence, - type ISessionClient, + type Attendee, + type AttendeeId, + type Presence, type PresenceEvents, - SessionClientStatus, + AttendeeStatus, } from "./presence.js"; export type { @@ -36,10 +36,10 @@ export type { BroadcastControlSettings, } from "./broadcastControls.js"; -export { acquirePresence } from "./experimentalAccess.js"; +export { getPresence } from "./experimentalAccess.js"; export { - acquirePresenceViaDataObject, + getPresenceViaDataObject, type ExperimentalPresenceDO, ExperimentalPresenceManager, } from "./datastorePresenceManagerFactory.js"; diff --git a/packages/framework/presence/src/internalTypes.ts b/packages/framework/presence/src/internalTypes.ts index 43272a3b5459..a0cd85184229 100644 --- a/packages/framework/presence/src/internalTypes.ts +++ b/packages/framework/presence/src/internalTypes.ts @@ -7,7 +7,7 @@ import type { IContainerRuntime } from "@fluidframework/container-runtime-defini import type { IFluidDataStoreRuntime } from "@fluidframework/datastore-definitions/internal"; import type { InternalTypes } from "./exposedInternalTypes.js"; -import type { ClientSessionId, ISessionClient } from "./presence.js"; +import type { AttendeeId, Attendee } from "./presence.js"; import type { IRuntimeInternal } from "@fluidframework/presence/internal/container-definitions/internal"; @@ -18,7 +18,7 @@ export interface ClientRecord { // Most value managers should provide value - implement Required> readonly value?: TValueState; - update(client: ISessionClient, received: number, value: TValueState): PostUpdateAction[]; + update(attendee: Attendee, received: number, value: TValueState): PostUpdateAction[]; } /** diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index a60262fda4e3..e12bee99b7ff 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -22,7 +22,7 @@ import type { LatestValueData, LatestValueMetadata, } from "./latestValueTypes.js"; -import type { ClientSessionId, ISessionClient, SpecificSessionClient } from "./presence.js"; +import type { AttendeeId, Attendee, SpecificAttendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -35,12 +35,12 @@ import { brandIVM } from "./valueManager.js"; export interface LatestMapValueClientData< T, Keys extends string | number, - SpecificSessionClientId extends ClientSessionId = ClientSessionId, + SpecificAttendeeId extends AttendeeId = AttendeeId, > { /** - * Associated client. + * Associated attendee. */ - client: ISessionClient; + attendee: Attendee; /** * @privateRemarks This could be regular map currently as no Map is @@ -67,7 +67,7 @@ export interface LatestMapItemValueClientData * @alpha */ export interface LatestMapItemRemovedClientData { - client: ISessionClient; + attendee: Attendee; key: K; metadata: LatestValueMetadata; } @@ -309,7 +309,7 @@ class ValueMapImpl implements ValueMap { * Entries in the map may vary over time and by client, but all values are expected to * be of the same type, which may be a union type. * - * @remarks Create using {@link LatestMap} registered to {@link PresenceStates}. + * @remarks Create using {@link LatestMap} registered to {@link StatesWorkspace}. * * @sealed * @alpha @@ -336,11 +336,11 @@ export interface LatestMapValueManager>; + clientValue(attendee: Attendee): ReadonlyMap>; } class LatestMapValueManagerImpl< @@ -380,28 +380,28 @@ class LatestMapValueManagerImpl< public *clientValues(): IterableIterator> { const allKnownStates = this.datastore.knownValues(this.key); - for (const clientSessionId of objectKeys(allKnownStates.states)) { - if (clientSessionId !== allKnownStates.self) { - const client = this.datastore.lookupClient(clientSessionId); - const items = this.clientValue(client); - yield { client, items }; + for (const attendeeId of objectKeys(allKnownStates.states)) { + if (attendeeId !== allKnownStates.self) { + const attendee = this.datastore.lookupClient(attendeeId); + const items = this.clientValue(attendee); + yield { attendee, items }; } } } - public clients(): ISessionClient[] { + public clients(): Attendee[] { const allKnownStates = this.datastore.knownValues(this.key); return objectKeys(allKnownStates.states) - .filter((clientSessionId) => clientSessionId !== allKnownStates.self) - .map((clientSessionId) => this.datastore.lookupClient(clientSessionId)); + .filter((attendeeId) => attendeeId !== allKnownStates.self) + .map((attendeeId) => this.datastore.lookupClient(attendeeId)); } - public clientValue(client: ISessionClient): ReadonlyMap> { + public clientValue(attendee: Attendee): ReadonlyMap> { const allKnownStates = this.datastore.knownValues(this.key); - const clientSessionId = client.sessionId; - const clientStateMap = allKnownStates.states[clientSessionId]; + const attendeeId = attendee.attendeeId; + const clientStateMap = allKnownStates.states[attendeeId]; if (clientStateMap === undefined) { - throw new Error("No entry for client"); + throw new Error("No entry for attendee"); } const items = new Map>(); for (const [key, item] of objectEntries(clientStateMap.items)) { @@ -416,15 +416,15 @@ class LatestMapValueManagerImpl< return items; } - public update( - client: SpecificSessionClient, + public update( + attendee: SpecificAttendee, _received: number, value: InternalTypes.MapValueState, ): PostUpdateAction[] { const allKnownStates = this.datastore.knownValues(this.key); - const clientSessionId: SpecificSessionClientId = client.sessionId; - const currentState = (allKnownStates.states[clientSessionId] ??= - // New client - prepare new client state directory + const attendeeId: SpecificAttendeeId = attendee.attendeeId; + const currentState = (allKnownStates.states[attendeeId] ??= + // New attendee - prepare new attendee state directory { rev: value.rev, items: {} as unknown as InternalTypes.MapValueState["items"], @@ -448,7 +448,7 @@ class LatestMapValueManagerImpl< currentState.rev = value.rev; } const allUpdates = { - client, + attendee, items: new Map>(), }; const postUpdateActions: PostUpdateAction[] = []; @@ -461,7 +461,7 @@ class LatestMapValueManagerImpl< if (item.value !== undefined) { const itemValue = item.value; const updatedItem = { - client, + attendee, key, value: itemValue, metadata, @@ -471,14 +471,14 @@ class LatestMapValueManagerImpl< } else if (hadPriorValue !== undefined) { postUpdateActions.push(() => this.events.emit("itemRemoved", { - client, + attendee, key, metadata, }), ); } } - this.datastore.update(this.key, clientSessionId, currentState); + this.datastore.update(this.key, attendeeId, currentState); postUpdateActions.push(() => this.events.emit("updated", allUpdates)); return postUpdateActions; } diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 83d4c8514e39..9ff23d7217d8 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -18,7 +18,7 @@ import type { InternalUtilityTypes } from "./exposedUtilityTypes.js"; import type { PostUpdateAction, ValueManager } from "./internalTypes.js"; import { objectEntries } from "./internalUtils.js"; import type { LatestValueClientData, LatestValueData } from "./latestValueTypes.js"; -import type { ISessionClient } from "./presence.js"; +import type { Attendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -48,7 +48,7 @@ export interface LatestValueManagerEvents { * Value manager that provides the latest known value from this client to others and read access to their values. * All participant clients must provide a value. * - * @remarks Create using {@link Latest} registered to {@link PresenceStates}. + * @remarks Create using {@link Latest} registered to {@link StatesWorkspace}. * * @sealed * @alpha @@ -80,11 +80,11 @@ export interface LatestValueManager { /** * Array of known remote clients. */ - clients(): ISessionClient[]; + clients(): Attendee[]; /** - * Access to a specific client's value. + * Access to a specific attendee's value. */ - clientValue(client: ISessionClient): LatestValueData; + clientValue(attendee: Attendee): LatestValueData; } class LatestValueManagerImpl @@ -121,10 +121,10 @@ class LatestValueManagerImpl public *clientValues(): IterableIterator> { const allKnownStates = this.datastore.knownValues(this.key); - for (const [clientSessionId, value] of objectEntries(allKnownStates.states)) { - if (clientSessionId !== allKnownStates.self) { + for (const [attendeeId, value] of objectEntries(allKnownStates.states)) { + if (attendeeId !== allKnownStates.self) { yield { - client: this.datastore.lookupClient(clientSessionId), + attendee: this.datastore.lookupClient(attendeeId), value: value.value, metadata: { revision: value.rev, timestamp: value.timestamp }, }; @@ -132,16 +132,16 @@ class LatestValueManagerImpl } } - public clients(): ISessionClient[] { + public clients(): Attendee[] { const allKnownStates = this.datastore.knownValues(this.key); return Object.keys(allKnownStates.states) - .filter((clientSessionId) => clientSessionId !== allKnownStates.self) - .map((clientSessionId) => this.datastore.lookupClient(clientSessionId)); + .filter((attendeeId) => attendeeId !== allKnownStates.self) + .map((attendeeId) => this.datastore.lookupClient(attendeeId)); } - public clientValue(client: ISessionClient): LatestValueData { + public clientValue(attendee: Attendee): LatestValueData { const allKnownStates = this.datastore.knownValues(this.key); - const clientState = allKnownStates.states[client.sessionId]; + const clientState = allKnownStates.states[attendee.attendeeId]; if (clientState === undefined) { throw new Error("No entry for clientId"); } @@ -152,21 +152,21 @@ class LatestValueManagerImpl } public update( - client: ISessionClient, + attendee: Attendee, _received: number, value: InternalTypes.ValueRequiredState, ): PostUpdateAction[] { const allKnownStates = this.datastore.knownValues(this.key); - const clientSessionId = client.sessionId; - const currentState = allKnownStates.states[clientSessionId]; + const attendeeId = attendee.attendeeId; + const currentState = allKnownStates.states[attendeeId]; if (currentState !== undefined && currentState.rev >= value.rev) { return []; } - this.datastore.update(this.key, clientSessionId, value); + this.datastore.update(this.key, attendeeId, value); return [ () => this.events.emit("updated", { - client, + attendee, value: value.value, metadata: { revision: value.rev, timestamp: value.timestamp }, }), diff --git a/packages/framework/presence/src/latestValueTypes.ts b/packages/framework/presence/src/latestValueTypes.ts index 8a0f6e645ef5..4b3dea1d7f29 100644 --- a/packages/framework/presence/src/latestValueTypes.ts +++ b/packages/framework/presence/src/latestValueTypes.ts @@ -6,7 +6,7 @@ import type { JsonDeserialized } from "@fluidframework/core-interfaces/internal/exposedUtilityTypes"; import type { InternalUtilityTypes } from "./exposedUtilityTypes.js"; -import type { ISessionClient } from "./presence.js"; +import type { Attendee } from "./presence.js"; /** * Metadata for the value state. @@ -38,11 +38,11 @@ export interface LatestValueData { } /** - * State of a specific client's value and its metadata. + * State of a specific attendee's value and its metadata. * * @sealed * @alpha */ export interface LatestValueClientData extends LatestValueData { - client: ISessionClient; + attendee: Attendee; } diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index 858b4d7df110..4ae077ab09bc 100644 --- a/packages/framework/presence/src/notificationsManager.ts +++ b/packages/framework/presence/src/notificationsManager.ts @@ -10,7 +10,7 @@ import type { JsonTypeWith } from "@fluidframework/core-interfaces/internal"; import type { InternalTypes } from "./exposedInternalTypes.js"; import type { InternalUtilityTypes } from "./exposedUtilityTypes.js"; import type { PostUpdateAction, ValueManager } from "./internalTypes.js"; -import type { ISessionClient } from "./presence.js"; +import type { Attendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -24,11 +24,7 @@ export interface NotificationsManagerEvents { * * @eventProperty */ - unattendedNotification: ( - name: string, - sender: ISessionClient, - ...content: unknown[] - ) => void; + unattendedNotification: (name: string, sender: Attendee, ...content: unknown[]) => void; } /** @@ -55,7 +51,7 @@ export interface NotificationListenable< on>( notificationName: K, listener: ( - sender: ISessionClient, + sender: Attendee, ...args: InternalUtilityTypes.JsonDeserializedParameters ) => void, ): Off; @@ -71,7 +67,7 @@ export interface NotificationListenable< off>( notificationName: K, listener: ( - sender: ISessionClient, + sender: Attendee, ...args: InternalUtilityTypes.JsonDeserializedParameters ) => void, ): void; @@ -87,7 +83,7 @@ export type NotificationSubscriptions< E extends InternalUtilityTypes.NotificationListeners, > = { [K in string & keyof InternalUtilityTypes.NotificationListeners]: ( - sender: ISessionClient, + sender: Attendee, ...args: InternalUtilityTypes.JsonDeserializedParameters ) => void; }; @@ -110,14 +106,14 @@ export interface NotificationEmitter>( notificationName: K, - targetClient: ISessionClient, + targetAttendee: Attendee, ...args: Parameters ): void; } @@ -126,7 +122,7 @@ export interface NotificationEmitter { + unicast: (name, targetAttendee, ...args) => { this.datastore.localUpdate( this.key, { @@ -192,7 +188,7 @@ class NotificationsManagerImpl< ignoreUnmonitored: true, }, // This is a notification, so we want to send it immediately. - { allowableUpdateLatencyMs: 0, targetClientId: targetClient.getConnectionId() }, + { allowableUpdateLatencyMs: 0, targetClientId: targetAttendee.getConnectionId() }, ); }, }; @@ -227,7 +223,7 @@ class NotificationsManagerImpl< } public update( - client: ISessionClient, + attendee: Attendee, _received: number, value: InternalTypes.ValueRequiredState, ): PostUpdateAction[] { @@ -236,7 +232,7 @@ class NotificationsManagerImpl< if (this.notificationsInternal.hasListeners(eventName)) { // Without schema validation, we don't know that the args are the correct type. // For now we assume the user is sending the correct types and there is no corruption along the way. - const args = [client, ...value.value.args] as Parameters< + const args = [attendee, ...value.value.args] as Parameters< NotificationSubscriptions[typeof eventName] >; postUpdateActions.push(() => this.notificationsInternal.emit(eventName, ...args)); @@ -245,7 +241,7 @@ class NotificationsManagerImpl< this.events.emit( "unattendedNotification", value.value.name, - client, + attendee, ...value.value.args, ), ); diff --git a/packages/framework/presence/src/presence.ts b/packages/framework/presence/src/presence.ts index 875421fa83c1..fcd7de655677 100644 --- a/packages/framework/presence/src/presence.ts +++ b/packages/framework/presence/src/presence.ts @@ -9,11 +9,11 @@ import type { SessionId } from "@fluidframework/id-compressor"; import type { ClientConnectionId } from "./baseTypes.js"; import type { BroadcastControlSettings } from "./broadcastControls.js"; import type { - PresenceNotifications, - PresenceNotificationsSchema, - PresenceStates, - PresenceStatesSchema, - PresenceWorkspaceAddress, + NotificationsWorkspace, + NotificationsWorkspaceSchema, + StatesWorkspace, + StatesWorkspaceSchema, + WorkspaceAddress, } from "./types.js"; /** @@ -22,36 +22,36 @@ import type { * @remarks * Each client once connected to a session is given a unique identifier for the * duration of the session. If a client disconnects and reconnects, it will - * retain its identifier. Prefer use of {@link ISessionClient} as a way to - * identify clients in a session. {@link ISessionClient.sessionId} will provide + * retain its identifier. Prefer use of {@link Attendee} as a way to + * identify clients in a session. {@link Attendee.attendeeId} will provide * the session ID. * * @alpha */ -export type ClientSessionId = SessionId & { readonly ClientSessionId: "ClientSessionId" }; +export type AttendeeId = SessionId & { readonly AttendeeId: "AttendeeId" }; /** - * The connection status of the {@link ISessionClient}. + * The connection status of the {@link Attendee}. * * @alpha */ -export const SessionClientStatus = { +export const AttendeeStatus = { /** - * The session client is connected to the Fluid service. + * The attendee is connected to the Fluid service. */ Connected: "Connected", /** - * The session client is not connected to the Fluid service. + * The attendee is not connected to the Fluid service. */ Disconnected: "Disconnected", } as const; /** - * Represents the connection status of an {@link ISessionClient}. + * Represents the connection status of an {@link Attendee}. * * This type can be either `'Connected'` or `'Disconnected'`, indicating whether - * the session client is currently connected to the Fluid service. + * the attendee is currently connected to the Fluid service. * * When `'Disconnected'`: * - State changes are kept locally and communicated to others upon reconnect. @@ -59,19 +59,18 @@ export const SessionClientStatus = { * * @alpha */ -export type SessionClientStatus = - (typeof SessionClientStatus)[keyof typeof SessionClientStatus]; +export type AttendeeStatus = (typeof AttendeeStatus)[keyof typeof AttendeeStatus]; /** * A client within a Fluid session (period of container connectivity to service). * * @remarks - * Note: This is very preliminary session client representation. + * Note: This is very preliminary attendee representation. * - * `ISessionClient` should be used as key to distinguish between different + * `Attendee` should be used as key to distinguish between different * clients as they join, rejoin, and disconnect from a session. While a - * client's {@link ClientConnectionId} from {@link ISessionClient.getConnectionStatus} - * may change over time, `ISessionClient` will be fixed. + * client's {@link ClientConnectionId} from {@link Attendee.getConnectionStatus} + * may change over time, `Attendee` will be fixed. * * @privateRemarks * As this is evolved, pay attention to how this relates to Audience, Service @@ -80,13 +79,11 @@ export type SessionClientStatus = * @sealed * @alpha */ -export interface ISessionClient< - SpecificSessionClientId extends ClientSessionId = ClientSessionId, -> { +export interface Attendee { /** * The session ID of the client that is stable over all connections. */ - readonly sessionId: SpecificSessionClientId; + readonly attendeeId: SpecificAttendeeId; /** * Get current client connection ID. @@ -96,27 +93,27 @@ export interface ISessionClient< * @remarks * Connection ID will change on reconnect. * - * If {@link ISessionClient.getConnectionStatus} is {@link (SessionClientStatus:variable).Disconnected}, this will represent the last known connection ID. + * If {@link Attendee.getConnectionStatus} is {@link (AttendeeStatus:variable).Disconnected}, this will represent the last known connection ID. */ getConnectionId(): ClientConnectionId; /** - * Get connection status of session client. + * Get connection status of attendee. * - * @returns Connection status of session client. + * @returns Connection status of attendee. * */ - getConnectionStatus(): SessionClientStatus; + getConnectionStatus(): AttendeeStatus; } /** - * Utility type limiting to a specific session client. (A session client with + * Utility type limiting to a specific attendee. (A attendee with * a specific session ID - not just any session ID.) * * @internal */ -export type SpecificSessionClient = - string extends SpecificSessionClientId ? never : ISessionClient; +export type SpecificAttendee = + string extends SpecificAttendeeId ? never : Attendee; /** * @sealed @@ -128,14 +125,14 @@ export interface PresenceEvents { * * @eventProperty */ - attendeeJoined: (attendee: ISessionClient) => void; + attendeeJoined: (attendee: Attendee) => void; /** * Raised when client appears disconnected from session. * * @eventProperty */ - attendeeDisconnected: (attendee: ISessionClient) => void; + attendeeDisconnected: (attendee: Attendee) => void; /** * Raised when a workspace is activated within the session. @@ -150,7 +147,7 @@ export interface PresenceEvents { * are missed. */ workspaceActivated: ( - workspaceAddress: PresenceWorkspaceAddress, + workspaceAddress: WorkspaceAddress, type: "States" | "Notifications" | "Unknown", ) => void; } @@ -161,7 +158,7 @@ export interface PresenceEvents { * @sealed * @alpha */ -export interface IPresence { +export interface Presence { /** * Events for Presence. */ @@ -174,35 +171,35 @@ export interface IPresence { * Attendee states are dynamic and will change as clients join and leave * the session. */ - getAttendees(): ReadonlySet; + getAttendees(): ReadonlySet; /** * Lookup a specific attendee in the session. * * @param clientId - Client connection or session ID */ - getAttendee(clientId: ClientConnectionId | ClientSessionId): ISessionClient; + getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee; /** - * Get this client's session client. + * Get this client's attendee. * - * @returns This client's session client. + * @returns This client's attendee. */ - getMyself(): ISessionClient; + getMyself(): Attendee; /** - * Acquires a PresenceStates workspace from store or adds new one. + * Acquires a StatesWorkspace from store or adds new one. * - * @param workspaceAddress - Address of the requested PresenceStates Workspace + * @param workspaceAddress - Address of the requested StatesWorkspace * @param requestedContent - Requested states for the workspace * @param controls - Optional settings for default broadcast controls - * @returns A PresenceStates workspace + * @returns A StatesWorkspace */ - getStates( - workspaceAddress: PresenceWorkspaceAddress, + getStates( + workspaceAddress: WorkspaceAddress, requestedContent: StatesSchema, controls?: BroadcastControlSettings, - ): PresenceStates; + ): StatesWorkspace; /** * Acquires a Notifications workspace from store or adds new one. @@ -211,8 +208,8 @@ export interface IPresence { * @param requestedContent - Requested notifications for the workspace * @returns A Notifications workspace */ - getNotifications( - notificationsId: PresenceWorkspaceAddress, + getNotifications( + notificationsId: WorkspaceAddress, requestedContent: NotificationsSchema, - ): PresenceNotifications; + ): NotificationsWorkspace; } diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index 980af1ebb3c7..7184fea44fef 100644 --- a/packages/framework/presence/src/presenceDatastoreManager.ts +++ b/packages/framework/presence/src/presenceDatastoreManager.ts @@ -12,7 +12,7 @@ import type { ClientConnectionId } from "./baseTypes.js"; import type { BroadcastControlSettings } from "./broadcastControls.js"; import type { IEphemeralRuntime, PostUpdateAction } from "./internalTypes.js"; import { objectEntries } from "./internalUtils.js"; -import type { ClientSessionId, ISessionClient, PresenceEvents } from "./presence.js"; +import type { AttendeeId, Attendee, PresenceEvents } from "./presence.js"; import type { ClientUpdateEntry, RuntimeLocalUpdateOptions, @@ -26,16 +26,12 @@ import { } from "./presenceStates.js"; import type { SystemWorkspaceDatastore } from "./systemWorkspace.js"; import { TimerManager } from "./timerManager.js"; -import type { - PresenceStates, - PresenceStatesSchema, - PresenceWorkspaceAddress, -} from "./types.js"; +import type { StatesWorkspace, StatesWorkspaceSchema, WorkspaceAddress } from "./types.js"; import type { IExtensionMessage } from "@fluidframework/presence/internal/container-definitions/internal"; -interface PresenceWorkspaceEntry { - public: PresenceStates; +interface StatesWorkspaceEntry { + public: StatesWorkspace; internal: PresenceStatesInternal; } @@ -43,16 +39,16 @@ interface SystemDatastore { "system:presence": SystemWorkspaceDatastore; } -type InternalWorkspaceAddress = `${"s" | "n"}:${PresenceWorkspaceAddress}`; +type InternalWorkspaceAddress = `${"s" | "n"}:${WorkspaceAddress}`; type PresenceDatastore = SystemDatastore & { - [WorkspaceAddress: string]: ValueElementMap; + [WorkspaceAddress: string]: ValueElementMap; }; interface GeneralDatastoreMessageContent { [WorkspaceAddress: string]: { [StateValueManagerKey: string]: { - [ClientSessionId: ClientSessionId]: ClientUpdateEntry; + [AttendeeId: AttendeeId]: ClientUpdateEntry; }; }; } @@ -97,11 +93,11 @@ function isPresenceMessage( */ export interface PresenceDatastoreManager { joinSession(clientId: ClientConnectionId): void; - getWorkspace( + getWorkspace( internalWorkspaceAddress: InternalWorkspaceAddress, requestedContent: TSchema, controls?: BroadcastControlSettings, - ): PresenceStates; + ): StatesWorkspace; processSignal(message: IExtensionMessage, local: boolean): void; } @@ -122,10 +118,10 @@ function mergeGeneralDatastoreMessageContent( // Iterate over each value manager and its data, merging it as needed. for (const [valueManagerKey, valueManagerValue] of objectEntries(workspaceData)) { - for (const [clientSessionId, value] of objectEntries(valueManagerValue)) { + for (const [attendeeId, value] of objectEntries(valueManagerValue)) { const mergeObject = (mergedData[valueManagerKey] ??= {}); - const oldData = mergeObject[clientSessionId]; - mergeObject[clientSessionId] = mergeValueDirectory( + const oldData = mergeObject[attendeeId]; + mergeObject[attendeeId] = mergeValueDirectory( oldData, value, 0, // local values do not need a time shift @@ -149,19 +145,16 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { private returnedMessages = 0; private refreshBroadcastRequested = false; private readonly timer = new TimerManager(); - private readonly workspaces = new Map< - string, - PresenceWorkspaceEntry - >(); + private readonly workspaces = new Map>(); public constructor( - private readonly clientSessionId: ClientSessionId, + private readonly attendeeId: AttendeeId, private readonly runtime: IEphemeralRuntime, - private readonly lookupClient: (clientId: ClientSessionId) => ISessionClient, + private readonly lookupClient: (clientId: AttendeeId) => Attendee, private readonly logger: ITelemetryLoggerExt | undefined, private readonly events: IEmitter>, systemWorkspaceDatastore: SystemWorkspaceDatastore, - systemWorkspace: PresenceWorkspaceEntry, + systemWorkspace: StatesWorkspaceEntry, ) { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions this.datastore = { "system:presence": systemWorkspaceDatastore } as PresenceDatastore; @@ -186,17 +179,17 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { } satisfies ClientJoinMessage["content"]); } - public getWorkspace( + public getWorkspace( internalWorkspaceAddress: InternalWorkspaceAddress, requestedContent: TSchema, controls?: BroadcastControlSettings, - ): PresenceStates { + ): StatesWorkspace { const existing = this.workspaces.get(internalWorkspaceAddress); if (existing) { return existing.internal.ensureContent(requestedContent, controls); } - let workspaceDatastore: ValueElementMap | undefined = + let workspaceDatastore: ValueElementMap | undefined = this.datastore[internalWorkspaceAddress]; if (workspaceDatastore === undefined) { workspaceDatastore = this.datastore[internalWorkspaceAddress] = {}; @@ -213,7 +206,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { const updates: GeneralDatastoreMessageContent[InternalWorkspaceAddress] = {}; for (const [key, value] of Object.entries(states)) { - updates[key] = { [this.clientSessionId]: value }; + updates[key] = { [this.attendeeId]: value }; } this.enqueueMessage( @@ -226,7 +219,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { const entry = createPresenceStates( { - clientSessionId: this.clientSessionId, + attendeeId: this.attendeeId, lookupClient: this.lookupClient, localUpdate, }, @@ -346,7 +339,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { public processSignal( // Note: IInboundSignalMessage is used here in place of IExtensionMessage // as IExtensionMessage's strictly JSON `content` creates type compatibility - // issues with `ClientSessionId` keys and really unknown value content. + // issues with `AttendeeId` keys and really unknown value content. // IExtensionMessage is a subset of IInboundSignalMessage so this is safe. // Change types of DatastoreUpdateMessage | ClientJoinMessage to // IExtensionMessage<> derivatives to see the issues. @@ -403,7 +396,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { // Separate internal type prefix from public workspace address const match = workspaceAddress.match(/^([^:]):([^:]+:.+)$/) as | null - | [string, string, PresenceWorkspaceAddress]; + | [string, string, WorkspaceAddress]; if (match === null) { continue; diff --git a/packages/framework/presence/src/presenceManager.ts b/packages/framework/presence/src/presenceManager.ts index 74dfc2a029d7..f7f29b9823e8 100644 --- a/packages/framework/presence/src/presenceManager.ts +++ b/packages/framework/presence/src/presenceManager.ts @@ -15,21 +15,12 @@ import { createChildMonitoringContext } from "@fluidframework/telemetry-utils/in import type { ClientConnectionId } from "./baseTypes.js"; import type { BroadcastControlSettings } from "./broadcastControls.js"; import type { IEphemeralRuntime } from "./internalTypes.js"; -import type { - ClientSessionId, - IPresence, - ISessionClient, - PresenceEvents, -} from "./presence.js"; +import type { AttendeeId, Presence, Attendee, PresenceEvents } from "./presence.js"; import type { PresenceDatastoreManager } from "./presenceDatastoreManager.js"; import { PresenceDatastoreManagerImpl } from "./presenceDatastoreManager.js"; import type { SystemWorkspace, SystemWorkspaceDatastore } from "./systemWorkspace.js"; import { createSystemWorkspace } from "./systemWorkspace.js"; -import type { - PresenceStates, - PresenceWorkspaceAddress, - PresenceStatesSchema, -} from "./types.js"; +import type { StatesWorkspace, WorkspaceAddress, StatesWorkspaceSchema } from "./types.js"; import type { IContainerExtension, @@ -48,7 +39,7 @@ export type PresenceExtensionInterface = Required< /** * The Presence manager */ -class PresenceManager implements IPresence, PresenceExtensionInterface { +class PresenceManager implements Presence, PresenceExtensionInterface { private readonly datastoreManager: PresenceDatastoreManager; private readonly systemWorkspace: SystemWorkspace; @@ -56,7 +47,7 @@ class PresenceManager implements IPresence, PresenceExtensionInterface { private readonly mc: MonitoringContext | undefined = undefined; - public constructor(runtime: IEphemeralRuntime, clientSessionId: ClientSessionId) { + public constructor(runtime: IEphemeralRuntime, attendeeId: AttendeeId) { const logger = runtime.logger; if (logger) { this.mc = createChildMonitoringContext({ logger, namespace: "Presence" }); @@ -64,7 +55,7 @@ class PresenceManager implements IPresence, PresenceExtensionInterface { } [this.datastoreManager, this.systemWorkspace] = setupSubComponents( - clientSessionId, + attendeeId, runtime, this.events, this.mc?.logger, @@ -101,23 +92,23 @@ class PresenceManager implements IPresence, PresenceExtensionInterface { this.systemWorkspace.removeClientConnectionId(clientConnectionId); } - public getAttendees(): ReadonlySet { + public getAttendees(): ReadonlySet { return this.systemWorkspace.getAttendees(); } - public getAttendee(clientId: ClientConnectionId | ClientSessionId): ISessionClient { + public getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee { return this.systemWorkspace.getAttendee(clientId); } - public getMyself(): ISessionClient { + public getMyself(): Attendee { return this.systemWorkspace.getMyself(); } - public getStates( - workspaceAddress: PresenceWorkspaceAddress, + public getStates( + workspaceAddress: WorkspaceAddress, requestedContent: TSchema, controls?: BroadcastControlSettings, - ): PresenceStates { + ): StatesWorkspace { return this.datastoreManager.getWorkspace( `s:${workspaceAddress}`, requestedContent, @@ -125,10 +116,10 @@ class PresenceManager implements IPresence, PresenceExtensionInterface { ); } - public getNotifications( - workspaceAddress: PresenceWorkspaceAddress, + public getNotifications( + workspaceAddress: WorkspaceAddress, requestedContent: TSchema, - ): PresenceStates { + ): StatesWorkspace { return this.datastoreManager.getWorkspace(`n:${workspaceAddress}`, requestedContent); } @@ -155,7 +146,7 @@ class PresenceManager implements IPresence, PresenceExtensionInterface { * attendee management. It is registered with the PresenceDatastoreManager. */ function setupSubComponents( - clientSessionId: ClientSessionId, + attendeeId: AttendeeId, runtime: IEphemeralRuntime, events: IEmitter, logger: ITelemetryLoggerExt | undefined, @@ -164,13 +155,13 @@ function setupSubComponents( clientToSessionId: {}, }; const systemWorkspaceConfig = createSystemWorkspace( - clientSessionId, + attendeeId, systemWorkspaceDatastore, events, runtime.getAudience(), ); const datastoreManager = new PresenceDatastoreManagerImpl( - clientSessionId, + attendeeId, runtime, systemWorkspaceConfig.workspace.getAttendee.bind(systemWorkspaceConfig.workspace), logger, @@ -188,7 +179,7 @@ function setupSubComponents( */ export function createPresenceManager( runtime: IEphemeralRuntime, - clientSessionId: ClientSessionId = createSessionId() as ClientSessionId, -): IPresence & PresenceExtensionInterface { - return new PresenceManager(runtime, clientSessionId); + attendeeId: AttendeeId = createSessionId() as AttendeeId, +): Presence & PresenceExtensionInterface { + return new PresenceManager(runtime, attendeeId); } diff --git a/packages/framework/presence/src/presenceStates.ts b/packages/framework/presence/src/presenceStates.ts index 3cbc995348c5..cbe3a067a745 100644 --- a/packages/framework/presence/src/presenceStates.ts +++ b/packages/framework/presence/src/presenceStates.ts @@ -12,15 +12,15 @@ import type { InternalTypes } from "./exposedInternalTypes.js"; import type { ClientRecord, PostUpdateAction } from "./internalTypes.js"; import type { RecordEntryTypes } from "./internalUtils.js"; import { getOrCreateRecord, objectEntries } from "./internalUtils.js"; -import type { ClientSessionId, ISessionClient } from "./presence.js"; +import type { AttendeeId, Attendee } from "./presence.js"; import type { LocalStateUpdateOptions, StateDatastore } from "./stateDatastore.js"; import { handleFromDatastore } from "./stateDatastore.js"; -import type { PresenceStates, PresenceStatesSchema } from "./types.js"; +import type { StatesWorkspace, StatesWorkspaceSchema } from "./types.js"; import { unbrandIVM } from "./valueManager.js"; /** * Extracts `Part` from {@link InternalTypes.ManagerFactory} return type - * matching the {@link PresenceStatesSchema} `Keys` given. + * matching the {@link StatesWorkspaceSchema} `Keys` given. * * @remarks * If the `Part` is an optional property, undefined will be included in the @@ -30,7 +30,7 @@ import { unbrandIVM } from "./valueManager.js"; * @internal */ export type MapSchemaElement< - TSchema extends PresenceStatesSchema, + TSchema extends StatesWorkspaceSchema, Part extends keyof ReturnType, Keys extends keyof TSchema = keyof TSchema, > = ReturnType[Part]; @@ -51,8 +51,8 @@ export interface RuntimeLocalUpdateOptions { * @internal */ export interface PresenceRuntime { - readonly clientSessionId: ClientSessionId; - lookupClient(clientId: ClientConnectionId): ISessionClient; + readonly attendeeId: AttendeeId; + lookupClient(clientId: ClientConnectionId): Attendee; localUpdate( states: { [key: string]: ClientUpdateEntry }, options: RuntimeLocalUpdateOptions, @@ -60,13 +60,13 @@ export interface PresenceRuntime { } type PresenceSubSchemaFromWorkspaceSchema< - TSchema extends PresenceStatesSchema, + TSchema extends StatesWorkspaceSchema, Part extends keyof ReturnType, > = { [Key in keyof TSchema]: MapSchemaElement; }; -type MapEntries = PresenceSubSchemaFromWorkspaceSchema< +type MapEntries = PresenceSubSchemaFromWorkspaceSchema< TSchema, "manager" >; @@ -82,7 +82,7 @@ type MapEntries = PresenceSubSchemaFromWor * * @internal */ -export interface ValueElementMap<_TSchema extends PresenceStatesSchema> { +export interface ValueElementMap<_TSchema extends StatesWorkspaceSchema> { [key: string]: ClientRecord>; } @@ -92,7 +92,7 @@ export interface ValueElementMap<_TSchema extends PresenceStatesSchema> { // type ValueElementMap = // | { // [Key in keyof TSchema & string]?: { -// [ClientSessionId: ClientSessionId]: InternalTypes.ValueDirectoryOrState>; +// [AttendeeId: AttendeeId]: InternalTypes.ValueDirectoryOrState>; // }; // } // | { @@ -124,10 +124,10 @@ interface ValueUpdateRecord { * @internal */ export interface PresenceStatesInternal { - ensureContent( + ensureContent( content: TSchemaAdditional, controls: BroadcastControlSettings | undefined, - ): PresenceStates; + ): StatesWorkspace; processUpdate( received: number, timeModifier: number, @@ -209,7 +209,7 @@ export function mergeValueDirectory< export function mergeUntrackedDatastore( key: string, remoteAllKnownState: ClientUpdateRecord, - datastore: ValueElementMap, + datastore: ValueElementMap, timeModifier: number, ): void { const localAllKnownState = getOrCreateRecord( @@ -217,10 +217,10 @@ export function mergeUntrackedDatastore( key, (): RecordEntryTypes => ({}), ); - for (const [clientSessionId, value] of objectEntries(remoteAllKnownState)) { + for (const [attendeeId, value] of objectEntries(remoteAllKnownState)) { if (!("ignoreUnmonitored" in value)) { - localAllKnownState[clientSessionId] = mergeValueDirectory( - localAllKnownState[clientSessionId], + localAllKnownState[attendeeId] = mergeValueDirectory( + localAllKnownState[attendeeId], value, timeModifier, ); @@ -229,7 +229,7 @@ export function mergeUntrackedDatastore( } /** - * The default allowable update latency for PresenceStates workspaces in milliseconds. + * The default allowable update latency for StatesWorkspace in milliseconds. */ const defaultAllowableUpdateLatencyMs = 60; @@ -237,21 +237,21 @@ const defaultAllowableUpdateLatencyMs = 60; * Produces the value type of a schema element or set of elements. */ type SchemaElementValueType< - TSchema extends PresenceStatesSchema, + TSchema extends StatesWorkspaceSchema, Keys extends keyof TSchema & string, > = Exclude, undefined>["value"]; -class PresenceStatesImpl +class PresenceStatesImpl implements PresenceStatesInternal, - PresenceStates, + StatesWorkspace, StateDatastore< keyof TSchema & string, SchemaElementValueType > { private readonly nodes: MapEntries; - public readonly props: PresenceStates["props"]; + public readonly props: StatesWorkspace["props"]; public readonly controls: RequiredBroadcastControl; @@ -268,7 +268,7 @@ class PresenceStatesImpl // Prepare initial map content from initial state { - const clientSessionId = this.runtime.clientSessionId; + const attendeeId = this.runtime.attendeeId; // Empty record does not satisfy the type, but nodes will post loop. // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const nodes = {} as MapEntries; @@ -280,7 +280,7 @@ class PresenceStatesImpl nodes[key as keyof TSchema] = newNodeData.manager; if ("initialData" in newNodeData) { const { value, allowableUpdateLatencyMs } = newNodeData.initialData; - (datastore[key] ??= {})[clientSessionId] = value; + (datastore[key] ??= {})[attendeeId] = value; newValues[key] = value; if (allowableUpdateLatencyMs !== undefined) { cumulativeAllowableUpdateLatencyMs = @@ -295,7 +295,7 @@ class PresenceStatesImpl // props is the public view of nodes that limits the entries types to // the public interface of the value manager with an additional type // filter that beguiles the type system. So just reinterpret cast. - this.props = this.nodes as unknown as PresenceStates["props"]; + this.props = this.nodes as unknown as StatesWorkspace["props"]; if (anyInitialValues) { this.runtime.localUpdate(newValues, { @@ -309,11 +309,11 @@ class PresenceStatesImpl public knownValues( key: Key, ): { - self: ClientSessionId | undefined; + self: AttendeeId | undefined; states: ClientRecord>; } { return { - self: this.runtime.clientSessionId, + self: this.runtime.attendeeId, // Caller must only use `key`s that are part of `this.datastore`. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion states: this.datastore[key]!, @@ -337,7 +337,7 @@ class PresenceStatesImpl public update( key: Key, - clientId: ClientSessionId, + clientId: AttendeeId, value: Exclude, undefined>["value"], ): void { // Callers my only use `key`s that are part of `this.datastore`. @@ -346,7 +346,7 @@ class PresenceStatesImpl allKnownState[clientId] = mergeValueDirectory(allKnownState[clientId], value, 0); } - public lookupClient(clientId: ClientConnectionId): ISessionClient { + public lookupClient(clientId: ClientConnectionId): Attendee { return this.runtime.lookupClient(clientId); } @@ -357,7 +357,7 @@ class PresenceStatesImpl >( key: TKey, nodeFactory: InternalTypes.ManagerFactory, - ): asserts this is PresenceStates< + ): asserts this is StatesWorkspace< TSchema & Record> > { assert(!(key in this.nodes), 0xa3c /* Already have entry for key in map */); @@ -372,7 +372,7 @@ class PresenceStatesImpl // Already have received state from other clients. Kept in `all`. // TODO: Send current `all` state to state manager. } - datastoreValue[this.runtime.clientSessionId] = value; + datastoreValue[this.runtime.attendeeId] = value; this.runtime.localUpdate( { [key]: value }, { @@ -383,10 +383,10 @@ class PresenceStatesImpl } } - public ensureContent( + public ensureContent( content: TSchemaAdditional, controls: BroadcastControlSettings | undefined, - ): PresenceStates { + ): StatesWorkspace { if (controls?.allowableUpdateLatencyMs !== undefined) { this.controls.allowableUpdateLatencyMs = controls.allowableUpdateLatencyMs; } @@ -401,7 +401,7 @@ class PresenceStatesImpl } } } - return this as PresenceStates; + return this as StatesWorkspace; } public processUpdate( @@ -417,8 +417,8 @@ class PresenceStatesImpl mergeUntrackedDatastore(key, remoteAllKnownState, this.datastore, timeModifier); } else { const node = unbrandIVM(brandedIVM); - for (const [clientSessionId, value] of objectEntries(remoteAllKnownState)) { - const client = this.runtime.lookupClient(clientSessionId); + for (const [attendeeId, value] of objectEntries(remoteAllKnownState)) { + const client = this.runtime.lookupClient(attendeeId); postUpdateActions.push(...node.update(client, received, value)); } } @@ -428,15 +428,15 @@ class PresenceStatesImpl } /** - * Create a new PresenceStates using the DataStoreRuntime provided. + * Create a new StatesWorkspace using the DataStoreRuntime provided. * @param initialContent - The initial value managers to register. */ -export function createPresenceStates( +export function createPresenceStates( runtime: PresenceRuntime, - datastore: ValueElementMap, + datastore: ValueElementMap, initialContent: TSchema, controls: BroadcastControlSettings | undefined, -): { public: PresenceStates; internal: PresenceStatesInternal } { +): { public: StatesWorkspace; internal: PresenceStatesInternal } { const impl = new PresenceStatesImpl(runtime, datastore, initialContent, controls); return { diff --git a/packages/framework/presence/src/stateDatastore.ts b/packages/framework/presence/src/stateDatastore.ts index c9cb8341273a..1bec30785b81 100644 --- a/packages/framework/presence/src/stateDatastore.ts +++ b/packages/framework/presence/src/stateDatastore.ts @@ -6,7 +6,7 @@ import type { ClientConnectionId } from "./baseTypes.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; import type { ClientRecord } from "./internalTypes.js"; -import type { ClientSessionId, ISessionClient } from "./presence.js"; +import type { Attendee, AttendeeId } from "./presence.js"; // type StateDatastoreSchemaNode< // TValue extends InternalTypes.ValueDirectoryOrState = InternalTypes.ValueDirectoryOrState, @@ -17,7 +17,7 @@ import type { ClientSessionId, ISessionClient } from "./presence.js"; // */ // export interface StateDatastoreSchema { // // This type is not precise. It may -// // need to be replaced with PresenceStates schema pattern +// // need to be replaced with StatesWorkspace schema pattern // // similar to what is commented out. // [key: string]: InternalTypes.ValueDirectoryOrState; // // [key: string]: StateDatastoreSchemaNode; @@ -49,12 +49,12 @@ export interface StateDatastore< }, options: LocalStateUpdateOptions, ): void; - update(key: TKey, clientSessionId: ClientSessionId, value: TValue): void; + update(key: TKey, attendeeId: AttendeeId, value: TValue): void; knownValues(key: TKey): { - self: ClientSessionId | undefined; + self: AttendeeId | undefined; states: ClientRecord; }; - lookupClient(clientId: ClientConnectionId): ISessionClient; + lookupClient(clientId: ClientConnectionId): Attendee; } /** diff --git a/packages/framework/presence/src/systemWorkspace.ts b/packages/framework/presence/src/systemWorkspace.ts index 677fd7ac4d95..0645153664a7 100644 --- a/packages/framework/presence/src/systemWorkspace.ts +++ b/packages/framework/presence/src/systemWorkspace.ts @@ -10,16 +10,11 @@ import { assert } from "@fluidframework/core-utils/internal"; import type { ClientConnectionId } from "./baseTypes.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; import type { PostUpdateAction } from "./internalTypes.js"; -import type { - ClientSessionId, - IPresence, - ISessionClient, - PresenceEvents, -} from "./presence.js"; -import { SessionClientStatus } from "./presence.js"; +import type { Attendee, PresenceEvents, AttendeeId, Presence } from "./presence.js"; +import { AttendeeStatus } from "./presence.js"; import type { PresenceStatesInternal } from "./presenceStates.js"; import { TimerManager } from "./timerManager.js"; -import type { PresenceStates, PresenceStatesSchema } from "./types.js"; +import type { StatesWorkspace, StatesWorkspaceSchema } from "./types.js"; /** * The system workspace's datastore structure. @@ -28,21 +23,21 @@ import type { PresenceStates, PresenceStatesSchema } from "./types.js"; */ export interface SystemWorkspaceDatastore { clientToSessionId: { - [ConnectionId: ClientConnectionId]: InternalTypes.ValueRequiredState; + [ConnectionId: ClientConnectionId]: InternalTypes.ValueRequiredState; }; } -class SessionClient implements ISessionClient { +class SessionClient implements Attendee { /** * Order is used to track the most recent client connection * during a session. */ public order: number = 0; - private connectionStatus: SessionClientStatus = SessionClientStatus.Disconnected; + private connectionStatus: AttendeeStatus = AttendeeStatus.Disconnected; public constructor( - public readonly sessionId: ClientSessionId, + public readonly attendeeId: AttendeeId, public connectionId: ClientConnectionId | undefined = undefined, ) {} @@ -53,16 +48,16 @@ class SessionClient implements ISessionClient { return this.connectionId; } - public getConnectionStatus(): SessionClientStatus { + public getConnectionStatus(): AttendeeStatus { return this.connectionStatus; } public setConnected(): void { - this.connectionStatus = SessionClientStatus.Connected; + this.connectionStatus = AttendeeStatus.Connected; } public setDisconnected(): void { - this.connectionStatus = SessionClientStatus.Disconnected; + this.connectionStatus = AttendeeStatus.Disconnected; } } @@ -70,9 +65,9 @@ class SessionClient implements ISessionClient { * @internal */ export interface SystemWorkspace - // Portion of IPresence that is handled by SystemWorkspace along with + // Portion of Presence that is handled by SystemWorkspace along with // responsiblity for emitting "attendeeJoined" events. - extends Pick { + extends Pick { /** * Must be called when the current client acquires a new connection. * @@ -95,9 +90,9 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { * session. The map covers entries for both session ids and connection * ids, which are never expected to collide, but if they did for same * client that would be fine. - * An entry is for session ID if the value's `sessionId` matches the key. + * An entry is for session ID if the value's `attendeeId` matches the key. */ - private readonly attendees = new Map(); + private readonly attendees = new Map(); // When local client disconnects, we lose the connectivity status updates for remote attendees in the session. // Upon reconnect, we mark all other attendees connections as stale and update their status to disconnected after 30 seconds of inactivity. @@ -106,18 +101,18 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { private readonly staleConnectionTimer = new TimerManager(); public constructor( - clientSessionId: ClientSessionId, + attendeeId: AttendeeId, private readonly datastore: SystemWorkspaceDatastore, private readonly events: IEmitter< Pick >, private readonly audience: IAudience, ) { - this.selfAttendee = new SessionClient(clientSessionId); - this.attendees.set(clientSessionId, this.selfAttendee); + this.selfAttendee = new SessionClient(attendeeId); + this.attendees.set(attendeeId, this.selfAttendee); } - public ensureContent( + public ensureContent( _content: TSchemaAdditional, ): never { throw new Error("Method not implemented."); @@ -128,9 +123,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { _timeModifier: number, remoteDatastore: { clientToSessionId: { - [ - ConnectionId: ClientConnectionId - ]: InternalTypes.ValueRequiredState & { + [ConnectionId: ClientConnectionId]: InternalTypes.ValueRequiredState & { ignoreUnmonitored?: true; }; }; @@ -142,9 +135,9 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { for (const [clientConnectionId, value] of Object.entries( remoteDatastore.clientToSessionId, )) { - const clientSessionId = value.value; + const attendeeId = value.value; const { attendee, isJoining } = this.ensureAttendee( - clientSessionId, + attendeeId, clientConnectionId, /* order */ value.rev, // If the attendee is present in audience OR if the attendee update is from the sending remote client itself, @@ -157,7 +150,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { postUpdateActions.push(() => this.events.emit("attendeeJoined", attendee)); } - const knownSessionId: InternalTypes.ValueRequiredState | undefined = + const knownSessionId: InternalTypes.ValueRequiredState | undefined = this.datastore.clientToSessionId[clientConnectionId]; if (knownSessionId === undefined) { this.datastore.clientToSessionId[clientConnectionId] = value; @@ -171,19 +164,19 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { public onConnectionAdded(clientConnectionId: ClientConnectionId): void { assert( - this.selfAttendee.getConnectionStatus() === SessionClientStatus.Disconnected, + this.selfAttendee.getConnectionStatus() === AttendeeStatus.Disconnected, 0xaad /* Local client should be 'Disconnected' before adding new connection. */, ); this.datastore.clientToSessionId[clientConnectionId] = { rev: this.selfAttendee.order++, timestamp: Date.now(), - value: this.selfAttendee.sessionId, + value: this.selfAttendee.attendeeId, }; // Mark 'Connected' remote attendees connections as stale for (const staleConnectionClient of this.attendees.values()) { - if (staleConnectionClient.getConnectionStatus() === SessionClientStatus.Connected) { + if (staleConnectionClient.getConnectionStatus() === AttendeeStatus.Connected) { this.staleConnectionClients.add(staleConnectionClient); } } @@ -218,7 +211,7 @@ 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; - const connected = attendee.getConnectionStatus() === SessionClientStatus.Connected; + const connected = attendee.getConnectionStatus() === AttendeeStatus.Connected; if (!attendeeReconnected && connected) { attendee.setDisconnected(); this.events.emit("attendeeDisconnected", attendee); @@ -226,11 +219,11 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { } } - public getAttendees(): ReadonlySet { + public getAttendees(): ReadonlySet { return new Set(this.attendees.values()); } - public getAttendee(clientId: ClientConnectionId | ClientSessionId): ISessionClient { + public getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee { const attendee = this.attendees.get(clientId); if (attendee) { return attendee; @@ -243,7 +236,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { throw new Error("Attendee not found"); } - public getMyself(): ISessionClient { + public getMyself(): Attendee { return this.selfAttendee; } @@ -253,19 +246,19 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { * to map. If present, make sure the current connection ID is updated. */ private ensureAttendee( - clientSessionId: ClientSessionId, + attendeeId: AttendeeId, clientConnectionId: ClientConnectionId, order: number, isConnected: boolean, ): { attendee: SessionClient; isJoining: boolean } { - let attendee = this.attendees.get(clientSessionId); + let attendee = this.attendees.get(attendeeId); let isJoining = false; if (attendee === undefined) { // New attendee. Create SessionClient and add session ID based // entry to map. - attendee = new SessionClient(clientSessionId, clientConnectionId); - this.attendees.set(clientSessionId, attendee); + attendee = new SessionClient(attendeeId, clientConnectionId); + this.attendees.set(attendeeId, attendee); if (isConnected) { attendee.setConnected(); isJoining = true; @@ -275,7 +268,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { // Update the order and current connection ID. attendee.order = order; // Known attendee is joining the session if they are currently disconnected - if (attendee.getConnectionStatus() === SessionClientStatus.Disconnected && isConnected) { + if (attendee.getConnectionStatus() === AttendeeStatus.Disconnected && isConnected) { attendee.setConnected(); isJoining = true; } @@ -300,7 +293,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { * @internal */ export function createSystemWorkspace( - clientSessionId: ClientSessionId, + attendeeId: AttendeeId, datastore: SystemWorkspaceDatastore, events: IEmitter>, audience: IAudience, @@ -308,15 +301,15 @@ export function createSystemWorkspace( workspace: SystemWorkspace; statesEntry: { internal: PresenceStatesInternal; - public: PresenceStates; + public: StatesWorkspace; }; } { - const workspace = new SystemWorkspaceImpl(clientSessionId, datastore, events, audience); + const workspace = new SystemWorkspaceImpl(attendeeId, datastore, events, audience); return { workspace, statesEntry: { internal: workspace, - public: undefined as unknown as PresenceStates, + public: undefined as unknown as StatesWorkspace, }, }; } diff --git a/packages/framework/presence/src/test/batching.spec.ts b/packages/framework/presence/src/test/batching.spec.ts index b45274e7d50d..63e378640d1f 100644 --- a/packages/framework/presence/src/test/batching.spec.ts +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -7,7 +7,7 @@ import { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal import { describe, it, after, afterEach, before, beforeEach } from "mocha"; import { useFakeTimers, type SinonFakeTimers } from "sinon"; -import { Latest, Notifications, type PresenceNotifications } from "../index.js"; +import { Latest, Notifications, type NotificationsWorkspace } from "../index.js"; import type { createPresenceManager } from "../presenceManager.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; @@ -34,7 +34,7 @@ describe("Presence", () => { clock.setSystemTime(initialTime); // Set up the presence connection. - presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + presence = prepareConnectedPresence(runtime, "attendeeId-2", "client2", clock, logger); }); afterEach(() => { @@ -62,13 +62,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 1010, "value": { @@ -91,13 +91,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 1, "timestamp": 1020, "value": { @@ -120,13 +120,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 2, "timestamp": 1020, "value": { @@ -171,13 +171,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 1010, "value": { @@ -215,13 +215,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 3, "timestamp": 1060, "value": { @@ -244,13 +244,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 6, "timestamp": 1140, "value": { @@ -317,13 +317,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 2, "timestamp": 1100, "value": { @@ -346,13 +346,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 5, "timestamp": 1220, "value": { @@ -416,13 +416,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 1010, "value": { @@ -431,7 +431,7 @@ describe("Presence", () => { }, }, "immediateUpdate": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 1010, "value": { @@ -454,13 +454,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 2, "timestamp": 1100, "value": { @@ -469,7 +469,7 @@ describe("Presence", () => { }, }, "immediateUpdate": { - "sessionId-2": { + "attendeeId-2": { "rev": 1, "timestamp": 1110, "value": { @@ -520,13 +520,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 2, "timestamp": 1050, "value": { @@ -535,7 +535,7 @@ describe("Presence", () => { }, }, "note": { - "sessionId-2": { + "attendeeId-2": { "rev": 1, "timestamp": 1020, "value": { @@ -555,12 +555,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client2": { "rev": 0, "timestamp": 1000, "value": "sessionId-2" }, + "client2": { "rev": 0, "timestamp": 1000, "value": "attendeeId-2" }, }, }, "s:name:testStateWorkspace": { "note": { - "sessionId-2": { + "attendeeId-2": { "rev": 2, "timestamp": 1060, "value": { "message": "final message" }, @@ -614,13 +614,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 2, "timestamp": 1050, "value": { @@ -631,7 +631,7 @@ describe("Presence", () => { }, "s:name:testStateWorkspace2": { "note": { - "sessionId-2": { + "attendeeId-2": { "rev": 2, "timestamp": 1060, "value": { @@ -686,12 +686,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client2": { "rev": 0, "timestamp": 1000, "value": "sessionId-2" }, + "client2": { "rev": 0, "timestamp": 1000, "value": "attendeeId-2" }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 0, "value": { "name": "newId", "args": [77] }, @@ -710,12 +710,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client2": { "rev": 0, "timestamp": 1000, "value": "sessionId-2" }, + "client2": { "rev": 0, "timestamp": 1000, "value": "attendeeId-2" }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 0, "value": { "name": "newId", "args": [88] }, @@ -730,7 +730,7 @@ describe("Presence", () => { // Configure a notifications workspace // eslint-disable-next-line @typescript-eslint/ban-types - const notificationsWorkspace: PresenceNotifications<{}> = presence.getNotifications( + const notificationsWorkspace: NotificationsWorkspace<{}> = presence.getNotifications( "name:testNotificationWorkspace", {}, ); @@ -775,13 +775,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "s:name:testStateWorkspace": { "count": { - "sessionId-2": { + "attendeeId-2": { "rev": 3, "timestamp": 1040, "value": { @@ -792,7 +792,7 @@ describe("Presence", () => { }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 0, "value": { @@ -817,13 +817,13 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": 1000, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 0, "value": { @@ -845,7 +845,7 @@ describe("Presence", () => { }); // will be queued, deadline is 1110 // eslint-disable-next-line @typescript-eslint/ban-types - const notificationsWorkspace: PresenceNotifications<{}> = presence.getNotifications( + const notificationsWorkspace: NotificationsWorkspace<{}> = presence.getNotifications( "name:testNotificationWorkspace", {}, ); @@ -867,7 +867,7 @@ describe("Presence", () => { const { count } = stateWorkspace.props; const { testEvents } = notificationsWorkspace.props; - testEvents.notifications.on("newId", (client, newId) => { + testEvents.notifications.on("newId", (attendee, newId) => { // do nothing }); diff --git a/packages/framework/presence/src/test/broadcastControlsTests.ts b/packages/framework/presence/src/test/broadcastControlsTests.ts index bcebcc4e65c6..cc850579c3ba 100644 --- a/packages/framework/presence/src/test/broadcastControlsTests.ts +++ b/packages/framework/presence/src/test/broadcastControlsTests.ts @@ -6,7 +6,7 @@ import assert from "node:assert"; import type { BroadcastControls, BroadcastControlSettings } from "../broadcastControls.js"; -import type { IPresence } from "../presence.js"; +import type { Presence } from "../presence.js"; import { createPresenceManager } from "../presenceManager.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; @@ -20,7 +20,7 @@ const testDefaultAllowableUpdateLatencyMs = 100; */ export function addControlsTests( createControls: ( - presence: IPresence, + presence: Presence, controlSettings?: BroadcastControlSettings, ) => { controls: BroadcastControls }, ): void { diff --git a/packages/framework/presence/src/test/eventing.spec.ts b/packages/framework/presence/src/test/eventing.spec.ts index 63fa7d4f342b..4868d6def6b4 100644 --- a/packages/framework/presence/src/test/eventing.spec.ts +++ b/packages/framework/presence/src/test/eventing.spec.ts @@ -9,7 +9,7 @@ import { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal import type { SinonFakeTimers, SinonSpy } from "sinon"; import { useFakeTimers, spy } from "sinon"; -import type { ISessionClient, PresenceWorkspaceAddress } from "../index.js"; +import type { Attendee, WorkspaceAddress } from "../index.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; import { assertFinalExpectations, prepareConnectedPresence } from "./testUtils.js"; @@ -33,13 +33,13 @@ const attendeeUpdate = { "client1": { "rev": 0, "timestamp": 0, - "value": "sessionId-1", + "value": "attendeeId-1", }, }, } as const; const latestUpdate = { "latest": { - "sessionId-1": { + "attendeeId-1": { "rev": 1, "timestamp": 0, "value": { x: 1, y: 1, z: 1 }, @@ -48,7 +48,7 @@ const latestUpdate = { } as const; const latestMapUpdate = { "latestMap": { - "sessionId-1": { + "attendeeId-1": { "rev": 1, "items": { "key1": { @@ -67,7 +67,7 @@ const latestMapUpdate = { } as const; const latestUpdateRev2 = { "latest": { - "sessionId-1": { + "attendeeId-1": { "rev": 2, "timestamp": 50, "value": { x: 2, y: 2, z: 2 }, @@ -76,7 +76,7 @@ const latestUpdateRev2 = { } as const; const itemRemovedMapUpdate = { "latestMap": { - "sessionId-1": { + "attendeeId-1": { "rev": 2, "items": { "key2": { @@ -89,7 +89,7 @@ const itemRemovedMapUpdate = { } as const; const itemRemovedAndItemUpdatedMapUpdate = { "latestMap": { - "sessionId-1": { + "attendeeId-1": { "rev": 2, "items": { "key2": { @@ -107,7 +107,7 @@ const itemRemovedAndItemUpdatedMapUpdate = { }; const itemUpdatedAndItemRemoveddMapUpdate = { "latestMap": { - "sessionId-1": { + "attendeeId-1": { "rev": 2, "items": { "key1": { @@ -129,7 +129,7 @@ const latestMapItemRemovedAndLatestUpdate = { } as const; const notificationsUpdate = { "notifications": { - "sessionId-1": { + "attendeeId-1": { "rev": 0, "timestamp": 0, "value": { "name": "newId", "args": [42] }, @@ -167,12 +167,12 @@ describe("Presence", () => { expectedValue: LatestMapValueExpected; }; - function verifyState(attendee: ISessionClient, verifications: StateVerification[]): void { + function verifyState(attendee: Attendee, verifications: StateVerification[]): void { assert.ok(attendee, "Eventing does not reflect new attendee"); assert.strictEqual( - attendee.sessionId, - "sessionId-1", - "Eventing does not reflect new attendee's sessionId", + attendee.attendeeId, + "attendeeId-1", + "Eventing does not reflect new attendee's attendeeId", ); assert.strictEqual( attendee.getConnectionId(), @@ -217,7 +217,7 @@ describe("Presence", () => { beforeEach(() => { logger = new EventAndErrorTrackingLogger(); runtime = new MockEphemeralRuntime(logger); - presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + presence = prepareConnectedPresence(runtime, "attendeeId-2", "client2", clock, logger); }); afterEach(function (done: Mocha.Done) { @@ -258,7 +258,7 @@ describe("Presence", () => { workspace.add( "notifications", Notifications<{ newId: (id: number) => void }>({ - newId: (_client: ISessionClient, _id: number) => {}, + newId: (_attendee: Attendee, _id: number) => {}, }), ); notificationManager = workspace.props.notifications; @@ -279,7 +279,7 @@ describe("Presence", () => { function setupNotificationsWorkspace(): void { const notificationsWorkspace = presence.getNotifications("name:testWorkspace", { notifications: Notifications<{ newId: (id: number) => void }>({ - newId: (_client: ISessionClient, _id: number) => {}, + newId: (_attendee: Attendee, _id: number) => {}, }), }); notificationManager = notificationsWorkspace.props.notifications; @@ -303,8 +303,8 @@ describe("Presence", () => { ); } - function getTestAttendee(): ISessionClient { - return presence.getAttendee("sessionId-1"); + function getTestAttendee(): Attendee { + return presence.getAttendee("attendeeId-1"); } describe("states workspace", () => { @@ -655,20 +655,18 @@ describe("Presence", () => { it("from unregistered workspace triggers 'workspaceActivated' event", async () => { // Setup notificationSpy = spy(); - const workspaceActivatedEventSpy = spy( - (workspaceAddress: PresenceWorkspaceAddress) => { - // Once activated, register the notifications workspace and listener for it's event - const notificationsWorkspace = presence.getNotifications(workspaceAddress, { - notifications: Notifications<{ newId: (id: number) => void }>({ - newId: (_client: ISessionClient, _id: number) => {}, - }), - }); - notificationsWorkspace.props.notifications.notifications.on( - "newId", - notificationSpy, - ); - }, - ); + const workspaceActivatedEventSpy = spy((workspaceAddress: WorkspaceAddress) => { + // Once activated, register the notifications workspace and listener for it's event + const notificationsWorkspace = presence.getNotifications(workspaceAddress, { + notifications: Notifications<{ newId: (id: number) => void }>({ + newId: (_attendee: Attendee, _id: number) => {}, + }), + }); + notificationsWorkspace.props.notifications.notifications.on( + "newId", + notificationSpy, + ); + }); presence.events.on("workspaceActivated", (workspaceAddress, type) => { if (workspaceAddress === "name:testWorkspace" && type === "Notifications") { workspaceActivatedEventSpy(workspaceAddress); diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index 041656121818..758874925695 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -12,7 +12,7 @@ import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; import type { BroadcastControlSettings, - IPresence, + Presence, LatestMapItemValueClientData, LatestMapValueManager, } from "@fluidframework/presence/alpha"; @@ -22,7 +22,7 @@ const testWorkspaceName = "name:testWorkspaceA"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type function createLatestMapManager( - presence: IPresence, + presence: Presence, valueControlSettings?: BroadcastControlSettings, ) { const states = presence.getStates(testWorkspaceName, { @@ -97,7 +97,7 @@ describe("Presence", () => { */ export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const presence = {} as IPresence; + const presence = {} as Presence; const statesWorkspace = presence.getStates("name:testStatesWorkspaceWithLatestMap", { fixedMap: LatestMap({ key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }), }); @@ -136,14 +136,14 @@ export function checkCompiles(): void { const localPointers = pointers.local; function logClientValue({ - client, + attendee, key, value, }: Pick< LatestMapItemValueClientData, - "client" | "key" | "value" + "attendee" | "key" | "value" >): void { - console.log(client.sessionId, key, value); + console.log(attendee.attendeeId, key, value); } localPointers.set("pen", { x: 1, y: 2 }); @@ -151,22 +151,22 @@ export function checkCompiles(): void { const pointerItemUpdatedOff = pointers.events.on("itemUpdated", logClientValue); pointerItemUpdatedOff(); - for (const client of pointers.clients()) { - const items = pointers.clientValue(client); + for (const attendee of pointers.clients()) { + const items = pointers.clientValue(attendee); for (const [key, { value }] of items.entries()) { - logClientValue({ client, key, value }); + logClientValue({ attendee, key, value }); } } - for (const { client, items } of pointers.clientValues()) { - for (const [key, { value }] of items.entries()) logClientValue({ client, key, value }); + for (const { attendee, items } of pointers.clientValues()) { + for (const [key, { value }] of items.entries()) logClientValue({ attendee, key, value }); } - pointers.events.on("itemRemoved", ({ client, key }) => - logClientValue({ client, key, value: "" }), + pointers.events.on("itemRemoved", ({ attendee, key }) => + logClientValue({ attendee, key, value: "" }), ); - pointers.events.on("updated", ({ client, items }) => { - for (const [key, { value }] of items.entries()) logClientValue({ client, key, value }); + pointers.events.on("updated", ({ attendee, items }) => { + for (const [key, { value }] of items.entries()) logClientValue({ attendee, key, value }); }); } diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index e8e2dd90d24b..c32eec749de3 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -12,7 +12,7 @@ import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; import type { BroadcastControlSettings, - IPresence, + Presence, LatestValueClientData, } from "@fluidframework/presence/alpha"; import { Latest } from "@fluidframework/presence/alpha"; @@ -21,7 +21,7 @@ const testWorkspaceName = "name:testWorkspaceA"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type function createLatestManager( - presence: IPresence, + presence: Presence, valueControlSettings?: BroadcastControlSettings, ) { const states = presence.getStates(testWorkspaceName, { @@ -38,7 +38,7 @@ describe("Presence", () => { it("API use compiles", () => {}); describe("when initialized", () => { - let presence: IPresence; + let presence: Presence; beforeEach(() => { presence = createPresenceManager(new MockEphemeralRuntime()); @@ -103,7 +103,7 @@ describe("Presence", () => { */ export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const presence = {} as IPresence; + const presence = {} as Presence; const statesWorkspace = presence.getStates("name:testStatesWorkspaceWithLatest", { cursor: Latest({ x: 0, y: 0 }), camera: Latest({ x: 0, y: 0, z: 0 }), @@ -123,8 +123,8 @@ export function checkCompiles(): void { function logClientValue< T /* following extends should not be required: */ extends Record, - >({ client, value }: Pick, "client" | "value">): void { - console.log(client.sessionId, value); + >({ attendee, value }: Pick, "attendee" | "value">): void { + console.log(attendee.attendeeId, value); } // Create new cursor state @@ -134,17 +134,17 @@ export function checkCompiles(): void { cursor.local = { x: 1, y: 2 }; // Listen to others cursor updates - const cursorUpdatedOff = cursor.events.on("updated", ({ client, value }) => - console.log(`client ${client.sessionId}'s cursor is now at (${value.x},${value.y})`), + const cursorUpdatedOff = cursor.events.on("updated", ({ attendee, value }) => + console.log(`attendee ${attendee.attendeeId}'s cursor is now at (${value.x},${value.y})`), ); cursorUpdatedOff(); - for (const client of cursor.clients()) { - logClientValue({ client, ...cursor.clientValue(client) }); + for (const attendee of cursor.clients()) { + logClientValue({ attendee, ...cursor.clientValue(attendee) }); } // Enumerate all cursor values - for (const { client, value } of cursor.clientValues()) { - logClientValue({ client, value }); + for (const { attendee, value } of cursor.clientValues()) { + logClientValue({ attendee, value }); } } diff --git a/packages/framework/presence/src/test/notificationsManager.spec.ts b/packages/framework/presence/src/test/notificationsManager.spec.ts index 1789143cc95d..454f66e6c8a9 100644 --- a/packages/framework/presence/src/test/notificationsManager.spec.ts +++ b/packages/framework/presence/src/test/notificationsManager.spec.ts @@ -8,7 +8,7 @@ import { strict as assert, fail } from "node:assert"; import { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal"; import { useFakeTimers, type SinonFakeTimers } from "sinon"; -import type { ISessionClient, NotificationsManager, PresenceNotifications } from "../index.js"; +import type { Attendee, NotificationsManager, NotificationsWorkspace } from "../index.js"; import { Notifications } from "../index.js"; import type { createPresenceManager } from "../presenceManager.js"; @@ -29,7 +29,7 @@ describe("Presence", () => { let clock: SinonFakeTimers; let presence: ReturnType; // eslint-disable-next-line @typescript-eslint/ban-types - let notificationsWorkspace: PresenceNotifications<{}>; + let notificationsWorkspace: NotificationsWorkspace<{}>; before(async () => { clock = useFakeTimers(); @@ -45,7 +45,7 @@ describe("Presence", () => { clock.setSystemTime(initialTime); // Set up the presence connection - presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + presence = prepareConnectedPresence(runtime, "attendeeId-2", "client2", clock, logger); // Get a notifications workspace notificationsWorkspace = presence.getNotifications("name:testNotificationWorkspace", {}); @@ -100,7 +100,7 @@ describe("Presence", () => { }, "testEvents" >({ - newId: (_client: ISessionClient, _id: number) => {}, + newId: (_attendee: Attendee, _id: number) => {}, }), ); @@ -116,12 +116,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client2": { "rev": 0, "timestamp": 1000, "value": "sessionId-2" }, + "client2": { "rev": 0, "timestamp": 1000, "value": "attendeeId-2" }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 0, "value": { "name": "newId", "args": [42] }, @@ -151,7 +151,7 @@ describe("Presence", () => { }, "testEvents" >({ - newId: (_client: ISessionClient, _id: number) => {}, + newId: (_attendee: Attendee, _id: number) => {}, }), ); @@ -167,12 +167,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client2": { "rev": 0, "timestamp": 1000, "value": "sessionId-2" }, + "client2": { "rev": 0, "timestamp": 1000, "value": "attendeeId-2" }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-2": { + "attendeeId-2": { "rev": 0, "timestamp": 0, "value": { "name": "newId", "args": [42] }, @@ -193,17 +193,17 @@ describe("Presence", () => { }); it("raises named event when notification is received", async () => { - type EventCalls = { client: ISessionClient; id: number }[]; + type EventCalls = { attendee: Attendee; id: number }[]; const eventHandlerCalls = { original: [] as EventCalls, secondary: [] as EventCalls, tertiary: [] as EventCalls, }; - function originalEventHandler(client: ISessionClient, id: number): void { - assert.equal(client.sessionId, "sessionId-3"); + function originalEventHandler(attendee: Attendee, id: number): void { + assert.equal(attendee.attendeeId, "attendeeId-3"); assert.equal(id, 42); - eventHandlerCalls.original.push({ client, id }); + eventHandlerCalls.original.push({ attendee, id }); } notificationsWorkspace.add( @@ -226,11 +226,11 @@ describe("Presence", () => { }); const disconnectFunctions = [ - testEvents.notifications.on("newId", (client: ISessionClient, id: number) => { - eventHandlerCalls.secondary.push({ client, id }); + testEvents.notifications.on("newId", (attendee: Attendee, id: number) => { + eventHandlerCalls.secondary.push({ attendee, id }); }), - testEvents.notifications.on("newId", (client: ISessionClient, id: number) => { - eventHandlerCalls.tertiary.push({ client, id }); + testEvents.notifications.on("newId", (attendee: Attendee, id: number) => { + eventHandlerCalls.tertiary.push({ attendee, id }); }), ]; @@ -245,12 +245,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client3": { "rev": 0, "timestamp": 1000, "value": "sessionId-3" }, + "client3": { "rev": 0, "timestamp": 1000, "value": "attendeeId-3" }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-3": { + "attendeeId-3": { "rev": 0, "timestamp": 0, "value": { "name": "newId", "args": [42] }, @@ -296,7 +296,7 @@ describe("Presence", () => { }, "testEvents" >({ - newId: (client: ISessionClient, id: number) => { + newId: (attendee: Attendee, id: number) => { fail(`Unexpected newId event`); }, }), @@ -306,7 +306,7 @@ describe("Presence", () => { testEvents.events.on("unattendedNotification", (name, sender, ...content) => { assert.equal(name, "oldId"); - assert.equal(sender.sessionId, "sessionId-3"); + assert.equal(sender.attendeeId, "attendeeId-3"); assert.deepEqual(content, [41]); assert(!unattendedEventCalled); unattendedEventCalled = true; @@ -323,12 +323,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client3": { "rev": 0, "timestamp": 1000, "value": "sessionId-3" }, + "client3": { "rev": 0, "timestamp": 1000, "value": "attendeeId-3" }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-3": { + "attendeeId-3": { "rev": 0, "timestamp": 0, "value": { "name": "oldId", "args": [41] }, @@ -349,7 +349,7 @@ describe("Presence", () => { it("raises `unattendedEvent` event when recognized notification is received without listeners", async () => { let unattendedEventCalled = false; - function newIdEventHandler(client: ISessionClient, id: number): void { + function newIdEventHandler(attendee: Attendee, id: number): void { fail(`Unexpected newId event`); } @@ -370,7 +370,7 @@ describe("Presence", () => { testEvents.events.on("unattendedNotification", (name, sender, ...content) => { assert.equal(name, "newId"); - assert.equal(sender.sessionId, "sessionId-3"); + assert.equal(sender.attendeeId, "attendeeId-3"); assert.deepEqual(content, [43]); assert(!unattendedEventCalled); unattendedEventCalled = true; @@ -389,12 +389,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client3": { "rev": 0, "timestamp": 1000, "value": "sessionId-3" }, + "client3": { "rev": 0, "timestamp": 1000, "value": "attendeeId-3" }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-3": { + "attendeeId-3": { "rev": 0, "timestamp": 0, "value": { "name": "newId", "args": [43] }, @@ -415,8 +415,8 @@ describe("Presence", () => { it("removed listeners are not called when related notification is received", async () => { let originalEventHandlerCalled = false; - function originalEventHandler(client: ISessionClient, id: number): void { - assert.equal(client.sessionId, "sessionId-3"); + function originalEventHandler(attendee: Attendee, id: number): void { + assert.equal(attendee.attendeeId, "attendeeId-3"); assert.equal(id, 44); assert.equal(originalEventHandlerCalled, false); originalEventHandlerCalled = true; @@ -443,7 +443,7 @@ describe("Presence", () => { const disconnect = testEvents.notifications.on( "newId", - (_client: ISessionClient, _id: number) => { + (_attendee: Attendee, _id: number) => { fail(`Unexpected event raised on disconnected listener`); }, ); @@ -461,12 +461,12 @@ describe("Presence", () => { "data": { "system:presence": { "clientToSessionId": { - "client3": { "rev": 0, "timestamp": 1000, "value": "sessionId-3" }, + "client3": { "rev": 0, "timestamp": 1000, "value": "attendeeId-3" }, }, }, "n:name:testNotificationWorkspace": { "testEvents": { - "sessionId-3": { + "attendeeId-3": { "rev": 0, "timestamp": 0, "value": { "name": "newId", "args": [44] }, diff --git a/packages/framework/presence/src/test/presenceDatastoreManager.spec.ts b/packages/framework/presence/src/test/presenceDatastoreManager.spec.ts index 8e8473dbf8c9..182e9584c3e9 100644 --- a/packages/framework/presence/src/test/presenceDatastoreManager.spec.ts +++ b/packages/framework/presence/src/test/presenceDatastoreManager.spec.ts @@ -52,14 +52,14 @@ describe("Presence", () => { it("sends join when connected during initialization", () => { // Setup, Act (call to createPresenceManager), & Verify (post createPresenceManager call) - prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + prepareConnectedPresence(runtime, "attendeeId-2", "client2", clock, logger); }); describe("responds to ClientJoin", () => { let presence: ReturnType; beforeEach(() => { - presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + presence = prepareConnectedPresence(runtime, "attendeeId-2", "client2", clock, logger); // Pass a little time (to mimic reality) clock.tick(10); @@ -85,7 +85,7 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": initialTime, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, @@ -155,7 +155,7 @@ describe("Presence", () => { "client2": { "rev": 0, "timestamp": initialTime, - "value": "sessionId-2", + "value": "attendeeId-2", }, }, }, @@ -182,14 +182,14 @@ describe("Presence", () => { "client1": { "rev": 0, "timestamp": 0, - "value": "sessionId-1", + "value": "attendeeId-1", }, }, }; const statesWorkspaceUpdate = { "latest": { - "sessionId-1": { + "attendeeId-1": { "rev": 1, "timestamp": 0, "value": {}, @@ -199,7 +199,7 @@ describe("Presence", () => { const notificationsWorkspaceUpdate = { "testEvents": { - "sessionId-1": { + "attendeeId-1": { "rev": 0, "timestamp": 0, "value": {}, @@ -209,7 +209,7 @@ describe("Presence", () => { }; beforeEach(() => { - presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + presence = prepareConnectedPresence(runtime, "attendeeId-2", "client2", clock, logger); // Pass a little time (to mimic reality) clock.tick(10); @@ -291,7 +291,7 @@ describe("Presence", () => { "system:presence": systemWorkspaceUpdate, "u:name:testUnknownWorkspace": { "latest": { - "sessionId-1": { + "attendeeId-1": { "rev": 1, "timestamp": 0, "value": { x: 1, y: 1, z: 1 }, diff --git a/packages/framework/presence/src/test/presenceManager.spec.ts b/packages/framework/presence/src/test/presenceManager.spec.ts index 8dc30f4ee98c..0bcc35dd9967 100644 --- a/packages/framework/presence/src/test/presenceManager.spec.ts +++ b/packages/framework/presence/src/test/presenceManager.spec.ts @@ -10,7 +10,7 @@ import type { SinonFakeTimers } from "sinon"; import { useFakeTimers } from "sinon"; import type { ClientConnectionId } from "../baseTypes.js"; -import { SessionClientStatus, type ISessionClient } from "../presence.js"; +import { AttendeeStatus, type Attendee } from "../presence.js"; import { createPresenceManager } from "../presenceManager.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; @@ -80,7 +80,7 @@ describe("Presence", () => { const afterCleanUp: (() => void)[] = []; beforeEach(() => { - presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + presence = prepareConnectedPresence(runtime, "attendeeId-2", "client2", clock, logger); }); afterEach(() => { @@ -91,7 +91,7 @@ describe("Presence", () => { }); describe("attendee", () => { - const attendeeSessionId = "sessionId-4"; + const attendeeSessionId = "attendeeId-4"; const initialAttendeeConnectionId = "client4"; // Note: this connection id exists in the mock runtime audience since // initialization, but should go unnoticed by the presence manager @@ -103,8 +103,8 @@ describe("Presence", () => { // Processes join signals and returns the attendees that were announced via `attendeeJoined` function processJoinSignals( signals: ReturnType[], - ): ISessionClient[] { - const joinedAttendees: ISessionClient[] = []; + ): Attendee[] { + const joinedAttendees: Attendee[] = []; const cleanUpListener = presence.events.on("attendeeJoined", (attendee) => { joinedAttendees.push(attendee); }); @@ -118,13 +118,13 @@ describe("Presence", () => { } function verifyAttendee( - actualAttendee: ISessionClient, + actualAttendee: Attendee, expectedConnectionId: ClientConnectionId, expectedSessionId: string, - expectedConnectionStatus: SessionClientStatus = SessionClientStatus.Connected, + expectedConnectionStatus: AttendeeStatus = AttendeeStatus.Connected, ): void { assert.equal( - actualAttendee.sessionId, + actualAttendee.attendeeId, expectedSessionId, "Attendee has wrong session id", ); @@ -146,14 +146,14 @@ describe("Presence", () => { initialAttendeeSignal = generateBasicClientJoin(clock.now - 50, { averageLatency: 50, - clientSessionId: attendeeSessionId, + attendeeId: attendeeSessionId, clientConnectionId: initialAttendeeConnectionId, updateProviders: ["client2"], }); rejoinAttendeeSignal = generateBasicClientJoin(clock.now - 20, { averageLatency: 20, - clientSessionId: attendeeSessionId, // Same session id + attendeeId: attendeeSessionId, // Same session id clientConnectionId: rejoinAttendeeConnectionId, // Different connection id connectionOrder: 1, updateProviders: ["client2"], @@ -254,7 +254,7 @@ describe("Presence", () => { const collateralAttendeeConnectionId = "client3"; const collateralAttendeeSignal = generateBasicClientJoin(clock.now - 10, { averageLatency: 40, - clientSessionId: attendeeSessionId, + attendeeId: attendeeSessionId, clientConnectionId: rejoinAttendeeConnectionId, connectionOrder: 1, updateProviders: ["client2"], @@ -293,7 +293,7 @@ describe("Presence", () => { // Rejoin signal for the collateral attendee unknown to audience const rejoinSignal = generateBasicClientJoin(clock.now - 10, { averageLatency: 40, - clientSessionId: "collateral-id", + attendeeId: "collateral-id", clientConnectionId: newAttendeeConnectionId, updateProviders: [initialAttendeeConnectionId], connectionOrder: 1, @@ -309,7 +309,7 @@ describe("Presence", () => { // Response signal sent by the initial attendee responding to the collateral attendees rejoin signal const responseSignal = generateBasicClientJoin(clock.now - 5, { averageLatency: 20, - clientSessionId: attendeeSessionId, + attendeeId: attendeeSessionId, clientConnectionId: initialAttendeeConnectionId, priorClientToSessionId: { ...initialAttendeeSignal.content.data["system:presence"].clientToSessionId, @@ -355,7 +355,7 @@ describe("Presence", () => { }); describe("that is already known", () => { - let knownAttendee: ISessionClient | undefined; + let knownAttendee: Attendee | undefined; beforeEach(() => { // Setup known attendee @@ -396,9 +396,9 @@ describe("Presence", () => { }); for (const [status, setup] of [ - [SessionClientStatus.Connected, () => {}] as const, + [AttendeeStatus.Connected, () => {}] as const, [ - SessionClientStatus.Disconnected, + AttendeeStatus.Disconnected, () => runtime.removeMember(initialAttendeeConnectionId), ] as const, ]) { @@ -448,7 +448,7 @@ describe("Presence", () => { // (e.g. being in audience, sending an update, or (re)joining the session) before their connection status set to "Disconnected". // If an attendee with a stale connection becomes active, their "stale" status is removed. describe("and then local client disconnects", () => { - let remoteDisconnectedAttendees: ISessionClient[]; + let remoteDisconnectedAttendees: Attendee[]; beforeEach(() => { // Setup assert(knownAttendee !== undefined, "No attendee was set in beforeEach"); @@ -474,7 +474,7 @@ describe("Presence", () => { clock.tick(15_001); assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Connected, + AttendeeStatus.Connected, "Attendee with stale connection should still be 'Connected' after 15s", ); @@ -482,7 +482,7 @@ describe("Presence", () => { clock.tick(15_001); assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Disconnected, + AttendeeStatus.Disconnected, "Attendee with stale connection should be 'Disconnected' 30s after reconnection", ); assert.strictEqual( @@ -502,7 +502,7 @@ describe("Presence", () => { // Verify - attendee with stale connection should still be 'Connected' if local client never reconnects assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Connected, + AttendeeStatus.Connected, "Attendee with stale connection should still be 'Connected' after 30s", ); }); @@ -521,7 +521,7 @@ describe("Presence", () => { // Verify - attendee with stale connection should still be 'Connected' if local client never reconnects for at least 30s assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Connected, + AttendeeStatus.Connected, "Attendee with stale connection should still be 'Connected' after 30s", ); }); @@ -549,7 +549,7 @@ describe("Presence", () => { // Verify - rejoining attendee should still be 'Connected' with no `attendeeJoined` announced assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Connected, + AttendeeStatus.Connected, "Active attendee should still be 'Connected' 30s after reconnection", ); }); @@ -595,7 +595,7 @@ describe("Presence", () => { // Verify - active attendee should still be 'Connected' assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Connected, + AttendeeStatus.Connected, "Active attendee should still be 'Connected' 30s after reconnection", ); }); @@ -623,7 +623,7 @@ describe("Presence", () => { // Verify - active attendee status should be 'Disconnected' and no other `attendeeDisconnected` should be announced. assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Disconnected, + AttendeeStatus.Disconnected, "Attendee should be 'Disconnected'", ); assert.strictEqual( @@ -638,7 +638,7 @@ describe("Presence", () => { assert(knownAttendee !== undefined, "No attendee was set in beforeEach"); assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Connected, + AttendeeStatus.Connected, "Known attendee is not connected", ); @@ -657,7 +657,7 @@ describe("Presence", () => { clock.tick(15_001); assert.strictEqual( knownAttendee.getConnectionStatus(), - SessionClientStatus.Connected, + AttendeeStatus.Connected, "Attendee with stale connection should still be connected", ); @@ -665,7 +665,7 @@ describe("Presence", () => { clock.tick(15_001); assert.equal( knownAttendee.getConnectionStatus(), - SessionClientStatus.Disconnected, + AttendeeStatus.Disconnected, "Attendee with stale connection has wrong status", ); assert.strictEqual( @@ -680,7 +680,7 @@ describe("Presence", () => { it("is announced via `attendeeDisconnected`", () => { // Setup assert(knownAttendee !== undefined, "No attendee was set in beforeEach"); - let disconnectedAttendee: ISessionClient | undefined; + let disconnectedAttendee: Attendee | undefined; afterCleanUp.push( presence.events.on("attendeeDisconnected", (attendee) => { assert( @@ -703,7 +703,7 @@ describe("Presence", () => { disconnectedAttendee, initialAttendeeConnectionId, attendeeSessionId, - SessionClientStatus.Disconnected, + AttendeeStatus.Disconnected, ); }); @@ -737,7 +737,7 @@ describe("Presence", () => { }); describe("that is rejoining", () => { - let priorAttendee: ISessionClient | undefined; + let priorAttendee: Attendee | undefined; beforeEach(() => { // Setup prior attendee const joinedAttendees = processJoinSignals([initialAttendeeSignal]); diff --git a/packages/framework/presence/src/test/presenceStates.spec.ts b/packages/framework/presence/src/test/presenceStates.spec.ts index 008c9f58aa7b..253c91b5cfd7 100644 --- a/packages/framework/presence/src/test/presenceStates.spec.ts +++ b/packages/framework/presence/src/test/presenceStates.spec.ts @@ -9,12 +9,12 @@ import type { } from "@fluidframework/core-interfaces/internal/exposedUtilityTypes"; import type { InternalTypes } from "../exposedInternalTypes.js"; -import type { IPresence } from "../presence.js"; +import type { Presence } from "../presence.js"; import { addControlsTests } from "./broadcastControlsTests.js"; describe("Presence", () => { - describe("PresenceStates", () => { + describe("StatesWorkspace", () => { /** * See {@link checkCompiles} below */ @@ -46,7 +46,7 @@ declare function createValueManager( */ export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const presence = {} as IPresence; + const presence = {} as Presence; const statesWorkspace = presence.getStates("name:testWorkspaceA", { cursor: createValueManager({ x: 0, y: 0 }), // eslint-disable-next-line prefer-object-spread diff --git a/packages/framework/presence/src/test/testUtils.ts b/packages/framework/presence/src/test/testUtils.ts index 848dd23f3334..ec1112bcb838 100644 --- a/packages/framework/presence/src/test/testUtils.ts +++ b/packages/framework/presence/src/test/testUtils.ts @@ -12,7 +12,7 @@ import { createPresenceManager } from "../presenceManager.js"; import type { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; -import type { ClientConnectionId, ClientSessionId } from "@fluidframework/presence/alpha"; +import type { ClientConnectionId, AttendeeId } from "@fluidframework/presence/alpha"; import type { IExtensionMessage } from "@fluidframework/presence/internal/container-definitions/internal"; /** @@ -39,14 +39,14 @@ export function createInstanceOf(): T { export function generateBasicClientJoin( fixedTime: number, { - clientSessionId = "sessionId-2", + attendeeId = "attendeeId-2", clientConnectionId = "client2", updateProviders = ["client0", "client1", "client3"], connectionOrder = 0, averageLatency = 0, priorClientToSessionId = {}, }: { - clientSessionId?: string; + attendeeId?: string; clientConnectionId?: ClientConnectionId; updateProviders?: string[]; connectionOrder?: number; @@ -68,7 +68,7 @@ export function generateBasicClientJoin( [clientConnectionId]: { "rev": connectionOrder, "timestamp": fixedTime, - "value": clientSessionId, + "value": attendeeId, }, }, }, @@ -84,14 +84,14 @@ export function generateBasicClientJoin( * Prepares an instance of presence as it would be if initialized while connected. * * @param runtime - the mock runtime - * @param clientSessionId - the client session id given to presence + * @param attendeeId - the client session id given to presence * @param clientConnectionId - the client connection id * @param clock - the fake timer. * @param logger - optional logger to track telemetry events */ export function prepareConnectedPresence( runtime: MockEphemeralRuntime, - clientSessionId: string, + attendeeId: string, clientConnectionId: ClientConnectionId, clock: Omit, logger?: EventAndErrorTrackingLogger, @@ -113,13 +113,13 @@ export function prepareConnectedPresence( } const expectedClientJoin = generateBasicClientJoin(clock.now, { - clientSessionId, + attendeeId, clientConnectionId, updateProviders: quorumClientIds, }); runtime.signalsExpected.push([expectedClientJoin.type, expectedClientJoin.content]); - const presence = createPresenceManager(runtime, clientSessionId as ClientSessionId); + const presence = createPresenceManager(runtime, attendeeId as AttendeeId); // Validate expectations post initialization to make sure logger // and runtime are left in a clean expectation state. diff --git a/packages/framework/presence/src/types.ts b/packages/framework/presence/src/types.ts index 52ad1e5b8d9c..8fd4c8492d5b 100644 --- a/packages/framework/presence/src/types.ts +++ b/packages/framework/presence/src/types.ts @@ -23,39 +23,39 @@ import type { NotificationsManager } from "./notificationsManager.js"; * * @alpha */ -export type PresenceWorkspaceAddress = `${string}:${string}`; +export type WorkspaceAddress = `${string}:${string}`; /** - * Single entry in {@link PresenceStatesSchema} or {@link PresenceNotificationsSchema}. + * Single entry in {@link StatesWorkspaceSchema} or {@link NotificationsWorkspaceSchema}. * * @alpha */ -export type PresenceWorkspaceEntry< +export type StatesWorkspaceEntry< TKey extends string, TValue extends InternalTypes.ValueDirectoryOrState, TManager = unknown, > = InternalTypes.ManagerFactory; -// #region PresenceStates +// #region StatesWorkspace /** - * Schema for a {@link PresenceStates} workspace. + * Schema for a {@link StatesWorkspace} workspace. * - * Keys of schema are the keys of the {@link PresenceStates} providing access to `Value Manager`s. + * Keys of schema are the keys of the {@link StatesWorkspace} providing access to `Value Manager`s. * * @alpha */ -export interface PresenceStatesSchema { - [key: string]: PresenceWorkspaceEntry>; +export interface StatesWorkspaceSchema { + [key: string]: StatesWorkspaceEntry>; } /** - * Map of `Value Manager`s registered with {@link PresenceStates}. + * Map of `Value Manager`s registered with {@link StatesWorkspace}. * * @sealed * @alpha */ -export type PresenceStatesEntries = { +export type StatesWorkspaceEntries = { /** * Registered `Value Manager`s */ @@ -67,7 +67,7 @@ export type PresenceStatesEntries = { }; /** - * `PresenceStates` maintains a registry of `Value Manager`s that all share and provide access to + * `StatesWorkspace` maintains a registry of `Value Manager`s that all share and provide access to * presence state values across client members in a session. * * `Value Manager`s offer variations on how to manage states, but all share same principle that @@ -76,12 +76,12 @@ export type PresenceStatesEntries = { * @sealed * @alpha */ -export interface PresenceStates< - TSchema extends PresenceStatesSchema, +export interface StatesWorkspace< + TSchema extends StatesWorkspaceSchema, TManagerConstraints = unknown, > { /** - * Registers a new `Value Manager` with the {@link PresenceStates}. + * Registers a new `Value Manager` with the {@link StatesWorkspace}. * @param key - new unique key for the `Value Manager` within the workspace * @param manager - factory for creating a `Value Manager` */ @@ -92,7 +92,7 @@ export interface PresenceStates< >( key: TKey, manager: InternalTypes.ManagerFactory, - ): asserts this is PresenceStates< + ): asserts this is StatesWorkspace< TSchema & Record>, TManagerConstraints >; @@ -100,7 +100,7 @@ export interface PresenceStates< /** * Registry of `Value Manager`s. */ - readonly props: PresenceStatesEntries; + readonly props: StatesWorkspaceEntries; /** * Default controls for management of broadcast updates. @@ -108,18 +108,18 @@ export interface PresenceStates< readonly controls: BroadcastControls; } -// #endregion PresenceStates +// #endregion StatesWorkspace -// #region PresenceNotifications +// #region NotificationsWorkspace /** - * Schema for a {@link PresenceNotifications} workspace. + * Schema for a {@link NotificationsWorkspace} workspace. * - * Keys of schema are the keys of the {@link PresenceNotifications} providing access to {@link NotificationsManager}s. + * Keys of schema are the keys of the {@link NotificationsWorkspace} providing access to {@link NotificationsManager}s. * * @alpha */ -export interface PresenceNotificationsSchema { +export interface NotificationsWorkspaceSchema { [key: string]: InternalTypes.ManagerFactory< typeof key, InternalTypes.ValueRequiredState, @@ -128,21 +128,21 @@ export interface PresenceNotificationsSchema { } /** - * `PresenceNotifications` maintains a registry of {@link NotificationsManager}s + * `NotificationsWorkspace` maintains a registry of {@link NotificationsManager}s * that facilitate messages across client members in a session. * * @privateRemarks - * This should be kept mostly in sync with {@link PresenceStates}. Notably the + * This should be kept mostly in sync with {@link StatesWorkspace}. Notably the * return type of `add` is limited here and the `controls` property is omitted. - * The `PresenceStatesImpl` class implements `PresenceStates` and therefore - * `PresenceNotifications`, so long as this is proper subset. + * The `PresenceStatesImpl` class implements `StatesWorkspace` and therefore + * `NotificationsWorkspace`, so long as this is proper subset. * * @sealed * @alpha */ -export interface PresenceNotifications { +export interface NotificationsWorkspace { /** - * Registers a new `Value Manager` with the {@link PresenceNotifications}. + * Registers a new `Value Manager` with the {@link NotificationsWorkspace}. * @param key - new unique key for the `Value Manager` within the workspace * @param manager - factory for creating a `Value Manager` */ @@ -153,14 +153,14 @@ export interface PresenceNotifications( key: TKey, manager: InternalTypes.ManagerFactory, - ): asserts this is PresenceNotifications< + ): asserts this is NotificationsWorkspace< TSchema & Record> >; /** * Registry of `Value Manager`s. */ - readonly props: PresenceStatesEntries; + readonly props: StatesWorkspaceEntries; } -// #endregion PresenceNotifications +// #endregion NotificationsWorkspace diff --git a/packages/runtime/test-runtime-utils/src/assertionShortCodesMap.ts b/packages/runtime/test-runtime-utils/src/assertionShortCodesMap.ts index 0caffbeef52a..b4273e0a44d8 100644 --- a/packages/runtime/test-runtime-utils/src/assertionShortCodesMap.ts +++ b/packages/runtime/test-runtime-utils/src/assertionShortCodesMap.ts @@ -1527,7 +1527,7 @@ export const shortCodeMap = { "0xa33": "clientId (from stateHandler) could only be undefined if we've never connected, but we have a CSN so we know that's not the case", "0xa34": "Should have found the batchId in batchIdBySeqNum map", "0xa35": "batchIdsAll and batchIdsBySeqNum should be in sync", - "0xa39": "Container does not support extensions. Use acquirePresenceViaDataObject.", + "0xa39": "Container does not support extensions. Use getPresenceViaDataObject.", "0xa3d": "Partial batch should have exactly one message", "0xa3e": "Empty batch is always considered a full batch", "0xa3f": "segments cannot be undefined", diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts index 9b4dfbb6f3c7..0f95769e327a 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts @@ -15,11 +15,11 @@ import { AttachState } from "@fluidframework/container-definitions"; import { ConnectionState } from "@fluidframework/container-loader"; import { ContainerSchema, type IFluidContainer } from "@fluidframework/fluid-static"; import { - acquirePresenceViaDataObject, + getPresenceViaDataObject, ExperimentalPresenceManager, type ExperimentalPresenceDO, - type IPresence, - type ISessionClient, + type Presence, + type Attendee, // eslint-disable-next-line import/no-internal-modules } from "@fluidframework/presence/alpha"; import { InsecureTokenProvider } from "@fluidframework/test-runtime-utils/internal"; @@ -61,7 +61,7 @@ const getOrCreatePresenceContainer = async ( scopes?: ScopeType[], ): Promise<{ container: IFluidContainer; - presence: IPresence; + presence: Presence; services: AzureContainerServices; client: AzureClient; containerId: string; @@ -107,7 +107,7 @@ const getOrCreatePresenceContainer = async ( "Container is not attached after attach is called", ); - const presence = acquirePresenceViaDataObject( + const presence = getPresenceViaDataObject( container.initialObjects.presence as ExperimentalPresenceDO, ); return { @@ -132,7 +132,7 @@ function isConnected(container: IFluidContainer | undefined): boolean { } class MessageHandler { - public presence: IPresence | undefined; + public presence: Presence | undefined; public container: IFluidContainer | undefined; public containerId: string | undefined; @@ -159,21 +159,21 @@ class MessageHandler { this.containerId = containerId; // Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session. - presence.events.on("attendeeJoined", (attendee: ISessionClient) => { + presence.events.on("attendeeJoined", (attendee: Attendee) => { const m: MessageToParent = { event: "attendeeJoined", - sessionId: attendee.sessionId, + attendeeId: attendee.attendeeId, }; send(m); }); - presence.events.on("attendeeDisconnected", (attendee: ISessionClient) => { + presence.events.on("attendeeDisconnected", (attendee: Attendee) => { const m: MessageToParent = { event: "attendeeDisconnected", - sessionId: attendee.sessionId, + attendeeId: attendee.attendeeId, }; send(m); }); - send({ event: "ready", containerId, sessionId: presence.getMyself().sessionId }); + send({ event: "ready", containerId, attendeeId: presence.getMyself().attendeeId }); break; } @@ -192,7 +192,7 @@ class MessageHandler { this.container.disconnect(); send({ event: "disconnectedSelf", - sessionId: this.presence.getMyself().sessionId, + attendeeId: this.presence.getMyself().attendeeId, }); break; diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts index 2fb975e996b2..a7d55abb688f 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts @@ -5,7 +5,7 @@ import type { AzureUser } from "@fluidframework/azure-client/internal"; // eslint-disable-next-line import/no-internal-modules -import type { ClientSessionId } from "@fluidframework/presence/alpha"; +import type { AttendeeId } from "@fluidframework/presence/alpha"; export type MessageToChild = ConnectCommand | DisconnectSelfCommand; interface ConnectCommand { @@ -26,23 +26,23 @@ export type MessageFromChild = | ErrorEvent; interface AttendeeDisconnectedEvent { event: "attendeeDisconnected"; - sessionId: ClientSessionId; + attendeeId: AttendeeId; } interface AttendeeJoinedEvent { event: "attendeeJoined"; - sessionId: ClientSessionId; + attendeeId: AttendeeId; } interface ReadyEvent { event: "ready"; containerId: string; - sessionId: ClientSessionId; + attendeeId: AttendeeId; } interface DisconnectedSelfEvent { event: "disconnectedSelf"; - sessionId: ClientSessionId; + attendeeId: AttendeeId; } interface ErrorEvent { event: "error"; diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts index 1974e117701b..4a0f185d6d52 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts @@ -63,7 +63,7 @@ describe(`Presence with AzureClient`, () => { (resolve, reject) => { child.once("message", (msg: MessageFromChild) => { if (msg.event === "ready" && msg.containerId) { - containerCreatorSessionId = msg.sessionId; + containerCreatorSessionId = msg.attendeeId; resolve(msg.containerId); } else { reject(new Error(`Non-ready message from child0: ${JSON.stringify(msg)}`)); @@ -152,7 +152,10 @@ describe(`Presence with AzureClient`, () => { timeoutPromise( (resolve) => { child.on("message", (msg: MessageFromChild) => { - if (msg.event === "attendeeDisconnected" && msg.sessionId === creatorSessionId) { + if ( + msg.event === "attendeeDisconnected" && + msg.attendeeId === creatorSessionId + ) { resolve(); } }); From a3b153b3d5c145541c8dbda638d6f5a4a72a98db Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:09:57 +0200 Subject: [PATCH 2/6] refactor(client-presence): value manager renaming (#24300) ## Description Renaming of value manager presence APIs See .changeset/sour-mirrors-wave.md for all changes. Documentation changes were made as well where old name was previously mentioned. Wouldn't want doc changes to block the renames so review here is welcome but they can be cleaned up later. --- .changeset/sour-mirrors-wave.md | 48 ++++-- docs/docs/build/presence.md | 34 ++--- examples/apps/ai-collab/src/app/presence.ts | 4 +- .../apps/presence-tracker/src/FocusTracker.ts | 18 +-- .../apps/presence-tracker/src/MouseTracker.ts | 18 +-- .../apps/presence-tracker/src/reactions.ts | 4 +- .../external-controller/src/presence.ts | 13 +- .../external-controller/src/view.ts | 8 +- packages/framework/presence/README.md | 34 ++--- .../presence/api-report/presence.alpha.api.md | 140 +++++++++--------- .../presence/src/broadcastControls.ts | 2 +- .../presence/src/exposedInternalTypes.ts | 2 +- packages/framework/presence/src/index.ts | 30 ++-- .../framework/presence/src/internalTypes.ts | 2 +- .../presence/src/latestMapValueManager.ts | 84 +++++------ .../presence/src/latestValueManager.ts | 44 +++--- .../presence/src/latestValueTypes.ts | 8 +- .../presence/src/notificationsManager.ts | 2 +- .../framework/presence/src/presenceManager.ts | 2 +- .../framework/presence/src/presenceStates.ts | 6 +- .../framework/presence/src/stateFactory.ts | 23 +++ .../framework/presence/src/systemWorkspace.ts | 2 +- .../presence/src/test/batching.spec.ts | 29 ++-- .../presence/src/test/eventing.spec.ts | 22 +-- .../src/test/latestMapValueManager.spec.ts | 23 +-- .../src/test/latestValueManager.spec.ts | 26 ++-- packages/framework/presence/src/types.ts | 26 ++-- 27 files changed, 349 insertions(+), 305 deletions(-) create mode 100644 packages/framework/presence/src/stateFactory.ts diff --git a/.changeset/sour-mirrors-wave.md b/.changeset/sour-mirrors-wave.md index e9f54649af2d..af5ef8bbe1d5 100644 --- a/.changeset/sour-mirrors-wave.md +++ b/.changeset/sour-mirrors-wave.md @@ -11,6 +11,24 @@ The following API changes have been made to improve clarity and consistency: | Original | New | |----------|-----| +| `acquirePresence` | `getPresence` | +| `acquirePresenceViaDataObject` | `getPresenceViaDataObject` | +| `ClientSessionId` | `AttendeeId` | +| `IPresence` | `Presence` | +| `ISessionClient` | `Attendee` | +| `Latest` (import) | `StateFactory` | +| `Latest` (call) | `StateFactory.latest` | +| `LatestMap` (import) | `StateFactory` | +| `LatestMap` (call) | `StateFactory.latestMap` | +| `LatestMapItemValueClientData` | `LatestMapItemUpdatedClientData` | +| `LatestMapValueClientData` | `LatestMapClientData` | +| `LatestMapValueManager` | `LatestMap` | +| `LatestMapValueManagerEvents` | `LatestMapEvents` | +| `LatestValueClientData` | `LatestClientData` | +| `LatestValueData` | `LatestData` | +| `LatestValueManager` | `Latest` | +| `LatestValueManagerEvents` | `LatestEvents` | +| `LatestValueMetadata` | `LatestMetadata` | | `PresenceNotifications` | `NotificationsWorkspace` | | `PresenceNotificationsSchema` | `NotificationsWorkspaceSchema` | | `PresenceStates` | `StatesWorkspace` | @@ -18,15 +36,27 @@ The following API changes have been made to improve clarity and consistency: | `PresenceStatesSchema` | `StatesWorkspaceSchema` | | `PresenceWorkspaceAddress` | `WorkspaceAddress` | | `PresenceWorkspaceEntry` | `StatesWorkspaceEntry` | -| `ClientSessionId` | `AttendeeId` | -| `IPresence` | `Presence` | -| `ISessionClient` | `Attendee` | | `SessionClientStatus` | `AttendeeStatus` | -| `acquirePresence` | `getPresence` | -| `acquirePresenceViaDataObject` | `getPresenceViaDataObject` | +| `ValueMap` | `StateMap` | ```json { + "acquirePresence": "getPresence", + "acquirePresenceViaDataObject": "getPresenceViaDataObject", + "ClientSessionId": "AttendeeId", + "IPresence": "Presence", + "ISessionClient": "Attendee", + "Latest": "StateFactory", + "LatestMap": "StateFactory", + "LatestMapItemValueClientData": "LatestMapItemUpdatedClientData", + "LatestMapValueClientData": "LatestMapClientData", + "LatestMapValueManager": "LatestMap", + "LatestMapValueManagerEvents": "LatestMapEvents", + "LatestValueClientData": "LatestClientData", + "LatestValueData": "LatestData", + "LatestValueManager": "Latest", + "LatestValueManagerEvents": "LatestEvents", + "LatestValueMetadata": "LatestMetadata", "PresenceNotifications": "NotificationsWorkspace", "PresenceNotificationsSchema": "NotificationsWorkspaceSchema", "PresenceStates": "StatesWorkspace", @@ -34,12 +64,10 @@ The following API changes have been made to improve clarity and consistency: "PresenceStatesSchema": "StatesWorkspaceSchema", "PresenceWorkspaceAddress": "WorkspaceAddress", "PresenceWorkspaceEntry": "StatesWorkspaceEntry", - "ClientSessionId": "AttendeeId", - "IPresence": "Presence", - "ISessionClient": "Attendee", "SessionClientStatus": "AttendeeStatus", - "acquirePresence": "getPresence", - "acquirePresenceViaDataObject": "getPresenceViaDataObject" + "ValueMap": "StateMap" } ``` The JSON table above can be used to automate most of these replacements in your codebase. You can implement a simple script that reads this JSON and performs the necessary replacements in your files. + +Note: To fully replace OLD `Latest` and `LatestMap` functions, you should import `StateFactory` and call `StateFactory.latest` and `StateFactory.latestMap` respectively. NEW `Latest` and `LatestMap` APIs replace `LatestValueManager` and `LatestMapValueManager`. diff --git a/docs/docs/build/presence.md b/docs/docs/build/presence.md index 4d118afc94fc..7dd629e8b253 100644 --- a/docs/docs/build/presence.md +++ b/docs/docs/build/presence.md @@ -35,25 +35,25 @@ There are two types of workspaces: States and Notifications. #### States Workspace -A `StatesWorkspace`, allows sharing of simple data across attendees where each attendee maintains their own data values that others may read, but not change. This is distinct from a Fluid DDS where data values might be manipulated by multiple clients and one ultimate value is derived. Shared, independent values are maintained by value managers that specialize in incrementality and history of values. +A `StatesWorkspace`, allows sharing of simple data across attendees where each attendee maintains their own data values that others may read, but not change. This is distinct from a Fluid DDS where data values might be manipulated by multiple clients and one ultimate value is derived. Shared, independent values are maintained by State objects that specialize in incrementality and history of values. #### Notifications Workspace A `NotificationsWorkspace`, is similar to states workspace, but is dedicated to notification use-cases via `NotificationsManager`. -### Value Managers +### States -#### LatestValueManager +#### Latest -Latest value manager retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `StatesWorkspace`. +`Latest` retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `StatesWorkspace`. -#### LatestMapValueManager +#### LatestMap -Latest map value manager retains the most recent atomic value each attendee has shared under arbitrary keys. Values associated with a key may be nullified (appears as deleted). Use `LatestMap` to add one to `StatesWorkspace`. +`LatestMap` retains the most recent atomic value each attendee has shared under arbitrary keys. Values associated with a key may be nullified (appears as deleted). Use `StateFactory.latest` to add one to `StatesWorkspace`. #### NotificationsManager -Notifications value managers are special case where no data is retained during a session and all interactions appear as events that are sent and received. Notifications value managers may be mixed into a `StatesWorkspace` for convenience. They are the only type of value managers permitted in a `NotificationsWorkspace`. Use `Notifications` to add one to `NotificationsWorkspace` or `StatesWorkspace`. +Notifications are special case where no data is retained during a session and all interactions appear as events that are sent and received. Notifications may be mixed into a `StatesWorkspace` for convenience. `NotificationsManager` is the only presence object permitted in a `NotificationsWorkspace`. Use `Notifications` to add one to `NotificationsWorkspace` or `StatesWorkspace`. ## Onboarding @@ -87,19 +87,19 @@ Current API does not provide a mechanism to validate that state and notification Example: ```typescript -presence.getStates("app:v1states", { myState: Latest({ x: 0 }) }); +presence.getStates("app:v1states", { myState: StateFactory.latest({ x: 0 }) }); ``` is incompatible with ```typescript -presence.getStates("app:v1states", { myState: Latest({ x: "text" }) }); +presence.getStates("app:v1states", { myState: StateFactory.latest({ x: "text" }) }); ``` as "app:v1states"+"myState" have different value type expectations: `{x: number}` versus `{x: string}`. ```typescript -presence.getStates("app:v1states", { myState2: Latest({ x: true }) }); +presence.getStates("app:v1states", { myState2: StateFactory.latest({ x: true }) }); ``` would be compatible with both of the prior schemas as "myState2" is a different name. Though in this situation none of the different clients would be able to observe each other. @@ -114,12 +114,12 @@ Notifications are fundamentally unreliable at this time as there are no built-in Presence updates are grouped together and throttled to prevent flooding the network with messages when presence values are rapidly updated. This means the presence infrastructure will not immediately broadcast updates but will broadcast them after a configurable delay. -The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling grouping with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a [States Workspace](#states-workspace) or [Value Manager](#value-managers) and/or (2) updated later using the `controls` member of Workspace or Value Manager. [States Workspace](#states-workspace) configuration applies when a Value Manager does not have its own setting. +The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling grouping with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a [States Workspace](#states-workspace) or [States](#states) and/or (2) updated later using the `controls` member of Workspace or States. [States Workspace](#states-workspace) configuration applies when States do not have their own setting. Notifications are never queued; they effectively always have an `allowableUpdateLatencyMs` of 0. However, they may be grouped with other updates that were already queued. Note that due to throttling, clients will not receive updates for every intermediate value set by another client. For example, -with `Latest*ValueManagers`, the only value sent is the value at the time the outgoing grouped message is sent. Previous +with `Latest` and `LatestMap`, the only value sent is the value at the time the outgoing grouped message is sent. Previous values set by the client will not be broadcast or seen by other clients. #### Example @@ -131,12 +131,12 @@ You can configure the grouping and throttling behavior using the `allowableUpdat const stateWorkspace = presence.getStates( "app:v1states", { - // This value manager has an allowable latency of 100ms. - position: Latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }), - // This value manager uses the workspace default. - count: Latest({ num: 0 }), + // This Latest state has an allowable latency of 100ms. + position: StateFactory.latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }), + // This Latest state uses the workspace default. + count: StateFactory.latest({ num: 0 }), }, - // Specify the default for all value managers in this workspace to 200ms, + // Specify the default for all state in this workspace to 200ms, // overriding the default value of 60ms. { allowableUpdateLatencyMs: 200 }, ); diff --git a/examples/apps/ai-collab/src/app/presence.ts b/examples/apps/ai-collab/src/app/presence.ts index 9c2e768e2482..c87bc2218a01 100644 --- a/examples/apps/ai-collab/src/app/presence.ts +++ b/examples/apps/ai-collab/src/app/presence.ts @@ -5,7 +5,7 @@ import { Presence, - Latest, + StateFactory, type Attendee, type StatesWorkspace, type StatesWorkspaceSchema, @@ -18,7 +18,7 @@ export interface User { } const statesSchema = { - onlineUsers: Latest({ photo: "" } satisfies User), + onlineUsers: StateFactory.latest({ photo: "" } satisfies User), } satisfies StatesWorkspaceSchema; export type UserPresence = StatesWorkspace; diff --git a/examples/apps/presence-tracker/src/FocusTracker.ts b/examples/apps/presence-tracker/src/FocusTracker.ts index 7f55768fc7a7..82a74ecc7b8e 100644 --- a/examples/apps/presence-tracker/src/FocusTracker.ts +++ b/examples/apps/presence-tracker/src/FocusTracker.ts @@ -7,11 +7,11 @@ import { TypedEventEmitter } from "@fluid-internal/client-utils"; import { IEvent } from "@fluidframework/core-interfaces"; import type { Attendee, - LatestValueManager, + Latest, Presence, StatesWorkspace, } from "@fluidframework/presence/alpha"; -import { Latest, AttendeeStatus } from "@fluidframework/presence/alpha"; +import { AttendeeStatus, StateFactory } from "@fluidframework/presence/alpha"; /** * IFocusState is the data that individual session clients share via presence. @@ -38,9 +38,9 @@ export interface IFocusTrackerEvents extends IEvent { */ export class FocusTracker extends TypedEventEmitter { /** - * A value manager that tracks the latest focus state of connected session clients. + * State that tracks the latest focus state of connected session clients. */ - private readonly focus: LatestValueManager; + private readonly focus: Latest; constructor( private readonly presence: Presence, @@ -53,22 +53,22 @@ export class FocusTracker extends TypedEventEmitter { ) { super(); - // Create a Latest value manager to track the focus state. The value is initialized with current focus state of the + // Create a Latest state object to track the focus state. The value is initialized with current focus state of the // window. statesWorkspace.add( "focus", - Latest({ hasFocus: window.document.hasFocus() }), + StateFactory.latest({ hasFocus: window.document.hasFocus() }), ); - // Save a reference to the value manager for easy access within the FocusTracker. + // Save a reference to the focus state for easy access within the FocusTracker. this.focus = statesWorkspace.props.focus; - // When the focus value manager is updated, the FocusTracker should emit the focusChanged event. + // When the focus state is updated, the FocusTracker should emit the focusChanged event. this.focus.events.on("updated", ({ attendee, value }) => { this.emit("focusChanged", this.focus.local); }); - // Listen to the local focus and blur events. On each event, update the local focus state in the value manager, then + // Listen to the local focus and blur events. On each event, update the local focus state, then // emit the focusChanged event with the local data. window.addEventListener("focus", () => { this.focus.local = { diff --git a/examples/apps/presence-tracker/src/MouseTracker.ts b/examples/apps/presence-tracker/src/MouseTracker.ts index f11e0339a22a..76b86224be79 100644 --- a/examples/apps/presence-tracker/src/MouseTracker.ts +++ b/examples/apps/presence-tracker/src/MouseTracker.ts @@ -8,10 +8,10 @@ import type { IEvent } from "@fluidframework/core-interfaces"; import type { Presence, Attendee, - LatestValueManager, + Latest, StatesWorkspace, } from "@fluidframework/presence/alpha"; -import { Latest, AttendeeStatus } from "@fluidframework/presence/alpha"; +import { AttendeeStatus, StateFactory } from "@fluidframework/presence/alpha"; /** * IMousePosition is the data that individual session clients share via presence. @@ -39,9 +39,9 @@ export interface IMouseTrackerEvents extends IEvent { */ export class MouseTracker extends TypedEventEmitter { /** - * A value manager that tracks the latest mouse position of connected session clients. + * State that tracks the latest mouse position of connected session clients. */ - private readonly cursor: LatestValueManager; + private readonly cursor: Latest; constructor( private readonly presence: Presence, @@ -54,13 +54,13 @@ export class MouseTracker extends TypedEventEmitter { ) { super(); - // Create a Latest value manager to track the mouse position. - statesWorkspace.add("cursor", Latest({ x: 0, y: 0 })); + // Create a Latest state object to track the mouse position. + statesWorkspace.add("cursor", StateFactory.latest({ x: 0, y: 0 })); - // Save a reference to the value manager for easy access within the MouseTracker. + // Save a reference to the cursor state for easy access within the MouseTracker. this.cursor = statesWorkspace.props.cursor; - // When the cursor value manager is updated, the MouseTracker should emit the mousePositionChanged event. + // When the cursor state is updated, the MouseTracker should emit the mousePositionChanged event. this.cursor.events.on("updated", () => { this.emit("mousePositionChanged"); }); @@ -71,7 +71,7 @@ export class MouseTracker extends TypedEventEmitter { this.emit("mousePositionChanged"); }); - // Listen to the local mousemove event and update the local position in the value manager + // Listen to the local mousemove event and update the local position in the cursor state. window.addEventListener("mousemove", (e) => { // Alert all connected clients that there has been a change to this client's mouse position this.cursor.local = { diff --git a/examples/apps/presence-tracker/src/reactions.ts b/examples/apps/presence-tracker/src/reactions.ts index f3c7e8717bc1..eda95223115e 100644 --- a/examples/apps/presence-tracker/src/reactions.ts +++ b/examples/apps/presence-tracker/src/reactions.ts @@ -15,8 +15,8 @@ import type { IMousePosition, MouseTracker } from "./MouseTracker.js"; */ export function initializeReactions(presence: Presence, mouseTracker: MouseTracker) { // Create a notifications workspace to send reactions-related notifications. This workspace will be created if it - // doesn't exist. We also create a Notifications value manager. You can also - // add value managers to the workspace later. + // doesn't exist. We also create a NotificationsManager. You can also + // add presence objects to the workspace later. const notificationsWorkspace = presence.getNotifications( // A unique key identifying this workspace. "name:reactions", diff --git a/examples/service-clients/azure-client/external-controller/src/presence.ts b/examples/service-clients/azure-client/external-controller/src/presence.ts index 7b337fcbd0fd..59dde16b2c5e 100644 --- a/examples/service-clients/azure-client/external-controller/src/presence.ts +++ b/examples/service-clients/azure-client/external-controller/src/presence.ts @@ -5,8 +5,7 @@ import { Presence, - Latest, - LatestMap, + StateFactory, type StatesWorkspace, type StatesWorkspaceSchema, } from "@fluidframework/presence/alpha"; @@ -23,14 +22,12 @@ export interface DiceValues { * the values of two dice as last rolled by clients). No practical application * would need both of these states. * - * The first state, lastRoll, is using the simpler {@link @fluidframework/presence#Latest | Latest} - * pattern (-\> {@link @fluidframework/presence#LatestValueManager | LatestValueManager}) where + * The first state, lastRoll, is using {@link @fluidframework/presence#Latest| Latest} where * all dice values are updated as a whole ({@link DiceValues} structure which has optional values). * If any part of the data is updated, then the entire data structure is shared. This means * keeping a local copy of the data structure or recomposing it each time making an update. * - * The second state, lastDiceRolls, is using the {@link @fluidframework/presence#LatestMap | LatestMap} - * pattern (-\> {@link @fluidframework/presence#LatestMapManager | LatestMapManager}) where + * The second state, lastDiceRolls, is using {@link @fluidframework/presence#LatestMap| LatesttMap} where * each die is updated independently. This allows for more granular updates, but also requires * more verbose setting/reading logic and use of boxed values (e.g. `{ value: DieValue}`). This * pattern more directly lends itself to handling arbitrary numbers of dice. @@ -39,8 +36,8 @@ export interface DiceValues { */ const statesSchema = { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - lastRoll: Latest({} as DiceValues), - lastDiceRolls: LatestMap<{ value: DieValue }, `die${number}`>(), + lastRoll: StateFactory.latest({} as DiceValues), + lastDiceRolls: StateFactory.latestMap<{ value: DieValue }, `die${number}`>(), } satisfies StatesWorkspaceSchema; export type DicePresence = StatesWorkspace; 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 2eb568d883f5..061de09deabb 100644 --- a/examples/service-clients/azure-client/external-controller/src/view.ts +++ b/examples/service-clients/azure-client/external-controller/src/view.ts @@ -4,7 +4,7 @@ */ import { AzureMember, IAzureAudience } from "@fluidframework/azure-client"; -import type { Presence, LatestValueManager } from "@fluidframework/presence/alpha"; +import type { Latest, Presence } from "@fluidframework/presence/alpha"; import { ICustomUserDetails } from "./app.js"; import { IDiceRollerController } from "./controller.js"; @@ -108,7 +108,7 @@ function makeDiceValueElement(id: string, value: DiceValues): HTMLDivElement[] { export function makeDiceValuesView( target: HTMLDivElement, - lastRoll: LatestValueManager, + lastRoll: Latest, ): void { const children = makeDiceHeaderElement(); for (const clientValue of lastRoll.clientValues()) { @@ -125,7 +125,7 @@ function addLogEntry(logDiv: HTMLDivElement, entry: string): void { function makePresenceView( // Biome insist on no semicolon - https://dev.azure.com/fluidframework/internal/_workitems/edit/9083 - presenceConfig?: { presence: Presence; lastRoll: LatestValueManager }, + presenceConfig?: { presence: Presence; lastRoll: Latest }, audience?: IAzureAudience, ): HTMLDivElement { const presenceDiv = document.createElement("div"); @@ -198,7 +198,7 @@ function makePresenceView( export function makeAppView( diceRollerControllers: IDiceRollerController[], // Biome insist on no semicolon - https://dev.azure.com/fluidframework/internal/_workitems/edit/9083 - presenceConfig?: { presence: Presence; lastRoll: LatestValueManager }, + presenceConfig?: { presence: Presence; lastRoll: Latest }, audience?: IAzureAudience, ): HTMLDivElement { const diceRollerViews = diceRollerControllers.map((controller) => diff --git a/packages/framework/presence/README.md b/packages/framework/presence/README.md index 926e887cfabb..94bfd53dbc69 100644 --- a/packages/framework/presence/README.md +++ b/packages/framework/presence/README.md @@ -56,26 +56,26 @@ There are two types of workspaces: States and Notifications. #### States Workspace -A `StatesWorkspace`, allows sharing of simple data across attendees where each attendee maintains their own data values that others may read, but not change. This is distinct from a Fluid DDS where data values might be manipulated by multiple clients and one ultimate value is derived. Shared, independent values are maintained by value managers that specialize in incrementality and history of values. +A `StatesWorkspace`, allows sharing of simple data across attendees where each attendee maintains their own data values that others may read, but not change. This is distinct from a Fluid DDS where data values might be manipulated by multiple clients and one ultimate value is derived. Shared, independent values are maintained by State objects that specialize in incrementality and history of values. #### Notifications Workspace A `NotificationsWorkspace`, is similar to states workspace, but is dedicated to notification use-cases via `NotificationsManager`. -### Value Managers +### States -#### LatestValueManager +#### Latest -Latest value manager retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `StatesWorkspace`. +`Latest` retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `StatesWorkspace`. -#### LatestMapValueManager +#### LatestMap -Latest map value manager retains the most recent atomic value each attendee has shared under arbitrary keys. Values associated with a key may be nullified (appears as deleted). Use `LatestMap` to add one to `StatesWorkspace`. +`LatestMap` retains the most recent atomic value each attendee has shared under arbitrary keys. Values associated with a key may be nullified (appears as deleted). Use `StateFactory.latestMap` to add one to `StatesWorkspace`. #### NotificationsManager -Notifications value managers are special case where no data is retained during a session and all interactions appear as events that are sent and received. Notifications value managers may be mixed into a `StatesWorkspace` for convenience. They are the only type of value managers permitted in a `NotificationsWorkspace`. Use `Notifications` to add one to `NotificationsWorkspace` or `StatesWorkspace`. +Notifications are special case where no data is retained during a session and all interactions appear as events that are sent and received. Notifications may be mixed into a `StatesWorkspace` for convenience. `NotificationsManager` is the only presence object permitted in a `NotificationsWorkspace`. Use `Notifications` to add one to `NotificationsWorkspace` or `StatesWorkspace`. ## Onboarding @@ -108,16 +108,16 @@ Current API does not provide a mechanism to validate that state and notification Example: ```typescript -presence.getStates("app:v1states", { myState: Latest({x: 0})}); +presence.getStates("app:v1states", { myState: StateFactory.latest({x: 0})}); ``` is incompatible with ```typescript -presence.getStates("app:v1states", { myState: Latest({x: "text"})}); +presence.getStates("app:v1states", { myState: StateFactory.latest({x: "text"})}); ``` as "app:v1states"+"myState" have different value type expectations: `{x: number}` versus `{x: string}`. ```typescript -presence.getStates("app:v1states", { myState2: Latest({x: true})}); +presence.getStates("app:v1states", { myState2: StateFactory.latest({x: true})}); ``` would be compatible with both of the prior schemas as "myState2" is a different name. Though in this situation none of the different clients would be able to observe each other. @@ -132,12 +132,12 @@ Notifications are fundamentally unreliable at this time as there are no built-in Presence updates are grouped together and throttled to prevent flooding the network with messages when presence values are rapidly updated. This means the presence infrastructure will not immediately broadcast updates but will broadcast them after a configurable delay. -The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling grouping with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a [States Workspace](#states-workspace) or [Value Manager](#value-managers) and/or (2) updated later using the `controls` member of Workspace or Value Manager. [States Workspace](#states-workspace) configuration applies when a Value Manager does not have its own setting. +The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling grouping with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a [States Workspace](#states-workspace) or [States](#states) and/or (2) updated later using the `controls` member of Workspace or States. [States Workspace](#states-workspace) configuration applies when States do not have their own setting. Notifications are never queued; they effectively always have an `allowableUpdateLatencyMs` of 0. However, they may be grouped with other updates that were already queued. Note that due to throttling, clients receiving updates may not see updates for all values set by another. For example, -with `Latest*ValueManagers`, the only value sent is the value at the time the outgoing grouped message is sent. Previous +with `Latest` and `LatestMap`, the only value sent is the value at the time the outgoing grouped message is sent. Previous values set by the client will not be broadcast or seen by other clients. #### Example @@ -148,12 +148,12 @@ You can configure the grouping and throttling behavior using the `allowableUpdat // Configure a states workspace const stateWorkspace = presence.getStates("app:v1states", { - // This value manager has an allowable latency of 100ms. - position: Latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }), - // This value manager uses the workspace default. - count: Latest({ num: 0 }), + // This Latest state has an allowable latency of 100ms. + position: StateFactory.latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }), + // This Latest state uses the workspace default. + count: StateFactory.latest({ num: 0 }), }, - // Specify the default for all value managers in this workspace to 200ms, + // Specify the default for all state in this workspace to 200ms, // overriding the default value of 60ms. { allowableUpdateLatencyMs: 200 } ); diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 1e5288762699..4c0bda758472 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -127,53 +127,72 @@ export namespace InternalUtilityTypes { }; } -// @alpha -export function Latest(initialValue: JsonSerializable & JsonDeserialized & object, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, LatestValueManager>; +// @alpha @sealed +export interface Latest { + clients(): Attendee[]; + clientValue(attendee: Attendee): LatestData; + clientValues(): IterableIterator>; + readonly controls: BroadcastControls; + readonly events: Listenable>; + get local(): InternalUtilityTypes.FullyReadonly>; + set local(value: JsonSerializable & JsonDeserialized); +} // @alpha -export function LatestMap(initialValues?: { - [K in Keys]: JsonSerializable & JsonDeserialized; -}, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, LatestMapValueManager>; +export function latest(initialValue: JsonSerializable & JsonDeserialized & object, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, Latest>; // @alpha @sealed -export interface LatestMapItemRemovedClientData { +export interface LatestClientData extends LatestData { // (undocumented) attendee: Attendee; - // (undocumented) - key: K; - // (undocumented) - metadata: LatestValueMetadata; } // @alpha @sealed -export interface LatestMapItemValueClientData extends LatestValueClientData { +export interface LatestData { // (undocumented) - key: K; + metadata: LatestMetadata; + // (undocumented) + value: InternalUtilityTypes.FullyReadonly>; } -// @alpha @sealed -export interface LatestMapValueClientData { - attendee: Attendee; - // (undocumented) - items: ReadonlyMap>; +// @alpha @sealed (undocumented) +export interface LatestEvents { + // @eventProperty + localUpdated: (update: { + value: InternalUtilityTypes.FullyReadonly & JsonDeserialized>; + }) => void; + // @eventProperty + updated: (update: LatestClientData) => void; } // @alpha @sealed -export interface LatestMapValueManager { +export interface LatestMap { clients(): Attendee[]; - clientValue(attendee: Attendee): ReadonlyMap>; - clientValues(): IterableIterator>; + clientValue(attendee: Attendee): ReadonlyMap>; + clientValues(): IterableIterator>; readonly controls: BroadcastControls; - readonly events: Listenable>; - readonly local: ValueMap; + readonly events: Listenable>; + readonly local: StateMap; +} + +// @alpha +export function latestMap(initialValues?: { + [K in Keys]: JsonSerializable & JsonDeserialized; +}, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, LatestMap>; + +// @alpha @sealed +export interface LatestMapClientData { + attendee: Attendee; + // (undocumented) + items: ReadonlyMap>; } // @alpha @sealed (undocumented) -export interface LatestMapValueManagerEvents { +export interface LatestMapEvents { // @eventProperty itemRemoved: (removedItem: LatestMapItemRemovedClientData) => void; // @eventProperty - itemUpdated: (updatedItem: LatestMapItemValueClientData) => void; + itemUpdated: (updatedItem: LatestMapItemUpdatedClientData) => void; // @eventProperty localItemRemoved: (removedItem: { key: K; @@ -184,46 +203,27 @@ export interface LatestMapValueManagerEvents { key: K; }) => void; // @eventProperty - updated: (updates: LatestMapValueClientData) => void; + updated: (updates: LatestMapClientData) => void; } // @alpha @sealed -export interface LatestValueClientData extends LatestValueData { +export interface LatestMapItemRemovedClientData { // (undocumented) attendee: Attendee; -} - -// @alpha @sealed -export interface LatestValueData { // (undocumented) - metadata: LatestValueMetadata; + key: K; // (undocumented) - value: InternalUtilityTypes.FullyReadonly>; + metadata: LatestMetadata; } // @alpha @sealed -export interface LatestValueManager { - clients(): Attendee[]; - clientValue(attendee: Attendee): LatestValueData; - clientValues(): IterableIterator>; - readonly controls: BroadcastControls; - readonly events: Listenable>; - get local(): InternalUtilityTypes.FullyReadonly>; - set local(value: JsonSerializable & JsonDeserialized); -} - -// @alpha @sealed (undocumented) -export interface LatestValueManagerEvents { - // @eventProperty - localUpdated: (update: { - value: InternalUtilityTypes.FullyReadonly & JsonDeserialized>; - }) => void; - // @eventProperty - updated: (update: LatestValueClientData) => void; +export interface LatestMapItemUpdatedClientData extends LatestClientData { + // (undocumented) + key: K; } // @alpha @sealed -export interface LatestValueMetadata { +export interface LatestMetadata { revision: number; timestamp: number; } @@ -292,6 +292,27 @@ export interface PresenceEvents { workspaceActivated: (workspaceAddress: WorkspaceAddress, type: "States" | "Notifications" | "Unknown") => void; } +// @alpha +export const StateFactory: { + latest: typeof latest; + latestMap: typeof latestMap; +}; + +// @alpha @sealed +export interface StateMap { + clear(): void; + // (undocumented) + delete(key: K): boolean; + forEach(callbackfn: (value: InternalUtilityTypes.FullyReadonly>, key: K, map: StateMap) => void, thisArg?: unknown): void; + get(key: K): InternalUtilityTypes.FullyReadonly> | undefined; + // (undocumented) + has(key: K): boolean; + keys(): IterableIterator; + set(key: K, value: JsonSerializable & JsonDeserialized): this; + // (undocumented) + readonly size: number; +} + // @alpha @sealed export interface StatesWorkspace { add, TManager extends TManagerConstraints>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is StatesWorkspace>, TManagerConstraints>; @@ -302,7 +323,7 @@ export interface StatesWorkspace = { /** - * Registered `Value Manager`s + * Registered State objects. */ readonly [Key in keyof TSchema]: ReturnType["manager"] extends InternalTypes.StateValue ? TManager : never; }; @@ -316,21 +337,6 @@ export interface StatesWorkspaceSchema { [key: string]: StatesWorkspaceEntry>; } -// @alpha @sealed -export interface ValueMap { - clear(): void; - // (undocumented) - delete(key: K): boolean; - forEach(callbackfn: (value: InternalUtilityTypes.FullyReadonly>, key: K, map: ValueMap) => void, thisArg?: unknown): void; - get(key: K): InternalUtilityTypes.FullyReadonly> | undefined; - // (undocumented) - has(key: K): boolean; - keys(): IterableIterator; - set(key: K, value: JsonSerializable & JsonDeserialized): this; - // (undocumented) - readonly size: number; -} - // @alpha export type WorkspaceAddress = `${string}:${string}`; diff --git a/packages/framework/presence/src/broadcastControls.ts b/packages/framework/presence/src/broadcastControls.ts index c212f7f0f078..aec305addab3 100644 --- a/packages/framework/presence/src/broadcastControls.ts +++ b/packages/framework/presence/src/broadcastControls.ts @@ -4,7 +4,7 @@ */ /** - * Common controls for Value Managers. + * Common controls for States objects. * * @sealed * @alpha diff --git a/packages/framework/presence/src/exposedInternalTypes.ts b/packages/framework/presence/src/exposedInternalTypes.ts index 5e9ab0a3575e..6d86d9b2f71f 100644 --- a/packages/framework/presence/src/exposedInternalTypes.ts +++ b/packages/framework/presence/src/exposedInternalTypes.ts @@ -100,7 +100,7 @@ export namespace InternalTypes { export type StateValue = T & StateValueBrand; /** - * Package internal function declaration for value manager instantiation. + * Package internal function declaration for state and notification instantiation. * * @system */ diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index bbd668094b6b..32ad8cd05e53 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -26,9 +26,9 @@ export type { export { type Attendee, type AttendeeId, + AttendeeStatus, type Presence, type PresenceEvents, - AttendeeStatus, } from "./presence.js"; export type { @@ -44,24 +44,24 @@ export { ExperimentalPresenceManager, } from "./datastorePresenceManagerFactory.js"; -export { +export type { + latestMap, LatestMap, - type LatestMapItemRemovedClientData, - type LatestMapItemValueClientData, - type LatestMapValueClientData, - type LatestMapValueManager, - type LatestMapValueManagerEvents, - type ValueMap, + LatestMapClientData, + LatestMapEvents, + LatestMapItemRemovedClientData, + LatestMapItemUpdatedClientData, + StateMap, } from "./latestMapValueManager.js"; -export { +export type { + latest, Latest, - type LatestValueManager, - type LatestValueManagerEvents, + LatestEvents, } from "./latestValueManager.js"; export type { - LatestValueClientData, - LatestValueData, - LatestValueMetadata, + LatestClientData, + LatestData, + LatestMetadata, } from "./latestValueTypes.js"; export { @@ -73,5 +73,7 @@ export { type NotificationsManagerEvents, } from "./notificationsManager.js"; +export { StateFactory } from "./stateFactory.js"; + export type { InternalTypes } from "./exposedInternalTypes.js"; export type { InternalUtilityTypes } from "./exposedUtilityTypes.js"; diff --git a/packages/framework/presence/src/internalTypes.ts b/packages/framework/presence/src/internalTypes.ts index a0cd85184229..5af893f7695b 100644 --- a/packages/framework/presence/src/internalTypes.ts +++ b/packages/framework/presence/src/internalTypes.ts @@ -44,7 +44,7 @@ export interface ValueManager< TValueState extends InternalTypes.ValueDirectoryOrState = InternalTypes.ValueDirectoryOrState, > { - // Most value managers should provide value - implement Required> + // State objects should provide value - implement Required> readonly value?: TValueState; update(attendee: Attendee, received: number, value: TValueState): PostUpdateAction[]; } diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index e12bee99b7ff..d67bd3ad8ff9 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -17,11 +17,7 @@ import type { InternalTypes } from "./exposedInternalTypes.js"; import type { InternalUtilityTypes } from "./exposedUtilityTypes.js"; import type { PostUpdateAction, ValueManager } from "./internalTypes.js"; import { objectEntries, objectKeys } from "./internalUtils.js"; -import type { - LatestValueClientData, - LatestValueData, - LatestValueMetadata, -} from "./latestValueTypes.js"; +import type { LatestClientData, LatestData, LatestMetadata } from "./latestValueTypes.js"; import type { AttendeeId, Attendee, SpecificAttendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -32,7 +28,7 @@ import { brandIVM } from "./valueManager.js"; * @sealed * @alpha */ -export interface LatestMapValueClientData< +export interface LatestMapClientData< T, Keys extends string | number, SpecificAttendeeId extends AttendeeId = AttendeeId, @@ -46,7 +42,7 @@ export interface LatestMapValueClientData< * @privateRemarks This could be regular map currently as no Map is * stored internally and a new instance is created for every request. */ - items: ReadonlyMap>; + items: ReadonlyMap>; } /** @@ -55,8 +51,8 @@ export interface LatestMapValueClientData< * @sealed * @alpha */ -export interface LatestMapItemValueClientData - extends LatestValueClientData { +export interface LatestMapItemUpdatedClientData + extends LatestClientData { key: K; } @@ -69,14 +65,14 @@ export interface LatestMapItemValueClientData export interface LatestMapItemRemovedClientData { attendee: Attendee; key: K; - metadata: LatestValueMetadata; + metadata: LatestMetadata; } /** * @sealed * @alpha */ -export interface LatestMapValueManagerEvents { +export interface LatestMapEvents { /** * Raised when any item's value for remote client is updated. * @param updates - Map of one or more values updated. @@ -85,7 +81,7 @@ export interface LatestMapValueManagerEvents { * * @eventProperty */ - updated: (updates: LatestMapValueClientData) => void; + updated: (updates: LatestMapClientData) => void; /** * Raised when specific item's value of remote client is updated. @@ -93,7 +89,7 @@ export interface LatestMapValueManagerEvents { * * @eventProperty */ - itemUpdated: (updatedItem: LatestMapItemValueClientData) => void; + itemUpdated: (updatedItem: LatestMapItemUpdatedClientData) => void; /** * Raised when specific item of remote client is removed. @@ -131,15 +127,15 @@ export interface LatestMapValueManagerEvents { * @sealed * @alpha */ -export interface ValueMap { +export interface StateMap { /** - * ${@link ValueMap.delete}s all elements in the ValueMap. + * ${@link StateMap.delete}s all elements in the StateMap. * @remarks This is not yet implemented. */ clear(): void; /** - * @returns true if an element in the ValueMap existed and has been removed, or false if + * @returns true if an element in the StateMap existed and has been removed, or false if * the element does not exist. * @remarks No entry is fully removed. Instead an undefined placeholder is locally and * transmitted to all other clients. For better performance limit the number of deleted @@ -150,19 +146,19 @@ export interface ValueMap { delete(key: K): boolean; /** - * Executes a provided function once per each key/value pair in the ValueMap, in arbitrary order. + * Executes a provided function once per each key/value pair in the StateMap, in arbitrary order. */ forEach( callbackfn: ( value: InternalUtilityTypes.FullyReadonly>, key: K, - map: ValueMap, + map: StateMap, ) => void, thisArg?: unknown, ): void; /** - * Returns a specified element from the ValueMap object. + * Returns a specified element from the StateMap object. * @returns Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned. */ get(key: K): InternalUtilityTypes.FullyReadonly> | undefined; @@ -173,7 +169,7 @@ export interface ValueMap { has(key: K): boolean; /** - * Adds a new element with a specified key and value to the ValueMap. If an element with the same key already exists, the element will be updated. + * Adds a new element with a specified key and value to the StateMap. If an element with the same key already exists, the element will be updated. * The value will be transmitted to all other connected clients. * * @remarks Manager assumes ownership of the value and its references. @@ -183,7 +179,7 @@ export interface ValueMap { set(key: K, value: JsonSerializable & JsonDeserialized): this; /** - * @returns the number of elements in the ValueMap. + * @returns the number of elements in the StateMap. */ readonly size: number; @@ -208,12 +204,12 @@ export interface ValueMap { // values(): IterableIterator>>; } -class ValueMapImpl implements ValueMap { +class ValueMapImpl implements StateMap { private countDefined: number; public constructor( private readonly value: InternalTypes.MapValueState, private readonly emitter: IEmitter< - Pick, "localItemUpdated" | "localItemRemoved"> + Pick, "localItemUpdated" | "localItemRemoved"> >, private readonly localUpdate: ( updates: InternalTypes.MapValueState< @@ -264,7 +260,7 @@ class ValueMapImpl implements ValueMap { callbackfn: ( value: InternalUtilityTypes.FullyReadonly>, key: K, - map: ValueMap, + map: StateMap, ) => void, thisArg?: unknown, ): void { @@ -304,21 +300,21 @@ class ValueMapImpl implements ValueMap { } /** - * Value manager that provides a `Map` of latest known values from this client to + * State that provides a `Map` of latest known values from this client to * others and read access to their values. * Entries in the map may vary over time and by client, but all values are expected to * be of the same type, which may be a union type. * - * @remarks Create using {@link LatestMap} registered to {@link StatesWorkspace}. + * @remarks Create using {@link StateFactory.latestMap} registered to {@link StatesWorkspace}. * * @sealed * @alpha */ -export interface LatestMapValueManager { +export interface LatestMap { /** - * Events for LatestMap value manager. + * Events for LatestMap. */ - readonly events: Listenable>; + readonly events: Listenable>; /** * Controls for management of sending updates. @@ -328,11 +324,11 @@ export interface LatestMapValueManager; + readonly local: StateMap; /** * Iterable access to remote clients' map of values. */ - clientValues(): IterableIterator>; + clientValues(): IterableIterator>; /** * Array of known remote clients. */ @@ -340,7 +336,7 @@ export interface LatestMapValueManager>; + clientValue(attendee: Attendee): ReadonlyMap>; } class LatestMapValueManagerImpl< @@ -348,10 +344,10 @@ class LatestMapValueManagerImpl< RegistrationKey extends string, Keys extends string | number = string | number, > implements - LatestMapValueManager, + LatestMap, Required>> { - public readonly events = createEmitter>(); + public readonly events = createEmitter>(); public readonly controls: OptionalBroadcastControl; public constructor( @@ -376,9 +372,9 @@ class LatestMapValueManagerImpl< ); } - public readonly local: ValueMap; + public readonly local: StateMap; - public *clientValues(): IterableIterator> { + public *clientValues(): IterableIterator> { const allKnownStates = this.datastore.knownValues(this.key); for (const attendeeId of objectKeys(allKnownStates.states)) { if (attendeeId !== allKnownStates.self) { @@ -396,14 +392,14 @@ class LatestMapValueManagerImpl< .map((attendeeId) => this.datastore.lookupClient(attendeeId)); } - public clientValue(attendee: Attendee): ReadonlyMap> { + public clientValue(attendee: Attendee): ReadonlyMap> { const allKnownStates = this.datastore.knownValues(this.key); const attendeeId = attendee.attendeeId; const clientStateMap = allKnownStates.states[attendeeId]; if (clientStateMap === undefined) { throw new Error("No entry for attendee"); } - const items = new Map>(); + const items = new Map>(); for (const [key, item] of objectEntries(clientStateMap.items)) { const value = item.value; if (value !== undefined) { @@ -449,7 +445,7 @@ class LatestMapValueManagerImpl< } const allUpdates = { attendee, - items: new Map>(), + items: new Map>(), }; const postUpdateActions: PostUpdateAction[] = []; for (const key of updatedItemKeys) { @@ -485,11 +481,11 @@ class LatestMapValueManagerImpl< } /** - * Factory for creating a {@link LatestMapValueManager}. + * Factory for creating a {@link LatestMap} State object. * * @alpha */ -export function LatestMap< +export function latestMap< T extends object, Keys extends string | number = string | number, RegistrationKey extends string = string, @@ -501,7 +497,7 @@ export function LatestMap< ): InternalTypes.ManagerFactory< RegistrationKey, InternalTypes.MapValueState, - LatestMapValueManager + LatestMap > { const timestamp = Date.now(); const value: InternalTypes.MapValueState< @@ -509,7 +505,7 @@ export function LatestMap< // This should be `Keys`, but will only work if properties are optional. string | number > = { rev: 0, items: {} }; - // LatestMapValueManager takes ownership of values within initialValues. + // LatestMap takes ownership of values within initialValues. if (initialValues !== undefined) { for (const key of objectKeys(initialValues)) { value.items[key] = { rev: 0, timestamp, value: initialValues[key] }; @@ -523,7 +519,7 @@ export function LatestMap< >, ): { initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; - manager: InternalTypes.StateValue>; + manager: InternalTypes.StateValue>; } => ({ initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs }, manager: brandIVM< diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 9ff23d7217d8..85b68de9ae45 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -17,7 +17,7 @@ import type { InternalTypes } from "./exposedInternalTypes.js"; import type { InternalUtilityTypes } from "./exposedUtilityTypes.js"; import type { PostUpdateAction, ValueManager } from "./internalTypes.js"; import { objectEntries } from "./internalUtils.js"; -import type { LatestValueClientData, LatestValueData } from "./latestValueTypes.js"; +import type { LatestClientData, LatestData } from "./latestValueTypes.js"; import type { Attendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -26,13 +26,13 @@ import { brandIVM } from "./valueManager.js"; * @sealed * @alpha */ -export interface LatestValueManagerEvents { +export interface LatestEvents { /** * Raised when remote client's value is updated, which may be the same value. * * @eventProperty */ - updated: (update: LatestValueClientData) => void; + updated: (update: LatestClientData) => void; /** * Raised when local client's value is updated, which may be the same value. @@ -45,19 +45,19 @@ export interface LatestValueManagerEvents { } /** - * Value manager that provides the latest known value from this client to others and read access to their values. + * State that provides the latest known value from this client to others and read access to their values. * All participant clients must provide a value. * - * @remarks Create using {@link Latest} registered to {@link StatesWorkspace}. + * @remarks Create using {@link StateFactory.latest} registered to {@link StatesWorkspace}. * * @sealed * @alpha */ -export interface LatestValueManager { +export interface Latest { /** - * Events for Latest value manager. + * Events for Latest. */ - readonly events: Listenable>; + readonly events: Listenable>; /** * Controls for management of sending updates. @@ -76,7 +76,7 @@ export interface LatestValueManager { /** * Iterable access to remote clients' values. */ - clientValues(): IterableIterator>; + clientValues(): IterableIterator>; /** * Array of known remote clients. */ @@ -84,15 +84,13 @@ export interface LatestValueManager { /** * Access to a specific attendee's value. */ - clientValue(attendee: Attendee): LatestValueData; + clientValue(attendee: Attendee): LatestData; } class LatestValueManagerImpl - implements - LatestValueManager, - Required>> + implements Latest, Required>> { - public readonly events = createEmitter>(); + public readonly events = createEmitter>(); public readonly controls: OptionalBroadcastControl; public constructor( @@ -119,7 +117,7 @@ class LatestValueManagerImpl this.events.emit("localUpdated", { value }); } - public *clientValues(): IterableIterator> { + public *clientValues(): IterableIterator> { const allKnownStates = this.datastore.knownValues(this.key); for (const [attendeeId, value] of objectEntries(allKnownStates.states)) { if (attendeeId !== allKnownStates.self) { @@ -139,7 +137,7 @@ class LatestValueManagerImpl .map((attendeeId) => this.datastore.lookupClient(attendeeId)); } - public clientValue(attendee: Attendee): LatestValueData { + public clientValue(attendee: Attendee): LatestData { const allKnownStates = this.datastore.knownValues(this.key); const clientState = allKnownStates.states[attendee.attendeeId]; if (clientState === undefined) { @@ -175,19 +173,15 @@ class LatestValueManagerImpl } /** - * Factory for creating a {@link LatestValueManager}. + * Factory for creating a {@link Latest} State object. * * @alpha */ -export function Latest( +export function latest( initialValue: JsonSerializable & JsonDeserialized & object, controls?: BroadcastControlSettings, -): InternalTypes.ManagerFactory< - Key, - InternalTypes.ValueRequiredState, - LatestValueManager -> { - // LatestValueManager takes ownership of initialValue but makes a shallow +): InternalTypes.ManagerFactory, Latest> { + // Latest takes ownership of initialValue but makes a shallow // copy for basic protection. const value: InternalTypes.ValueRequiredState = { rev: 0, @@ -202,7 +196,7 @@ export function Latest( >, ): { initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; - manager: InternalTypes.StateValue>; + manager: InternalTypes.StateValue>; } => ({ initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs }, manager: brandIVM, T, InternalTypes.ValueRequiredState>( diff --git a/packages/framework/presence/src/latestValueTypes.ts b/packages/framework/presence/src/latestValueTypes.ts index 4b3dea1d7f29..0328eb3c0bc1 100644 --- a/packages/framework/presence/src/latestValueTypes.ts +++ b/packages/framework/presence/src/latestValueTypes.ts @@ -14,7 +14,7 @@ import type { Attendee } from "./presence.js"; * @sealed * @alpha */ -export interface LatestValueMetadata { +export interface LatestMetadata { /** * The revision number for value that increases as value is changed. */ @@ -32,9 +32,9 @@ export interface LatestValueMetadata { * @sealed * @alpha */ -export interface LatestValueData { +export interface LatestData { value: InternalUtilityTypes.FullyReadonly>; - metadata: LatestValueMetadata; + metadata: LatestMetadata; } /** @@ -43,6 +43,6 @@ export interface LatestValueData { * @sealed * @alpha */ -export interface LatestValueClientData extends LatestValueData { +export interface LatestClientData extends LatestData { attendee: Attendee; } diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index 4ae077ab09bc..7fc38e8d0a18 100644 --- a/packages/framework/presence/src/notificationsManager.ts +++ b/packages/framework/presence/src/notificationsManager.ts @@ -119,7 +119,7 @@ export interface NotificationEmitter } this.nodes = nodes; // props is the public view of nodes that limits the entries types to - // the public interface of the value manager with an additional type + // the public interface of the State object with an additional type // filter that beguiles the type system. So just reinterpret cast. this.props = this.nodes as unknown as StatesWorkspace["props"]; @@ -397,7 +397,7 @@ class PresenceStatesImpl } else { const node = unbrandIVM(brandedIVM); if (!(node instanceof nodeFactory.instanceBase)) { - throw new TypeError(`State "${key}" previously created by different value manager.`); + throw new TypeError(`State "${key}" previously created by different State object.`); } } } @@ -429,7 +429,7 @@ class PresenceStatesImpl /** * Create a new StatesWorkspace using the DataStoreRuntime provided. - * @param initialContent - The initial value managers to register. + * @param initialContent - The initial State objects to register. */ export function createPresenceStates( runtime: PresenceRuntime, diff --git a/packages/framework/presence/src/stateFactory.ts b/packages/framework/presence/src/stateFactory.ts new file mode 100644 index 000000000000..bb2d1101178d --- /dev/null +++ b/packages/framework/presence/src/stateFactory.ts @@ -0,0 +1,23 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { latestMap } from "./latestMapValueManager.js"; +import { latest } from "./latestValueManager.js"; + +/** + * Factory for creating presence State objects. + * + * @alpha + */ +export const StateFactory = { + /** + * {@inheritdoc latest} + */ + latest, + /** + * {@inheritdoc latestMap} + */ + latestMap, +}; diff --git a/packages/framework/presence/src/systemWorkspace.ts b/packages/framework/presence/src/systemWorkspace.ts index 0645153664a7..5fa474ba5b1f 100644 --- a/packages/framework/presence/src/systemWorkspace.ts +++ b/packages/framework/presence/src/systemWorkspace.ts @@ -10,7 +10,7 @@ import { assert } from "@fluidframework/core-utils/internal"; import type { ClientConnectionId } from "./baseTypes.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; import type { PostUpdateAction } from "./internalTypes.js"; -import type { Attendee, PresenceEvents, AttendeeId, Presence } from "./presence.js"; +import type { Attendee, AttendeeId, Presence, PresenceEvents } from "./presence.js"; import { AttendeeStatus } from "./presence.js"; import type { PresenceStatesInternal } from "./presenceStates.js"; import { TimerManager } from "./timerManager.js"; diff --git a/packages/framework/presence/src/test/batching.spec.ts b/packages/framework/presence/src/test/batching.spec.ts index 63e378640d1f..76b5745994f1 100644 --- a/packages/framework/presence/src/test/batching.spec.ts +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -7,7 +7,8 @@ import { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal import { describe, it, after, afterEach, before, beforeEach } from "mocha"; import { useFakeTimers, type SinonFakeTimers } from "sinon"; -import { Latest, Notifications, type NotificationsWorkspace } from "../index.js"; +import type { NotificationsWorkspace } from "../index.js"; +import { Notifications, StateFactory } from "../index.js"; import type { createPresenceManager } from "../presenceManager.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; @@ -48,7 +49,7 @@ describe("Presence", () => { clock.restore(); }); - describe("LatestValueManager", () => { + describe("Latest", () => { it("sends signal immediately when allowable latency is 0", async () => { runtime.signalsExpected.push( [ @@ -143,7 +144,7 @@ describe("Presence", () => { // Configure a state workspace // SIGNAL #1 - intial data is sent immediately const stateWorkspace = presence.getStates("name:testStateWorkspace", { - count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), + count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), }); const { count } = stateWorkspace.props; @@ -192,7 +193,7 @@ describe("Presence", () => { // Configure a state workspace presence.getStates("name:testStateWorkspace", { - count: Latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), + count: StateFactory.latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), }); // will be queued; deadline is now 1070 // SIGNAL #1 @@ -266,7 +267,7 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.getStates("name:testStateWorkspace", { - count: Latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), + count: StateFactory.latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), }); // will be queued; deadline is now 1070 const { count } = stateWorkspace.props; @@ -368,7 +369,7 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.getStates("name:testStateWorkspace", { - count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), }); const { count } = stateWorkspace.props; @@ -484,11 +485,11 @@ describe("Presence", () => { ); // Configure a state workspace - // SIGNAL #1 - this signal is not queued because it contains a value manager with a latency of 0, + // SIGNAL #1 - this signal is not queued because it contains a State object with a latency of 0, // so the initial data will be sent immediately. const stateWorkspace = presence.getStates("name:testStateWorkspace", { - count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), - immediateUpdate: Latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), + count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + immediateUpdate: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), }); const { count, immediateUpdate } = stateWorkspace.props; @@ -574,8 +575,8 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.getStates("name:testStateWorkspace", { - count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), - note: Latest({ message: "" }, { allowableUpdateLatencyMs: 50 }), + count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + note: StateFactory.latest({ message: "" }, { allowableUpdateLatencyMs: 50 }), }); // will be queued, deadline is set to 1060 const { count, note } = stateWorkspace.props; @@ -646,11 +647,11 @@ describe("Presence", () => { // Configure two state workspaces const stateWorkspace = presence.getStates("name:testStateWorkspace", { - count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), }); // will be queued, deadline is 1110 const stateWorkspace2 = presence.getStates("name:testStateWorkspace2", { - note: Latest({ message: "" }, { allowableUpdateLatencyMs: 60 }), + note: StateFactory.latest({ message: "" }, { allowableUpdateLatencyMs: 60 }), }); // will be queued, deadline is 1070 const { count } = stateWorkspace.props; @@ -841,7 +842,7 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.getStates("name:testStateWorkspace", { - count: Latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), }); // will be queued, deadline is 1110 // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/packages/framework/presence/src/test/eventing.spec.ts b/packages/framework/presence/src/test/eventing.spec.ts index 4868d6def6b4..5048ddde3c0a 100644 --- a/packages/framework/presence/src/test/eventing.spec.ts +++ b/packages/framework/presence/src/test/eventing.spec.ts @@ -14,14 +14,8 @@ import type { Attendee, WorkspaceAddress } from "../index.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; import { assertFinalExpectations, prepareConnectedPresence } from "./testUtils.js"; -import { - Latest, - LatestMap, - Notifications, - type LatestMapValueManager, - type LatestValueManager, - type NotificationsManager, -} from "@fluidframework/presence/alpha"; +import type { Latest, LatestMap, NotificationsManager } from "@fluidframework/presence/alpha"; +import { Notifications, StateFactory } from "@fluidframework/presence/alpha"; const datastoreUpdateType = "Pres:DatastoreUpdate"; @@ -144,8 +138,8 @@ describe("Presence", () => { let logger: EventAndErrorTrackingLogger; let clock: SinonFakeTimers; let presence: ReturnType; - let latest: LatestValueManager<{ x: number; y: number; z: number }>; - let latestMap: LatestMapValueManager<{ a: number; b: number } | { c: number; d: number }>; + let latest: Latest<{ x: number; y: number; z: number }>; + let latestMap: LatestMap<{ a: number; b: number } | { c: number; d: number }>; let notificationManager: NotificationsManager<{ newId: (id: number) => void }>; interface LatestMapValueExpected { @@ -248,8 +242,8 @@ describe("Presence", () => { notifications, }: { notifications?: true } = {}): void { const states = presence.getStates("name:testWorkspace", { - latest: Latest({ x: 0, y: 0, z: 0 }), - latestMap: LatestMap({ key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }), + latest: StateFactory.latest({ x: 0, y: 0, z: 0 }), + latestMap: StateFactory.latestMap({ key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }), }); latest = states.props.latest; latestMap = states.props.latestMap; @@ -267,10 +261,10 @@ describe("Presence", () => { function setupMultipleStatesWorkspaces(): void { const latestsStates = presence.getStates("name:testWorkspace1", { - latest: Latest({ x: 0, y: 0, z: 0 }), + latest: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); const latesetMapStates = presence.getStates("name:testWorkspace2", { - latestMap: LatestMap({ key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }), + latestMap: StateFactory.latestMap({ key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }), }); latest = latestsStates.props.latest; latestMap = latesetMapStates.props.latestMap; diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index 758874925695..ef9693cca6f0 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -12,11 +12,11 @@ import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; import type { BroadcastControlSettings, + LatestMap, + LatestMapItemUpdatedClientData, Presence, - LatestMapItemValueClientData, - LatestMapValueManager, } from "@fluidframework/presence/alpha"; -import { LatestMap } from "@fluidframework/presence/alpha"; +import { StateFactory } from "@fluidframework/presence/alpha"; const testWorkspaceName = "name:testWorkspaceA"; @@ -26,7 +26,7 @@ function createLatestMapManager( valueControlSettings?: BroadcastControlSettings, ) { const states = presence.getStates(testWorkspaceName, { - fixedMap: LatestMap( + fixedMap: StateFactory.latestMap( { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, valueControlSettings, ), @@ -35,7 +35,7 @@ function createLatestMapManager( } describe("Presence", () => { - describe("LatestMapValueManager", () => { + describe("LatestMap", () => { /** * See {@link checkCompiles} below */ @@ -43,7 +43,7 @@ describe("Presence", () => { addControlsTests(createLatestMapManager); - function setupMapValueManager(): LatestMapValueManager< + function setupMapValueManager(): LatestMap< { x: number; y: number; @@ -52,7 +52,7 @@ describe("Presence", () => { > { const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.getStates(testWorkspaceName, { - fixedMap: LatestMap({ key1: { x: 0, y: 0 } }), + fixedMap: StateFactory.latestMap({ key1: { x: 0, y: 0 } }), }); return states.props.fixedMap; } @@ -99,7 +99,10 @@ export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const presence = {} as Presence; const statesWorkspace = presence.getStates("name:testStatesWorkspaceWithLatestMap", { - fixedMap: LatestMap({ key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }), + fixedMap: StateFactory.latestMap({ + key1: { x: 0, y: 0 }, + key2: { ref: "default", someId: 0 }, + }), }); // Workaround ts(2775): Assertions require every name in the call target to be declared with an explicit type annotation. const workspace: typeof statesWorkspace = statesWorkspace; @@ -130,7 +133,7 @@ export function checkCompiles(): void { tilt?: number; } - workspace.add("pointers", LatestMap({})); + workspace.add("pointers", StateFactory.latestMap({})); const pointers = workspace.props.pointers; const localPointers = pointers.local; @@ -140,7 +143,7 @@ export function checkCompiles(): void { key, value, }: Pick< - LatestMapItemValueClientData, + LatestMapItemUpdatedClientData, "attendee" | "key" | "value" >): void { console.log(attendee.attendeeId, key, value); diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index c32eec749de3..1c0379123a70 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -12,10 +12,10 @@ import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; import type { BroadcastControlSettings, + LatestClientData, Presence, - LatestValueClientData, } from "@fluidframework/presence/alpha"; -import { Latest } from "@fluidframework/presence/alpha"; +import { StateFactory } from "@fluidframework/presence/alpha"; const testWorkspaceName = "name:testWorkspaceA"; @@ -25,13 +25,13 @@ function createLatestManager( valueControlSettings?: BroadcastControlSettings, ) { const states = presence.getStates(testWorkspaceName, { - camera: Latest({ x: 0, y: 0, z: 0 }, valueControlSettings), + camera: StateFactory.latest({ x: 0, y: 0, z: 0 }, valueControlSettings), }); return states.props.camera; } describe("Presence", () => { - describe("LatestValueManager", () => { + describe("Latest", () => { /** * See {@link checkCompiles} below */ @@ -46,28 +46,28 @@ describe("Presence", () => { it("can set and get empty object as initial value", () => { const states = presence.getStates(testWorkspaceName, { - obj: Latest({}), + obj: StateFactory.latest({}), }); assert.deepStrictEqual(states.props.obj.local, {}); }); it("can set and get object with properties as initial value", () => { const states = presence.getStates(testWorkspaceName, { - obj: Latest({ x: 0, y: 0, z: 0 }), + obj: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); assert.deepStrictEqual(states.props.obj.local, { x: 0, y: 0, z: 0 }); }); it("can set and get empty array as initial value", () => { const states = presence.getStates(testWorkspaceName, { - arr: Latest([]), + arr: StateFactory.latest([]), }); assert.deepStrictEqual(states.props.arr.local, []); }); it("can set and get array with elements as initial value", () => { const states = presence.getStates(testWorkspaceName, { - arr: Latest([1, 2, 3]), + arr: StateFactory.latest([1, 2, 3]), }); assert.deepStrictEqual(states.props.arr.local, [1, 2, 3]); }); @@ -79,7 +79,7 @@ describe("Presence", () => { // Setup const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.getStates(testWorkspaceName, { - camera: Latest({ x: 0, y: 0, z: 0 }), + camera: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); const camera = states.props.camera; @@ -105,14 +105,14 @@ export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const presence = {} as Presence; const statesWorkspace = presence.getStates("name:testStatesWorkspaceWithLatest", { - cursor: Latest({ x: 0, y: 0 }), - camera: Latest({ x: 0, y: 0, z: 0 }), + cursor: StateFactory.latest({ x: 0, y: 0 }), + camera: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); // Workaround ts(2775): Assertions require every name in the call target to be declared with an explicit type annotation. const workspace: typeof statesWorkspace = statesWorkspace; const props = workspace.props; - workspace.add("caret", Latest({ id: "", pos: 0 })); + workspace.add("caret", StateFactory.latest({ id: "", pos: 0 })); const fakeAdd = workspace.props.caret.local.pos + props.camera.local.z + props.cursor.local.x; @@ -123,7 +123,7 @@ export function checkCompiles(): void { function logClientValue< T /* following extends should not be required: */ extends Record, - >({ attendee, value }: Pick, "attendee" | "value">): void { + >({ attendee, value }: Pick, "attendee" | "value">): void { console.log(attendee.attendeeId, value); } diff --git a/packages/framework/presence/src/types.ts b/packages/framework/presence/src/types.ts index 8fd4c8492d5b..b3d7da22f57c 100644 --- a/packages/framework/presence/src/types.ts +++ b/packages/framework/presence/src/types.ts @@ -41,7 +41,7 @@ export type StatesWorkspaceEntry< /** * Schema for a {@link StatesWorkspace} workspace. * - * Keys of schema are the keys of the {@link StatesWorkspace} providing access to `Value Manager`s. + * Keys of schema are the keys of the {@link StatesWorkspace} providing access to State objects. * * @alpha */ @@ -50,14 +50,14 @@ export interface StatesWorkspaceSchema { } /** - * Map of `Value Manager`s registered with {@link StatesWorkspace}. + * Map of State objects registered with {@link StatesWorkspace}. * * @sealed * @alpha */ export type StatesWorkspaceEntries = { /** - * Registered `Value Manager`s + * Registered State objects. */ readonly [Key in keyof TSchema]: ReturnType< TSchema[Key] @@ -67,10 +67,10 @@ export type StatesWorkspaceEntries = { }; /** - * `StatesWorkspace` maintains a registry of `Value Manager`s that all share and provide access to + * `StatesWorkspace` maintains a registry of State objects that all share and provide access to * presence state values across client members in a session. * - * `Value Manager`s offer variations on how to manage states, but all share same principle that + * State objects offer variations on how to manage states, but all share same principle that * each client's state is independent and may only be updated by originating client. * * @sealed @@ -81,9 +81,9 @@ export interface StatesWorkspace< TManagerConstraints = unknown, > { /** - * Registers a new `Value Manager` with the {@link StatesWorkspace}. - * @param key - new unique key for the `Value Manager` within the workspace - * @param manager - factory for creating a `Value Manager` + * Registers a new State object with the {@link StatesWorkspace}. + * @param key - new unique key for the State object within the workspace + * @param manager - factory for creating a State object */ add< TKey extends string, @@ -98,7 +98,7 @@ export interface StatesWorkspace< >; /** - * Registry of `Value Manager`s. + * Registry of State objects. */ readonly props: StatesWorkspaceEntries; @@ -142,9 +142,9 @@ export interface NotificationsWorkspaceSchema { */ export interface NotificationsWorkspace { /** - * Registers a new `Value Manager` with the {@link NotificationsWorkspace}. - * @param key - new unique key for the `Value Manager` within the workspace - * @param manager - factory for creating a `Value Manager` + * Registers a new `NotificationsManager` with the {@link NotificationsWorkspace}. + * @param key - new unique key for the `NotificationsManager` within the workspace + * @param manager - factory for creating a `NotificationsManager` */ add< TKey extends string, @@ -158,7 +158,7 @@ export interface NotificationsWorkspace; /** - * Registry of `Value Manager`s. + * Registry of `NotificationsManager`s. */ readonly props: StatesWorkspaceEntries; } From d50c631ed584af8be1e8c88f1aa88418f8172b3c Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Wed, 16 Apr 2025 18:29:04 +0200 Subject: [PATCH 3/6] refactor(client-presence): Restructure Presence API (#24351) Restructuring of Presence API All changes added in .changest/sour-mirrors-wave: Docs have been updated as well to reflect changes. Only thing remaining after this will be to pass root Presence to workspace and "value manager" level --- .changeset/sour-mirrors-wave.md | 46 +++---- docs/docs/build/presence.md | 8 +- examples/apps/ai-collab/src/app/presence.ts | 13 +- .../src/components/UserPresenceGroup.tsx | 18 +-- .../apps/presence-tracker/src/FocusTracker.ts | 4 +- .../apps/presence-tracker/src/MouseTracker.ts | 4 +- examples/apps/presence-tracker/src/app.ts | 18 +-- .../apps/presence-tracker/src/reactions.ts | 4 +- .../tests/presenceTracker.test.ts | 2 +- .../external-controller/src/presence.ts | 2 +- .../external-controller/src/view.ts | 11 +- packages/framework/presence/README.md | 8 +- .../presence/api-report/presence.alpha.api.md | 44 ++++--- packages/framework/presence/src/index.ts | 1 + .../presence/src/latestMapValueManager.ts | 16 +-- .../presence/src/latestValueManager.ts | 14 +- packages/framework/presence/src/presence.ts | 123 ++++++++++-------- .../presence/src/presenceDatastoreManager.ts | 2 +- .../framework/presence/src/presenceManager.ts | 60 ++++----- .../framework/presence/src/systemWorkspace.ts | 16 +-- .../presence/src/test/batching.spec.ts | 30 ++--- .../presence/src/test/eventing.spec.ts | 44 ++++--- .../src/test/latestMapValueManager.spec.ts | 25 ++-- .../src/test/latestValueManager.spec.ts | 20 +-- .../src/test/notificationsManager.spec.ts | 7 +- .../src/test/presenceDatastoreManager.spec.ts | 4 +- .../presence/src/test/presenceManager.spec.ts | 69 +++++----- .../presence/src/test/presenceStates.spec.ts | 4 +- .../src/test/multiprocess/childClient.ts | 14 +- .../src/test/multiprocess/messageTypes.ts | 6 +- .../test/multiprocess/presenceTest.spec.ts | 14 +- 31 files changed, 340 insertions(+), 311 deletions(-) diff --git a/.changeset/sour-mirrors-wave.md b/.changeset/sour-mirrors-wave.md index af5ef8bbe1d5..d2f4d3c774ec 100644 --- a/.changeset/sour-mirrors-wave.md +++ b/.changeset/sour-mirrors-wave.md @@ -15,6 +15,13 @@ The following API changes have been made to improve clarity and consistency: | `acquirePresenceViaDataObject` | `getPresenceViaDataObject` | | `ClientSessionId` | `AttendeeId` | | `IPresence` | `Presence` | +| `IPresence.events["attendeeJoined"]` | `Presence.attendees.events["attendeeConnected"]` | +| `IPresence.events["attendeeDisconnected"]` | `Presence.attendees.events["attendeeDisconnected"]` | +| `IPresence.getAttendee` | `Presence.attendees.getAttendee` | +| `IPresence.getAttendees` | `Presence.attendees.getAttendees` | +| `IPresence.getMyself` | `Presence.attendees.getMyself` | +| `IPresence.getNotifications` | `Presence.notifications.getWorkspace` | +| `IPresence.getStates` | `Presence.states.getWorkspace` | | `ISessionClient` | `Attendee` | | `Latest` (import) | `StateFactory` | | `Latest` (call) | `StateFactory.latest` | @@ -23,12 +30,20 @@ The following API changes have been made to improve clarity and consistency: | `LatestMapItemValueClientData` | `LatestMapItemUpdatedClientData` | | `LatestMapValueClientData` | `LatestMapClientData` | | `LatestMapValueManager` | `LatestMap` | +| `LatestMapValueManager.clients` | `LatestMap.getStateAttendees` | +| `LatestMapValueManager.clientValue` | `LatestMap.getRemote` | +| `LatestMapValueManager.clientValues` | `LatestMap.getRemotes` | | `LatestMapValueManagerEvents` | `LatestMapEvents` | | `LatestValueClientData` | `LatestClientData` | | `LatestValueData` | `LatestData` | | `LatestValueManager` | `Latest` | +| `LatestValueManager.clients` | `Latest.getStateAttendees` | +| `LatestValueManager.clientValue` | `Latest.getRemote` | +| `LatestValueManager.clientValues` | `Latest.getRemotes` | | `LatestValueManagerEvents` | `LatestEvents` | | `LatestValueMetadata` | `LatestMetadata` | +| `PresenceEvents.attendeeDisconnected` | `AttendeesEvents.attendeeDisconnected`| +| `PresenceEvents.attendeeJoined` | `AttendeesEvents.attendeeConnected`| | `PresenceNotifications` | `NotificationsWorkspace` | | `PresenceNotificationsSchema` | `NotificationsWorkspaceSchema` | | `PresenceStates` | `StatesWorkspace` | @@ -39,35 +54,4 @@ The following API changes have been made to improve clarity and consistency: | `SessionClientStatus` | `AttendeeStatus` | | `ValueMap` | `StateMap` | -```json -{ - "acquirePresence": "getPresence", - "acquirePresenceViaDataObject": "getPresenceViaDataObject", - "ClientSessionId": "AttendeeId", - "IPresence": "Presence", - "ISessionClient": "Attendee", - "Latest": "StateFactory", - "LatestMap": "StateFactory", - "LatestMapItemValueClientData": "LatestMapItemUpdatedClientData", - "LatestMapValueClientData": "LatestMapClientData", - "LatestMapValueManager": "LatestMap", - "LatestMapValueManagerEvents": "LatestMapEvents", - "LatestValueClientData": "LatestClientData", - "LatestValueData": "LatestData", - "LatestValueManager": "Latest", - "LatestValueManagerEvents": "LatestEvents", - "LatestValueMetadata": "LatestMetadata", - "PresenceNotifications": "NotificationsWorkspace", - "PresenceNotificationsSchema": "NotificationsWorkspaceSchema", - "PresenceStates": "StatesWorkspace", - "PresenceStatesEntries": "StatesWorkspaceEntries", - "PresenceStatesSchema": "StatesWorkspaceSchema", - "PresenceWorkspaceAddress": "WorkspaceAddress", - "PresenceWorkspaceEntry": "StatesWorkspaceEntry", - "SessionClientStatus": "AttendeeStatus", - "ValueMap": "StateMap" -} -``` -The JSON table above can be used to automate most of these replacements in your codebase. You can implement a simple script that reads this JSON and performs the necessary replacements in your files. - Note: To fully replace OLD `Latest` and `LatestMap` functions, you should import `StateFactory` and call `StateFactory.latest` and `StateFactory.latestMap` respectively. NEW `Latest` and `LatestMap` APIs replace `LatestValueManager` and `LatestMapValueManager`. diff --git a/docs/docs/build/presence.md b/docs/docs/build/presence.md index 7dd629e8b253..dfcfa876575a 100644 --- a/docs/docs/build/presence.md +++ b/docs/docs/build/presence.md @@ -87,19 +87,19 @@ Current API does not provide a mechanism to validate that state and notification Example: ```typescript -presence.getStates("app:v1states", { myState: StateFactory.latest({ x: 0 }) }); +presence.states.getWorkspace("app:v1states", { myState: StateFactory.latest({ x: 0 }) }); ``` is incompatible with ```typescript -presence.getStates("app:v1states", { myState: StateFactory.latest({ x: "text" }) }); +presence.states.getWorkspace("app:v1states", { myState: StateFactory.latest({ x: "text" }) }); ``` as "app:v1states"+"myState" have different value type expectations: `{x: number}` versus `{x: string}`. ```typescript -presence.getStates("app:v1states", { myState2: StateFactory.latest({ x: true }) }); +presence.states.getWorkspace("app:v1states", { myState2: StateFactory.latest({ x: true }) }); ``` would be compatible with both of the prior schemas as "myState2" is a different name. Though in this situation none of the different clients would be able to observe each other. @@ -128,7 +128,7 @@ You can configure the grouping and throttling behavior using the `allowableUpdat ```ts // Configure a states workspace -const stateWorkspace = presence.getStates( +const stateWorkspace = presence.states.getWorkspace( "app:v1states", { // This Latest state has an allowable latency of 100ms. diff --git a/examples/apps/ai-collab/src/app/presence.ts b/examples/apps/ai-collab/src/app/presence.ts index c87bc2218a01..ab5b2bc21853 100644 --- a/examples/apps/ai-collab/src/app/presence.ts +++ b/examples/apps/ai-collab/src/app/presence.ts @@ -25,7 +25,7 @@ export type UserPresence = StatesWorkspace; // Takes a presence object and returns the user presence object that contains the shared object states export function buildUserPresence(presence: Presence): UserPresence { - const states = presence.getStates(`name:user-avatar-states`, statesSchema); + const states = presence.states.getWorkspace(`name:user-avatar-states`, statesSchema); return states; } @@ -42,7 +42,7 @@ export class PresenceManager { const appSelectionWorkspaceAddress = "aiCollab:workspace"; // Initialize presence state for the app selection workspace - this.usersState = presence.getStates( + this.usersState = presence.states.getWorkspace( appSelectionWorkspaceAddress, // Workspace address statesSchema, // Workspace schema ); @@ -77,7 +77,10 @@ export class PresenceManager { this.usersState.props.onlineUsers.local = { photo: photoUrl }; } - this.userInfoMap.set(this.presence.getMyself(), this.usersState.props.onlineUsers.local); + this.userInfoMap.set( + this.presence.attendees.getMyself(), + this.usersState.props.onlineUsers.local, + ); this.userInfoCallback(this.userInfoMap); } @@ -98,9 +101,9 @@ export class PresenceManager { for (const sessionClient of sessionList) { // If local user or remote user is connected, then only add it to the list try { - const userInfo = this.usersState.props.onlineUsers.clientValue(sessionClient).value; + const userInfo = this.usersState.props.onlineUsers.getRemote(sessionClient).value; // If the user is local user, then add it to the beginning of the list - if (sessionClient.attendeeId === this.presence.getMyself().attendeeId) { + if (sessionClient.attendeeId === this.presence.attendees.getMyself().attendeeId) { userInfoList.push(userInfo); } else { // If the user is remote user, then add it to the end of the list diff --git a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx index 839d3a6387e4..0e512e4b3ae8 100644 --- a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx +++ b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx @@ -18,14 +18,16 @@ const UserPresenceGroup: React.FC = ({ presenceManager }): JS const [invalidations, setInvalidations] = useState(0); useEffect(() => { - // Listen to the attendeeJoined event and update the presence group when a new attendee joins - const unsubJoin = presenceManager.getPresence().events.on("attendeeJoined", () => { - setInvalidations(invalidations + Math.random()); - }); + // Listen to the attendeeConnected event and update the presence group when a new attendee joins + const unsubJoin = presenceManager + .getPresence() + .attendees.events.on("attendeeConnected", () => { + setInvalidations(invalidations + Math.random()); + }); // Listen to the attendeeDisconnected event and update the presence group when an attendee leaves const unsubDisconnect = presenceManager .getPresence() - .events.on("attendeeDisconnected", () => { + .attendees.events.on("attendeeDisconnected", () => { setInvalidations(invalidations + Math.random()); }); // Listen to the userInfoUpdate event and update the presence group when the user info is updated @@ -41,9 +43,9 @@ const UserPresenceGroup: React.FC = ({ presenceManager }): JS }); // Get the list of connected attendees - const connectedAttendees = [...presenceManager.getPresence().getAttendees()].filter( - (attendee) => attendee.getConnectionStatus() === "Connected", - ); + const connectedAttendees = [ + ...presenceManager.getPresence().attendees.getAttendees(), + ].filter((attendee) => attendee.getConnectionStatus() === "Connected"); // Get the user info for the connected attendees const userInfoList = presenceManager.getUserInfo(connectedAttendees); diff --git a/examples/apps/presence-tracker/src/FocusTracker.ts b/examples/apps/presence-tracker/src/FocusTracker.ts index 82a74ecc7b8e..5b30823bdbbb 100644 --- a/examples/apps/presence-tracker/src/FocusTracker.ts +++ b/examples/apps/presence-tracker/src/FocusTracker.ts @@ -104,10 +104,10 @@ export class FocusTracker extends TypedEventEmitter { // Include the local client in the map because this is used to render a // dashboard of all connected clients. - const currentClient = this.presence.getMyself(); + const currentClient = this.presence.attendees.getMyself(); statuses.set(currentClient, this.focus.local.hasFocus); - for (const { attendee, value } of this.focus.clientValues()) { + for (const { attendee, value } of this.focus.getRemotes()) { if (attendee.getConnectionStatus() === AttendeeStatus.Connected) { const { hasFocus } = value; statuses.set(attendee, hasFocus); diff --git a/examples/apps/presence-tracker/src/MouseTracker.ts b/examples/apps/presence-tracker/src/MouseTracker.ts index 76b86224be79..8d01e15bbd41 100644 --- a/examples/apps/presence-tracker/src/MouseTracker.ts +++ b/examples/apps/presence-tracker/src/MouseTracker.ts @@ -67,7 +67,7 @@ export class MouseTracker extends TypedEventEmitter { // When an attendee disconnects, emit the mousePositionChanged event so client can update their rendered view // accordingly. - this.presence.events.on("attendeeDisconnected", () => { + this.presence.attendees.events.on("attendeeDisconnected", () => { this.emit("mousePositionChanged"); }); @@ -88,7 +88,7 @@ export class MouseTracker extends TypedEventEmitter { public getMousePresences(): Map { const statuses: Map = new Map(); - for (const { attendee, value } of this.cursor.clientValues()) { + for (const { attendee, value } of this.cursor.getRemotes()) { if (attendee.getConnectionStatus() === AttendeeStatus.Connected) { statuses.set(attendee, value); } diff --git a/examples/apps/presence-tracker/src/app.ts b/examples/apps/presence-tracker/src/app.ts index 5f838b612a7e..c392258362de 100644 --- a/examples/apps/presence-tracker/src/app.ts +++ b/examples/apps/presence-tracker/src/app.ts @@ -62,7 +62,7 @@ async function start() { // Get the states workspace for the tracker data. This workspace will be created if it doesn't exist. // We create it with no states; we will pass the workspace to the Mouse and Focus trackers, and they will create value // managers within the workspace to track and share individual pieces of state. - const appPresence = presence.getStates("name:trackerData", {}); + const appPresence = presence.states.getWorkspace("name:trackerData", {}); // Update the browser URL and the window title with the actual container ID location.hash = id; @@ -85,7 +85,7 @@ async function start() { // Setting "fluid*" and these helpers are just for our test automation const buildAttendeeMap = () => { - return [...presence.getAttendees()].reduce((map, a) => { + return [...presence.attendees.getAttendees()].reduce((map, a) => { map[a.attendeeId] = a.getConnectionStatus(); return map; }, {}); @@ -107,20 +107,20 @@ async function start() { /* eslint-disable @typescript-eslint/dot-notation */ window["fluidSessionAttendeeCheck"] = checkAttendees; window["fluidSessionAttendees"] = buildAttendeeMap(); - window["fluidSessionAttendeeCount"] = presence.getAttendees().size; - presence.events.on("attendeeJoined", (attendee) => { + window["fluidSessionAttendeeCount"] = presence.attendees.getAttendees().size; + presence.attendees.events.on("attendeeConnected", (attendee) => { console.log(`Attendee joined: ${attendee.attendeeId}`); window["fluidSessionAttendees"] = buildAttendeeMap(); - window["fluidSessionAttendeeCount"] = presence.getAttendees().size; - window["fluidAttendeeJoinedCalled"] = true; + window["fluidSessionAttendeeCount"] = presence.attendees.getAttendees().size; + window["fluidattendeeConnectedCalled"] = true; }); - presence.events.on("attendeeDisconnected", (attendee) => { + presence.attendees.events.on("attendeeDisconnected", (attendee) => { console.log(`Attendee left: ${attendee.attendeeId}`); window["fluidSessionAttendees"] = buildAttendeeMap(); - window["fluidSessionAttendeeCount"] = presence.getAttendees().size; + window["fluidSessionAttendeeCount"] = presence.attendees.getAttendees().size; window["fluidAttendeeDisconnectedCalled"] = true; }); - window["fluidSessionId"] = presence.getMyself().attendeeId; + window["fluidSessionId"] = presence.attendees.getMyself().attendeeId; // Always set last as it is used as fence for load completion window["fluidContainerId"] = id; /* eslint-enable @typescript-eslint/dot-notation */ diff --git a/examples/apps/presence-tracker/src/reactions.ts b/examples/apps/presence-tracker/src/reactions.ts index eda95223115e..fb4d7ecea3a0 100644 --- a/examples/apps/presence-tracker/src/reactions.ts +++ b/examples/apps/presence-tracker/src/reactions.ts @@ -17,7 +17,7 @@ export function initializeReactions(presence: Presence, mouseTracker: MouseTrack // Create a notifications workspace to send reactions-related notifications. This workspace will be created if it // doesn't exist. We also create a NotificationsManager. You can also // add presence objects to the workspace later. - const notificationsWorkspace = presence.getNotifications( + const notificationsWorkspace = presence.notifications.getWorkspace( // A unique key identifying this workspace. "name:reactions", { @@ -46,7 +46,7 @@ export function initializeReactions(presence: Presence, mouseTracker: MouseTrack const reactionValue = selectedReaction.textContent; // Check that we're connected before sending notifications. - if (presence.getMyself().getConnectionStatus() === "Connected") { + if (presence.attendees.getMyself().getConnectionStatus() === "Connected") { notificationsWorkspace.props.reactions.emit.broadcast( "reaction", mouseTracker.getMyMousePosition(), diff --git a/examples/apps/presence-tracker/tests/presenceTracker.test.ts b/examples/apps/presence-tracker/tests/presenceTracker.test.ts index 4b53684b21be..81165a06bf6b 100644 --- a/examples/apps/presence-tracker/tests/presenceTracker.test.ts +++ b/examples/apps/presence-tracker/tests/presenceTracker.test.ts @@ -185,7 +185,7 @@ describe("presence-tracker", () => { const attendeeData = await page.evaluate(() => ({ attendeeCount: `${window["fluidSessionAttendeeCount"]}`, attendees: window["fluidSessionAttendees"] ?? {}, - attendeeJoinedCalled: `${window["fluidAttendeeJoinedCalled"]}`, + attendeeConnectedCalled: `${window["fluidattendeeConnectedCalled"]}`, attendeeDisconnectedCalled: `${window["fluidAttendeeDisconnectedCalled"]}`, })); throw new Error(`${timeoutErrorMessage} (${JSON.stringify(attendeeData)})`); diff --git a/examples/service-clients/azure-client/external-controller/src/presence.ts b/examples/service-clients/azure-client/external-controller/src/presence.ts index 59dde16b2c5e..d6622b4f282d 100644 --- a/examples/service-clients/azure-client/external-controller/src/presence.ts +++ b/examples/service-clients/azure-client/external-controller/src/presence.ts @@ -43,6 +43,6 @@ const statesSchema = { export type DicePresence = StatesWorkspace; export function buildDicePresence(presence: Presence): DicePresence { - const states = presence.getStates("name:app-client-states", statesSchema); + const states = presence.states.getWorkspace("name:app-client-states", statesSchema); return states; } 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 061de09deabb..b780d6ad2b6f 100644 --- a/examples/service-clients/azure-client/external-controller/src/view.ts +++ b/examples/service-clients/azure-client/external-controller/src/view.ts @@ -111,7 +111,7 @@ export function makeDiceValuesView( lastRoll: Latest, ): void { const children = makeDiceHeaderElement(); - for (const clientValue of lastRoll.clientValues()) { + for (const clientValue of lastRoll.getRemotes()) { children.push(...makeDiceValueElement(clientValue.attendee.attendeeId, clientValue.value)); } target.replaceChildren(...children); @@ -165,16 +165,19 @@ function makePresenceView( logContentDiv.style.overflowY = "scroll"; logContentDiv.style.border = "1px solid black"; if (audience !== undefined) { - presenceConfig.presence.events.on("attendeeJoined", (attendee) => { + presenceConfig.presence.attendees.events.on("attendeeConnected", (attendee) => { const name = audience.getMembers().get(attendee.getConnectionId())?.name; const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} 🔗 with id ${attendee.attendeeId} joined`; addLogEntry(logContentDiv, update); }); - presenceConfig.presence.events.on("attendeeDisconnected", (attendee) => { + presenceConfig.presence.attendees.events.on("attendeeDisconnected", (attendee) => { // Filter for remote attendees const self = audience.getMyself(); - if (self && attendee !== presenceConfig.presence.getAttendee(self.currentConnection)) { + if ( + self && + attendee !== presenceConfig.presence.attendees.getAttendee(self.currentConnection) + ) { const name = audience.getMembers().get(attendee.getConnectionId())?.name; const update = `client ${name === undefined ? "(unnamed)" : `named ${name}`} ⛓️‍💥 with id ${attendee.attendeeId} left`; addLogEntry(logContentDiv, update); diff --git a/packages/framework/presence/README.md b/packages/framework/presence/README.md index 94bfd53dbc69..9464db990fde 100644 --- a/packages/framework/presence/README.md +++ b/packages/framework/presence/README.md @@ -108,16 +108,16 @@ Current API does not provide a mechanism to validate that state and notification Example: ```typescript -presence.getStates("app:v1states", { myState: StateFactory.latest({x: 0})}); +presence.states.getWorkspace("app:v1states", { myState: StateFactory.latest({x: 0})}); ``` is incompatible with ```typescript -presence.getStates("app:v1states", { myState: StateFactory.latest({x: "text"})}); +presence.states.getWorkspace("app:v1states", { myState: StateFactory.latest({x: "text"})}); ``` as "app:v1states"+"myState" have different value type expectations: `{x: number}` versus `{x: string}`. ```typescript -presence.getStates("app:v1states", { myState2: StateFactory.latest({x: true})}); +presence.states.getWorkspace("app:v1states", { myState2: StateFactory.latest({x: true})}); ``` would be compatible with both of the prior schemas as "myState2" is a different name. Though in this situation none of the different clients would be able to observe each other. @@ -146,7 +146,7 @@ You can configure the grouping and throttling behavior using the `allowableUpdat ```ts // Configure a states workspace -const stateWorkspace = presence.getStates("app:v1states", +const stateWorkspace = presence.states.getWorkspace("app:v1states", { // This Latest state has an allowable latency of 100ms. position: StateFactory.latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }), diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 4c0bda758472..4d75684fb5d7 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -16,6 +16,14 @@ export type AttendeeId = SessionId & { readonly AttendeeId: "AttendeeId"; }; +// @alpha @sealed (undocumented) +export interface AttendeesEvents { + // @eventProperty + attendeeConnected: (attendee: Attendee) => void; + // @eventProperty + attendeeDisconnected: (attendee: Attendee) => void; +} + // @alpha export const AttendeeStatus: { readonly Connected: "Connected"; @@ -129,11 +137,11 @@ export namespace InternalUtilityTypes { // @alpha @sealed export interface Latest { - clients(): Attendee[]; - clientValue(attendee: Attendee): LatestData; - clientValues(): IterableIterator>; readonly controls: BroadcastControls; readonly events: Listenable>; + getRemote(attendee: Attendee): LatestData; + getRemotes(): IterableIterator>; + getStateAttendees(): Attendee[]; get local(): InternalUtilityTypes.FullyReadonly>; set local(value: JsonSerializable & JsonDeserialized); } @@ -167,11 +175,11 @@ export interface LatestEvents { // @alpha @sealed export interface LatestMap { - clients(): Attendee[]; - clientValue(attendee: Attendee): ReadonlyMap>; - clientValues(): IterableIterator>; readonly controls: BroadcastControls; readonly events: Listenable>; + getRemote(attendee: Attendee): ReadonlyMap>; + getRemotes(): IterableIterator>; + getStateAttendees(): Attendee[]; readonly local: StateMap; } @@ -275,20 +283,26 @@ export interface NotificationsWorkspaceSchema { // @alpha @sealed export interface Presence { + // (undocumented) + readonly attendees: { + readonly events: Listenable; + getAttendees(): ReadonlySet; + getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee; + getMyself(): Attendee; + }; readonly events: Listenable; - getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee; - getAttendees(): ReadonlySet; - getMyself(): Attendee; - getNotifications(notificationsId: WorkspaceAddress, requestedContent: NotificationsSchema): NotificationsWorkspace; - getStates(workspaceAddress: WorkspaceAddress, requestedContent: StatesSchema, controls?: BroadcastControlSettings): StatesWorkspace; + // (undocumented) + readonly notifications: { + getWorkspace(notificationsId: WorkspaceAddress, requestedNotifications: NotificationsSchema): NotificationsWorkspace; + }; + // (undocumented) + readonly states: { + getWorkspace(workspaceAddress: WorkspaceAddress, requestedStates: StatesSchema, controls?: BroadcastControlSettings): StatesWorkspace; + }; } // @alpha @sealed (undocumented) export interface PresenceEvents { - // @eventProperty - attendeeDisconnected: (attendee: Attendee) => void; - // @eventProperty - attendeeJoined: (attendee: Attendee) => void; workspaceActivated: (workspaceAddress: WorkspaceAddress, type: "States" | "Notifications" | "Unknown") => void; } diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 32ad8cd05e53..8fc483c90bbd 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -25,6 +25,7 @@ export type { export { type Attendee, + type AttendeesEvents, type AttendeeId, AttendeeStatus, type Presence, diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index d67bd3ad8ff9..61adc236603e 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -328,15 +328,15 @@ export interface LatestMap { /** * Iterable access to remote clients' map of values. */ - clientValues(): IterableIterator>; + getRemotes(): IterableIterator>; /** - * Array of known remote clients. + * Array of {@link Attendee}s that have provided states. */ - clients(): Attendee[]; + getStateAttendees(): Attendee[]; /** * Access to a specific client's map of values. */ - clientValue(attendee: Attendee): ReadonlyMap>; + getRemote(attendee: Attendee): ReadonlyMap>; } class LatestMapValueManagerImpl< @@ -374,25 +374,25 @@ class LatestMapValueManagerImpl< public readonly local: StateMap; - public *clientValues(): IterableIterator> { + public *getRemotes(): IterableIterator> { const allKnownStates = this.datastore.knownValues(this.key); for (const attendeeId of objectKeys(allKnownStates.states)) { if (attendeeId !== allKnownStates.self) { const attendee = this.datastore.lookupClient(attendeeId); - const items = this.clientValue(attendee); + const items = this.getRemote(attendee); yield { attendee, items }; } } } - public clients(): Attendee[] { + public getStateAttendees(): Attendee[] { const allKnownStates = this.datastore.knownValues(this.key); return objectKeys(allKnownStates.states) .filter((attendeeId) => attendeeId !== allKnownStates.self) .map((attendeeId) => this.datastore.lookupClient(attendeeId)); } - public clientValue(attendee: Attendee): ReadonlyMap> { + public getRemote(attendee: Attendee): ReadonlyMap> { const allKnownStates = this.datastore.knownValues(this.key); const attendeeId = attendee.attendeeId; const clientStateMap = allKnownStates.states[attendeeId]; diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 85b68de9ae45..2c8943d2dc12 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -76,15 +76,15 @@ export interface Latest { /** * Iterable access to remote clients' values. */ - clientValues(): IterableIterator>; + getRemotes(): IterableIterator>; /** - * Array of known remote clients. + * Array of {@link Attendee}s that have provided states. */ - clients(): Attendee[]; + getStateAttendees(): Attendee[]; /** * Access to a specific attendee's value. */ - clientValue(attendee: Attendee): LatestData; + getRemote(attendee: Attendee): LatestData; } class LatestValueManagerImpl @@ -117,7 +117,7 @@ class LatestValueManagerImpl this.events.emit("localUpdated", { value }); } - public *clientValues(): IterableIterator> { + public *getRemotes(): IterableIterator> { const allKnownStates = this.datastore.knownValues(this.key); for (const [attendeeId, value] of objectEntries(allKnownStates.states)) { if (attendeeId !== allKnownStates.self) { @@ -130,14 +130,14 @@ class LatestValueManagerImpl } } - public clients(): Attendee[] { + public getStateAttendees(): Attendee[] { const allKnownStates = this.datastore.knownValues(this.key); return Object.keys(allKnownStates.states) .filter((attendeeId) => attendeeId !== allKnownStates.self) .map((attendeeId) => this.datastore.lookupClient(attendeeId)); } - public clientValue(attendee: Attendee): LatestData { + public getRemote(attendee: Attendee): LatestData { const allKnownStates = this.datastore.knownValues(this.key); const clientState = allKnownStates.states[attendee.attendeeId]; if (clientState === undefined) { diff --git a/packages/framework/presence/src/presence.ts b/packages/framework/presence/src/presence.ts index fcd7de655677..8fa06b72b953 100644 --- a/packages/framework/presence/src/presence.ts +++ b/packages/framework/presence/src/presence.ts @@ -37,12 +37,12 @@ export type AttendeeId = SessionId & { readonly AttendeeId: "AttendeeId" }; */ export const AttendeeStatus = { /** - * The attendee is connected to the Fluid service. + * The {@link Attendee} is connected to the Fluid service. */ Connected: "Connected", /** - * The attendee is not connected to the Fluid service. + * The {@link Attendee} is not connected to the Fluid service. */ Disconnected: "Disconnected", } as const; @@ -67,7 +67,7 @@ export type AttendeeStatus = (typeof AttendeeStatus)[keyof typeof AttendeeStatus * @remarks * Note: This is very preliminary attendee representation. * - * `Attendee` should be used as key to distinguish between different + * {@link Attendee} should be used as key to distinguish between different * clients as they join, rejoin, and disconnect from a session. While a * client's {@link ClientConnectionId} from {@link Attendee.getConnectionStatus} * may change over time, `Attendee` will be fixed. @@ -119,13 +119,13 @@ export type SpecificAttendee = * @sealed * @alpha */ -export interface PresenceEvents { +export interface AttendeesEvents { /** * Raised when new client joins session. * * @eventProperty */ - attendeeJoined: (attendee: Attendee) => void; + attendeeConnected: (attendee: Attendee) => void; /** * Raised when client appears disconnected from session. @@ -133,7 +133,13 @@ export interface PresenceEvents { * @eventProperty */ attendeeDisconnected: (attendee: Attendee) => void; +} +/** + * @sealed + * @alpha + */ +export interface PresenceEvents { /** * Raised when a workspace is activated within the session. * @@ -164,52 +170,63 @@ export interface Presence { */ readonly events: Listenable; - /** - * Get all attendees in the session. - * - * @remarks - * Attendee states are dynamic and will change as clients join and leave - * the session. - */ - getAttendees(): ReadonlySet; - - /** - * Lookup a specific attendee in the session. - * - * @param clientId - Client connection or session ID - */ - getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee; - - /** - * Get this client's attendee. - * - * @returns This client's attendee. - */ - getMyself(): Attendee; - - /** - * Acquires a StatesWorkspace from store or adds new one. - * - * @param workspaceAddress - Address of the requested StatesWorkspace - * @param requestedContent - Requested states for the workspace - * @param controls - Optional settings for default broadcast controls - * @returns A StatesWorkspace - */ - getStates( - workspaceAddress: WorkspaceAddress, - requestedContent: StatesSchema, - controls?: BroadcastControlSettings, - ): StatesWorkspace; - - /** - * Acquires a Notifications workspace from store or adds new one. - * - * @param workspaceAddress - Address of the requested Notifications Workspace - * @param requestedContent - Requested notifications for the workspace - * @returns A Notifications workspace - */ - getNotifications( - notificationsId: WorkspaceAddress, - requestedContent: NotificationsSchema, - ): NotificationsWorkspace; + readonly attendees: { + /** + * Events for {@link Attendee}s. + */ + readonly events: Listenable; + + /** + * Get all {@link Attendee}s in the session. + * + * @remarks + * Attendee states are dynamic and will change as clients join and leave + * the session. + */ + getAttendees(): ReadonlySet; + + /** + * Lookup a specific {@link Attendee} in the session. + * + * @param clientId - Client connection or session ID + */ + getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee; + + /** + * Get this client's {@link Attendee}. + * + * @returns This client's attendee. + */ + getMyself(): Attendee; + }; + + readonly states: { + /** + * Acquires a StatesWorkspace from store or adds new one. + * + * @param workspaceAddress - Address of the requested StatesWorkspace + * @param requestedStates - Requested states for the workspace + * @param controls - Optional settings for default broadcast controls + * @returns A StatesWorkspace + */ + getWorkspace( + workspaceAddress: WorkspaceAddress, + requestedStates: StatesSchema, + controls?: BroadcastControlSettings, + ): StatesWorkspace; + }; + + readonly notifications: { + /** + * Acquires a Notifications workspace from store or adds new one. + * + * @param workspaceAddress - Address of the requested Notifications Workspace + * @param requestedNotifications - Requested notifications for the workspace + * @returns A Notifications workspace + */ + getWorkspace( + notificationsId: WorkspaceAddress, + requestedNotifications: NotificationsSchema, + ): NotificationsWorkspace; + }; } diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index 7184fea44fef..c875ecf59c67 100644 --- a/packages/framework/presence/src/presenceDatastoreManager.ts +++ b/packages/framework/presence/src/presenceDatastoreManager.ts @@ -152,7 +152,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { private readonly runtime: IEphemeralRuntime, private readonly lookupClient: (clientId: AttendeeId) => Attendee, private readonly logger: ITelemetryLoggerExt | undefined, - private readonly events: IEmitter>, + private readonly events: IEmitter, systemWorkspaceDatastore: SystemWorkspaceDatastore, systemWorkspace: StatesWorkspaceEntry, ) { diff --git a/packages/framework/presence/src/presenceManager.ts b/packages/framework/presence/src/presenceManager.ts index 5960f5541475..e5a233009802 100644 --- a/packages/framework/presence/src/presenceManager.ts +++ b/packages/framework/presence/src/presenceManager.ts @@ -4,7 +4,7 @@ */ import { createEmitter } from "@fluid-internal/client-utils"; -import type { IEmitter } from "@fluidframework/core-interfaces/internal"; +import type { IEmitter, Listenable } from "@fluidframework/core-interfaces/internal"; import { createSessionId } from "@fluidframework/id-compressor/internal"; import type { ITelemetryLoggerExt, @@ -15,7 +15,7 @@ import { createChildMonitoringContext } from "@fluidframework/telemetry-utils/in import type { ClientConnectionId } from "./baseTypes.js"; import type { BroadcastControlSettings } from "./broadcastControls.js"; import type { IEphemeralRuntime } from "./internalTypes.js"; -import type { Attendee, AttendeeId, Presence, PresenceEvents } from "./presence.js"; +import type { AttendeesEvents, AttendeeId, Presence, PresenceEvents } from "./presence.js"; import type { PresenceDatastoreManager } from "./presenceDatastoreManager.js"; import { PresenceDatastoreManagerImpl } from "./presenceDatastoreManager.js"; import type { SystemWorkspace, SystemWorkspaceDatastore } from "./systemWorkspace.js"; @@ -43,7 +43,25 @@ class PresenceManager implements Presence, PresenceExtensionInterface { private readonly datastoreManager: PresenceDatastoreManager; private readonly systemWorkspace: SystemWorkspace; - public readonly events = createEmitter(); + public readonly events = createEmitter(); + + public readonly attendees: Presence["attendees"]; + + public readonly states = { + getWorkspace: ( + workspaceAddress: WorkspaceAddress, + requestedContent: TSchema, + settings?: BroadcastControlSettings, + ): StatesWorkspace => + this.datastoreManager.getWorkspace(`s:${workspaceAddress}`, requestedContent, settings), + }; + public readonly notifications = { + getWorkspace: ( + workspaceAddress: WorkspaceAddress, + requestedContent: TSchema, + ): StatesWorkspace => + this.datastoreManager.getWorkspace(`n:${workspaceAddress}`, requestedContent), + }; private readonly mc: MonitoringContext | undefined = undefined; @@ -60,6 +78,7 @@ class PresenceManager implements Presence, PresenceExtensionInterface { this.events, this.mc?.logger, ); + this.attendees = this.systemWorkspace; runtime.on("connected", this.onConnect.bind(this)); @@ -91,38 +110,6 @@ class PresenceManager implements Presence, PresenceExtensionInterface { private removeClientConnectionId(clientConnectionId: ClientConnectionId): void { this.systemWorkspace.removeClientConnectionId(clientConnectionId); } - - public getAttendees(): ReadonlySet { - return this.systemWorkspace.getAttendees(); - } - - public getAttendee(clientId: ClientConnectionId | AttendeeId): Attendee { - return this.systemWorkspace.getAttendee(clientId); - } - - public getMyself(): Attendee { - return this.systemWorkspace.getMyself(); - } - - public getStates( - workspaceAddress: WorkspaceAddress, - requestedContent: TSchema, - controls?: BroadcastControlSettings, - ): StatesWorkspace { - return this.datastoreManager.getWorkspace( - `s:${workspaceAddress}`, - requestedContent, - controls, - ); - } - - public getNotifications( - workspaceAddress: WorkspaceAddress, - requestedContent: TSchema, - ): StatesWorkspace { - return this.datastoreManager.getWorkspace(`n:${workspaceAddress}`, requestedContent); - } - /** * Check for Presence message and process it. * @@ -148,7 +135,8 @@ class PresenceManager implements Presence, PresenceExtensionInterface { function setupSubComponents( attendeeId: AttendeeId, runtime: IEphemeralRuntime, - events: IEmitter, + events: Listenable & + IEmitter, logger: ITelemetryLoggerExt | undefined, ): [PresenceDatastoreManager, SystemWorkspace] { const systemWorkspaceDatastore: SystemWorkspaceDatastore = { diff --git a/packages/framework/presence/src/systemWorkspace.ts b/packages/framework/presence/src/systemWorkspace.ts index 5fa474ba5b1f..1a18ac5368d7 100644 --- a/packages/framework/presence/src/systemWorkspace.ts +++ b/packages/framework/presence/src/systemWorkspace.ts @@ -4,13 +4,13 @@ */ import type { IAudience } from "@fluidframework/container-definitions"; -import type { IEmitter } from "@fluidframework/core-interfaces/internal"; +import type { IEmitter, Listenable } from "@fluidframework/core-interfaces/internal"; import { assert } from "@fluidframework/core-utils/internal"; import type { ClientConnectionId } from "./baseTypes.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; import type { PostUpdateAction } from "./internalTypes.js"; -import type { Attendee, AttendeeId, Presence, PresenceEvents } from "./presence.js"; +import type { Attendee, AttendeesEvents, AttendeeId, Presence } from "./presence.js"; import { AttendeeStatus } from "./presence.js"; import type { PresenceStatesInternal } from "./presenceStates.js"; import { TimerManager } from "./timerManager.js"; @@ -66,8 +66,8 @@ class SessionClient implements Attendee { */ export interface SystemWorkspace // Portion of Presence that is handled by SystemWorkspace along with - // responsiblity for emitting "attendeeJoined" events. - extends Pick { + // responsiblity for emitting "attendeeConnected" events. + extends Exclude { /** * Must be called when the current client acquires a new connection. * @@ -103,9 +103,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { public constructor( attendeeId: AttendeeId, private readonly datastore: SystemWorkspaceDatastore, - private readonly events: IEmitter< - Pick - >, + public readonly events: Listenable & IEmitter, private readonly audience: IAudience, ) { this.selfAttendee = new SessionClient(attendeeId); @@ -147,7 +145,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { ); // If the attendee is joining the session, add them to the list of joining attendees to be announced later. if (isJoining) { - postUpdateActions.push(() => this.events.emit("attendeeJoined", attendee)); + postUpdateActions.push(() => this.events.emit("attendeeConnected", attendee)); } const knownSessionId: InternalTypes.ValueRequiredState | undefined = @@ -295,7 +293,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { export function createSystemWorkspace( attendeeId: AttendeeId, datastore: SystemWorkspaceDatastore, - events: IEmitter>, + events: Listenable & IEmitter, audience: IAudience, ): { workspace: SystemWorkspace; diff --git a/packages/framework/presence/src/test/batching.spec.ts b/packages/framework/presence/src/test/batching.spec.ts index 76b5745994f1..4e11e6aa16bc 100644 --- a/packages/framework/presence/src/test/batching.spec.ts +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -143,7 +143,7 @@ describe("Presence", () => { // Configure a state workspace // SIGNAL #1 - intial data is sent immediately - const stateWorkspace = presence.getStates("name:testStateWorkspace", { + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), }); @@ -192,7 +192,7 @@ describe("Presence", () => { ]); // Configure a state workspace - presence.getStates("name:testStateWorkspace", { + presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), }); // will be queued; deadline is now 1070 @@ -266,7 +266,7 @@ describe("Presence", () => { ); // Configure a state workspace - const stateWorkspace = presence.getStates("name:testStateWorkspace", { + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), }); // will be queued; deadline is now 1070 @@ -368,7 +368,7 @@ describe("Presence", () => { ); // Configure a state workspace - const stateWorkspace = presence.getStates("name:testStateWorkspace", { + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), }); @@ -487,7 +487,7 @@ describe("Presence", () => { // Configure a state workspace // SIGNAL #1 - this signal is not queued because it contains a State object with a latency of 0, // so the initial data will be sent immediately. - const stateWorkspace = presence.getStates("name:testStateWorkspace", { + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), immediateUpdate: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), }); @@ -574,7 +574,7 @@ describe("Presence", () => { ); // Configure a state workspace - const stateWorkspace = presence.getStates("name:testStateWorkspace", { + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), note: StateFactory.latest({ message: "" }, { allowableUpdateLatencyMs: 50 }), }); // will be queued, deadline is set to 1060 @@ -646,11 +646,11 @@ describe("Presence", () => { ]); // Configure two state workspaces - const stateWorkspace = presence.getStates("name:testStateWorkspace", { + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), }); // will be queued, deadline is 1110 - const stateWorkspace2 = presence.getStates("name:testStateWorkspace2", { + const stateWorkspace2 = presence.states.getWorkspace("name:testStateWorkspace2", { note: StateFactory.latest({ message: "" }, { allowableUpdateLatencyMs: 60 }), }); // will be queued, deadline is 1070 @@ -731,10 +731,8 @@ describe("Presence", () => { // Configure a notifications workspace // eslint-disable-next-line @typescript-eslint/ban-types - const notificationsWorkspace: NotificationsWorkspace<{}> = presence.getNotifications( - "name:testNotificationWorkspace", - {}, - ); + const notificationsWorkspace: NotificationsWorkspace<{}> = + presence.notifications.getWorkspace("name:testNotificationWorkspace", {}); notificationsWorkspace.add( "testEvents", @@ -841,15 +839,13 @@ describe("Presence", () => { ); // Configure a state workspace - const stateWorkspace = presence.getStates("name:testStateWorkspace", { + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), }); // will be queued, deadline is 1110 // eslint-disable-next-line @typescript-eslint/ban-types - const notificationsWorkspace: NotificationsWorkspace<{}> = presence.getNotifications( - "name:testNotificationWorkspace", - {}, - ); + const notificationsWorkspace: NotificationsWorkspace<{}> = + presence.notifications.getWorkspace("name:testNotificationWorkspace", {}); notificationsWorkspace.add( "testEvents", diff --git a/packages/framework/presence/src/test/eventing.spec.ts b/packages/framework/presence/src/test/eventing.spec.ts index 5048ddde3c0a..779d39afa2e8 100644 --- a/packages/framework/presence/src/test/eventing.spec.ts +++ b/packages/framework/presence/src/test/eventing.spec.ts @@ -178,7 +178,7 @@ describe("Presence", () => { switch (manager) { case "latest": { assert.deepEqual( - latest.clientValue(attendee).value, + latest.getRemote(attendee).value, expectedValue, "Eventing does not reflect latest value", ); @@ -186,12 +186,12 @@ describe("Presence", () => { } case "latestMap": { assert.deepEqual( - latestMap.clientValue(attendee).get("key1")?.value, + latestMap.getRemote(attendee).get("key1")?.value, expectedValue.key1, "Eventing does not reflect latest map value", ); assert.deepEqual( - latestMap.clientValue(attendee).get("key2")?.value, + latestMap.getRemote(attendee).get("key2")?.value, expectedValue.key2, "Eventing does not reflect latest map value", ); @@ -241,7 +241,7 @@ describe("Presence", () => { function setupSharedStatesWorkspace({ notifications, }: { notifications?: true } = {}): void { - const states = presence.getStates("name:testWorkspace", { + const states = presence.states.getWorkspace("name:testWorkspace", { latest: StateFactory.latest({ x: 0, y: 0, z: 0 }), latestMap: StateFactory.latestMap({ key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }), }); @@ -260,10 +260,10 @@ describe("Presence", () => { } function setupMultipleStatesWorkspaces(): void { - const latestsStates = presence.getStates("name:testWorkspace1", { + const latestsStates = presence.states.getWorkspace("name:testWorkspace1", { latest: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); - const latesetMapStates = presence.getStates("name:testWorkspace2", { + const latesetMapStates = presence.states.getWorkspace("name:testWorkspace2", { latestMap: StateFactory.latestMap({ key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }), }); latest = latestsStates.props.latest; @@ -271,11 +271,14 @@ describe("Presence", () => { } function setupNotificationsWorkspace(): void { - const notificationsWorkspace = presence.getNotifications("name:testWorkspace", { - notifications: Notifications<{ newId: (id: number) => void }>({ - newId: (_attendee: Attendee, _id: number) => {}, - }), - }); + const notificationsWorkspace = presence.notifications.getWorkspace( + "name:testWorkspace", + { + notifications: Notifications<{ newId: (id: number) => void }>({ + newId: (_attendee: Attendee, _id: number) => {}, + }), + }, + ); notificationManager = notificationsWorkspace.props.notifications; } @@ -298,7 +301,7 @@ describe("Presence", () => { } function getTestAttendee(): Attendee { - return presence.getAttendee("attendeeId-1"); + return presence.attendees.getAttendee("attendeeId-1"); } describe("states workspace", () => { @@ -344,7 +347,7 @@ describe("Presence", () => { latest.events.on("updated", latestUpdatedEventSpy); latestMap.events.on("updated", latestMapUpdatedEventSpy); latestMap.events.on("itemUpdated", itemUpdatedEventSpy); - presence.events.on("attendeeJoined", atteendeeEventSpy); + presence.attendees.events.on("attendeeConnected", atteendeeEventSpy); } it("'latest' update comes before 'latestMap' update in single workspace", async () => { @@ -573,7 +576,7 @@ describe("Presence", () => { notificationManager.notifications.on("newId", notificationSpy); latest.events.on("updated", latestSpy); latestMap.events.on("updated", latestMapSpy); - presence.events.on("attendeeJoined", attendeeSpy); + presence.attendees.events.on("attendeeConnected", attendeeSpy); } function assertSpies(): void { @@ -651,11 +654,14 @@ describe("Presence", () => { notificationSpy = spy(); const workspaceActivatedEventSpy = spy((workspaceAddress: WorkspaceAddress) => { // Once activated, register the notifications workspace and listener for it's event - const notificationsWorkspace = presence.getNotifications(workspaceAddress, { - notifications: Notifications<{ newId: (id: number) => void }>({ - newId: (_attendee: Attendee, _id: number) => {}, - }), - }); + const notificationsWorkspace = presence.notifications.getWorkspace( + workspaceAddress, + { + notifications: Notifications<{ newId: (id: number) => void }>({ + newId: (_attendee: Attendee, _id: number) => {}, + }), + }, + ); notificationsWorkspace.props.notifications.notifications.on( "newId", notificationSpy, diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index ef9693cca6f0..aef6cf4daa3b 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -25,7 +25,7 @@ function createLatestMapManager( presence: Presence, valueControlSettings?: BroadcastControlSettings, ) { - const states = presence.getStates(testWorkspaceName, { + const states = presence.states.getWorkspace(testWorkspaceName, { fixedMap: StateFactory.latestMap( { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, valueControlSettings, @@ -51,7 +51,7 @@ describe("Presence", () => { string > { const presence = createPresenceManager(new MockEphemeralRuntime()); - const states = presence.getStates(testWorkspaceName, { + const states = presence.states.getWorkspace(testWorkspaceName, { fixedMap: StateFactory.latestMap({ key1: { x: 0, y: 0 } }), }); return states.props.fixedMap; @@ -98,12 +98,15 @@ describe("Presence", () => { export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const presence = {} as Presence; - const statesWorkspace = presence.getStates("name:testStatesWorkspaceWithLatestMap", { - fixedMap: StateFactory.latestMap({ - key1: { x: 0, y: 0 }, - key2: { ref: "default", someId: 0 }, - }), - }); + const statesWorkspace = presence.states.getWorkspace( + "name:testStatesWorkspaceWithLatestMap", + { + fixedMap: StateFactory.latestMap({ + key1: { x: 0, y: 0 }, + key2: { ref: "default", someId: 0 }, + }), + }, + ); // Workaround ts(2775): Assertions require every name in the call target to be declared with an explicit type annotation. const workspace: typeof statesWorkspace = statesWorkspace; const props = workspace.props; @@ -154,14 +157,14 @@ export function checkCompiles(): void { const pointerItemUpdatedOff = pointers.events.on("itemUpdated", logClientValue); pointerItemUpdatedOff(); - for (const attendee of pointers.clients()) { - const items = pointers.clientValue(attendee); + for (const attendee of pointers.getStateAttendees()) { + const items = pointers.getRemote(attendee); for (const [key, { value }] of items.entries()) { logClientValue({ attendee, key, value }); } } - for (const { attendee, items } of pointers.clientValues()) { + for (const { attendee, items } of pointers.getRemotes()) { for (const [key, { value }] of items.entries()) logClientValue({ attendee, key, value }); } diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index 1c0379123a70..533b01773611 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -24,7 +24,7 @@ function createLatestManager( presence: Presence, valueControlSettings?: BroadcastControlSettings, ) { - const states = presence.getStates(testWorkspaceName, { + const states = presence.states.getWorkspace(testWorkspaceName, { camera: StateFactory.latest({ x: 0, y: 0, z: 0 }, valueControlSettings), }); return states.props.camera; @@ -45,28 +45,28 @@ describe("Presence", () => { }); it("can set and get empty object as initial value", () => { - const states = presence.getStates(testWorkspaceName, { + const states = presence.states.getWorkspace(testWorkspaceName, { obj: StateFactory.latest({}), }); assert.deepStrictEqual(states.props.obj.local, {}); }); it("can set and get object with properties as initial value", () => { - const states = presence.getStates(testWorkspaceName, { + const states = presence.states.getWorkspace(testWorkspaceName, { obj: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); assert.deepStrictEqual(states.props.obj.local, { x: 0, y: 0, z: 0 }); }); it("can set and get empty array as initial value", () => { - const states = presence.getStates(testWorkspaceName, { + const states = presence.states.getWorkspace(testWorkspaceName, { arr: StateFactory.latest([]), }); assert.deepStrictEqual(states.props.arr.local, []); }); it("can set and get array with elements as initial value", () => { - const states = presence.getStates(testWorkspaceName, { + const states = presence.states.getWorkspace(testWorkspaceName, { arr: StateFactory.latest([1, 2, 3]), }); assert.deepStrictEqual(states.props.arr.local, [1, 2, 3]); @@ -78,7 +78,7 @@ describe("Presence", () => { it("localUpdate event is fired with new value when local value is updated", () => { // Setup const presence = createPresenceManager(new MockEphemeralRuntime()); - const states = presence.getStates(testWorkspaceName, { + const states = presence.states.getWorkspace(testWorkspaceName, { camera: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); const camera = states.props.camera; @@ -104,7 +104,7 @@ describe("Presence", () => { export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const presence = {} as Presence; - const statesWorkspace = presence.getStates("name:testStatesWorkspaceWithLatest", { + const statesWorkspace = presence.states.getWorkspace("name:testStatesWorkspaceWithLatest", { cursor: StateFactory.latest({ x: 0, y: 0 }), camera: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); @@ -139,12 +139,12 @@ export function checkCompiles(): void { ); cursorUpdatedOff(); - for (const attendee of cursor.clients()) { - logClientValue({ attendee, ...cursor.clientValue(attendee) }); + for (const attendee of cursor.getStateAttendees()) { + logClientValue({ attendee, ...cursor.getRemote(attendee) }); } // Enumerate all cursor values - for (const { attendee, value } of cursor.clientValues()) { + for (const { attendee, value } of cursor.getRemotes()) { logClientValue({ attendee, value }); } } diff --git a/packages/framework/presence/src/test/notificationsManager.spec.ts b/packages/framework/presence/src/test/notificationsManager.spec.ts index 454f66e6c8a9..b67e168d4d3a 100644 --- a/packages/framework/presence/src/test/notificationsManager.spec.ts +++ b/packages/framework/presence/src/test/notificationsManager.spec.ts @@ -48,7 +48,10 @@ describe("Presence", () => { presence = prepareConnectedPresence(runtime, "attendeeId-2", "client2", clock, logger); // Get a notifications workspace - notificationsWorkspace = presence.getNotifications("name:testNotificationWorkspace", {}); + notificationsWorkspace = presence.notifications.getWorkspace( + "name:testNotificationWorkspace", + {}, + ); }); afterEach(function (done: Mocha.Done) { @@ -187,7 +190,7 @@ describe("Presence", () => { ]); // Act & Verify - testEvents.emit.unicast("newId", presence.getMyself(), 42); + testEvents.emit.unicast("newId", presence.attendees.getMyself(), 42); assertFinalExpectations(runtime, logger); }); diff --git a/packages/framework/presence/src/test/presenceDatastoreManager.spec.ts b/packages/framework/presence/src/test/presenceDatastoreManager.spec.ts index 182e9584c3e9..10f2b2d9f9f0 100644 --- a/packages/framework/presence/src/test/presenceDatastoreManager.spec.ts +++ b/packages/framework/presence/src/test/presenceDatastoreManager.spec.ts @@ -314,8 +314,8 @@ describe("Presence", () => { // Setup const listener = spy(); presence.events.on("workspaceActivated", listener); - presence.getStates("name:testStateWorkspace", {}); - presence.getNotifications("name:testNotificationWorkspace", {}); + presence.states.getWorkspace("name:testStateWorkspace", {}); + presence.notifications.getWorkspace("name:testNotificationWorkspace", {}); // Act presence.processSignal( diff --git a/packages/framework/presence/src/test/presenceManager.spec.ts b/packages/framework/presence/src/test/presenceManager.spec.ts index 0bcc35dd9967..8889b1cdf1d0 100644 --- a/packages/framework/presence/src/test/presenceManager.spec.ts +++ b/packages/framework/presence/src/test/presenceManager.spec.ts @@ -72,7 +72,7 @@ describe("Presence", () => { const presence = createPresenceManager(runtime); // Act & Verify - assert.throws(() => presence.getAttendee("unknown"), /Attendee not found/); + assert.throws(() => presence.attendees.getAttendee("unknown"), /Attendee not found/); }); describe("when connected", () => { @@ -100,14 +100,17 @@ describe("Presence", () => { let initialAttendeeSignal: ReturnType; let rejoinAttendeeSignal: ReturnType; - // Processes join signals and returns the attendees that were announced via `attendeeJoined` + // Processes join signals and returns the attendees that were announced via `attendeeConnected` function processJoinSignals( signals: ReturnType[], ): Attendee[] { const joinedAttendees: Attendee[] = []; - const cleanUpListener = presence.events.on("attendeeJoined", (attendee) => { - joinedAttendees.push(attendee); - }); + const cleanUpListener = presence.attendees.events.on( + "attendeeConnected", + (attendee) => { + joinedAttendees.push(attendee); + }, + ); for (const signal of signals) { presence.processSignal("", signal, false); @@ -164,7 +167,7 @@ describe("Presence", () => { it("is not announced via `attendeeDisconnected` when unknown connection is removed", () => { // Setup - presence.events.on("attendeeDisconnected", () => { + presence.attendees.events.on("attendeeDisconnected", () => { assert.fail( "`attendeeDisconnected` should not be emitted for unknown connection.", ); @@ -175,7 +178,7 @@ describe("Presence", () => { }); describe("that is joining", () => { - it('first time is announced via `attendeeJoined` with status "Connected"', () => { + it('first time is announced via `attendeeConnected` with status "Connected"', () => { // Act - simulate join message from client const joinedAttendees = processJoinSignals([initialAttendeeSignal]); // Verify @@ -187,7 +190,7 @@ describe("Presence", () => { verifyAttendee(joinedAttendees[0], initialAttendeeConnectionId, attendeeSessionId); }); - it('second time is announced once via `attendeeJoined` with status "Connected" when prior is unknown', () => { + it('second time is announced once via `attendeeConnected` with status "Connected" when prior is unknown', () => { // Setup runtime.removeMember(initialAttendeeConnectionId); @@ -203,7 +206,7 @@ describe("Presence", () => { verifyAttendee(joinedAttendees[0], rejoinAttendeeConnectionId, attendeeSessionId); }); - it('second time is announced once via `attendeeJoined` with status "Connected" when prior is still connected', () => { + it('second time is announced once via `attendeeConnected` with status "Connected" when prior is still connected', () => { // Act - simulate join message from client const joinedAttendees = processJoinSignals([rejoinAttendeeSignal]); @@ -217,7 +220,7 @@ describe("Presence", () => { verifyAttendee(joinedAttendees[0], rejoinAttendeeConnectionId, attendeeSessionId); }); - it('first time is announced via `attendeeJoined` with status "Connected" even if unknown to audience', () => { + it('first time is announced via `attendeeConnected` with status "Connected" even if unknown to audience', () => { // Setup - remove connection from audience runtime.removeMember(initialAttendeeConnectionId); @@ -234,7 +237,7 @@ describe("Presence", () => { verifyAttendee(joinedAttendees[0], initialAttendeeConnectionId, attendeeSessionId); }); - it('second time is announced once via `attendeeJoined` with status "Connected" even if most recent unknown to audience', () => { + it('second time is announced once via `attendeeConnected` with status "Connected" even if most recent unknown to audience', () => { // Setup - remove connection from audience runtime.removeMember(rejoinAttendeeConnectionId); @@ -249,7 +252,7 @@ describe("Presence", () => { verifyAttendee(joinedAttendees[0], rejoinAttendeeConnectionId, attendeeSessionId); }); - it("as collateral and disconnected is NOT announced via `attendeeJoined`", () => { + it("as collateral and disconnected is NOT announced via `attendeeConnected`", () => { // Setup - remove connections from audience const collateralAttendeeConnectionId = "client3"; const collateralAttendeeSignal = generateBasicClientJoin(clock.now - 10, { @@ -283,7 +286,7 @@ describe("Presence", () => { verifyAttendee(joinedAttendees[0], rejoinAttendeeConnectionId, attendeeSessionId); }); - it("as collateral with old connection info and connected is NOT announced via `attendeeJoined`", () => { + it("as collateral with old connection info and connected is NOT announced via `attendeeConnected`", () => { // Setup - generate signals // Both connection Id's unkonwn to audience @@ -369,7 +372,7 @@ describe("Presence", () => { it('is NOT announced when "rejoined" with same connection (duplicate signal)', () => { afterCleanUp.push( - presence.events.on("attendeeJoined", (attendee) => { + presence.attendees.events.on("attendeeConnected", (attendee) => { assert.fail( "Attendee should not be announced when rejoining with same connection", ); @@ -382,12 +385,12 @@ describe("Presence", () => { }); // To retain symmetry across Joined and Disconnected events, do not announce - // attendeeJoined when the attendee is already connected and we only see + // attendeeConnected when the attendee is already connected and we only see // a connection id update. This can happen when audience removal is late. - it('is not announced via `attendeeJoined` when already "Connected"', () => { + it('is not announced via `attendeeConnected` when already "Connected"', () => { // Setup afterCleanUp.push( - presence.events.on("attendeeJoined", () => { + presence.attendees.events.on("attendeeConnected", () => { assert.fail("No attendee should be announced in join processing"); }), ); @@ -411,7 +414,7 @@ describe("Presence", () => { setup(); // Act - const attendee = presence.getAttendee(id); + const attendee = presence.attendees.getAttendee(id); // Verify assert.equal(attendee, knownAttendee, "`getAttendee` returned wrong attendee"); @@ -429,7 +432,7 @@ describe("Presence", () => { setup(); // Act - const attendees = presence.getAttendees(); + const attendees = presence.attendees.getAttendees(); assert( attendees.has(knownAttendee), "`getAttendees` set does not contain attendee", @@ -454,8 +457,8 @@ describe("Presence", () => { assert(knownAttendee !== undefined, "No attendee was set in beforeEach"); remoteDisconnectedAttendees = []; afterCleanUp.push( - presence.events.on("attendeeDisconnected", (attendee) => { - if (attendee !== presence.getMyself()) { + presence.attendees.events.on("attendeeDisconnected", (attendee) => { + if (attendee !== presence.attendees.getMyself()) { remoteDisconnectedAttendees.push(attendee); } }), @@ -531,9 +534,9 @@ describe("Presence", () => { // Setup - fail if attendee joined is announced afterCleanUp.push( - presence.events.on("attendeeJoined", () => { + presence.attendees.events.on("attendeeConnected", () => { assert.fail( - "No `attendeeJoined` should be announced for rejoining attendee that's already 'Connected'", + "No `attendeeConnected` should be announced for rejoining attendee that's already 'Connected'", ); }), ); @@ -546,7 +549,7 @@ describe("Presence", () => { processJoinSignals([rejoinAttendeeSignal]); clock.tick(600_000); - // Verify - rejoining attendee should still be 'Connected' with no `attendeeJoined` announced + // Verify - rejoining attendee should still be 'Connected' with no `attendeeConnected` announced assert.strictEqual( knownAttendee.getConnectionStatus(), AttendeeStatus.Connected, @@ -559,9 +562,9 @@ describe("Presence", () => { // Setup - fail if attendee joined is announced afterCleanUp.push( - presence.events.on("attendeeJoined", () => { + presence.attendees.events.on("attendeeConnected", () => { assert.fail( - "No `attendeeJoined` should be announced for active attendee that's already 'Connected'", + "No `attendeeConnected` should be announced for active attendee that's already 'Connected'", ); }), ); @@ -682,7 +685,7 @@ describe("Presence", () => { assert(knownAttendee !== undefined, "No attendee was set in beforeEach"); let disconnectedAttendee: Attendee | undefined; afterCleanUp.push( - presence.events.on("attendeeDisconnected", (attendee) => { + presence.attendees.events.on("attendeeDisconnected", (attendee) => { assert( disconnectedAttendee === undefined, "Only one attendee should be disconnected", @@ -719,7 +722,7 @@ describe("Presence", () => { runtime.removeMember(initialAttendeeConnectionId); afterCleanUp.push( - presence.events.on("attendeeDisconnected", (attendee) => { + presence.attendees.events.on("attendeeDisconnected", (attendee) => { assert.fail( "`attendeeDisconnected` should not be emitted for already disconnected attendee", ); @@ -754,7 +757,7 @@ describe("Presence", () => { it("is NOT announced when rejoined with same connection (duplicate signal)", () => { // Setup afterCleanUp.push( - presence.events.on("attendeeJoined", (attendee) => { + presence.attendees.events.on("attendeeConnected", (attendee) => { assert.fail( "Attendee should not be announced when rejoining with same connection", ); @@ -778,14 +781,18 @@ describe("Presence", () => { // Verify - session id is unchanged and connection id is updated verifyAttendee(priorAttendee, rejoinAttendeeConnectionId, attendeeSessionId); // Attendee is available via new connection id - const attendeeViaUpdatedId = presence.getAttendee(rejoinAttendeeConnectionId); + const attendeeViaUpdatedId = presence.attendees.getAttendee( + rejoinAttendeeConnectionId, + ); assert.equal( attendeeViaUpdatedId, priorAttendee, "getAttendee returned wrong attendee for updated connection id", ); // Attendee is available via old connection id - const attendeeViaOriginalId = presence.getAttendee(initialAttendeeConnectionId); + const attendeeViaOriginalId = presence.attendees.getAttendee( + initialAttendeeConnectionId, + ); assert.equal( attendeeViaOriginalId, priorAttendee, diff --git a/packages/framework/presence/src/test/presenceStates.spec.ts b/packages/framework/presence/src/test/presenceStates.spec.ts index 253c91b5cfd7..880b5713c142 100644 --- a/packages/framework/presence/src/test/presenceStates.spec.ts +++ b/packages/framework/presence/src/test/presenceStates.spec.ts @@ -21,7 +21,7 @@ describe("Presence", () => { it("API use compiles", () => {}); addControlsTests((presence, controlSettings) => { - return presence.getStates("name:testWorkspaceA", {}, controlSettings); + return presence.states.getWorkspace("name:testWorkspaceA", {}, controlSettings); }); }); }); @@ -47,7 +47,7 @@ declare function createValueManager( export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const presence = {} as Presence; - const statesWorkspace = presence.getStates("name:testWorkspaceA", { + const statesWorkspace = presence.states.getWorkspace("name:testWorkspaceA", { cursor: createValueManager({ x: 0, y: 0 }), // eslint-disable-next-line prefer-object-spread camera: Object.assign({ instanceBase: undefined as unknown as new () => unknown }, () => ({ diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts index 0f95769e327a..5cdee9d43b01 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/childClient.ts @@ -159,21 +159,25 @@ class MessageHandler { this.containerId = containerId; // Listen for presence events to notify parent/orchestrator when a new attendee joins or leaves the session. - presence.events.on("attendeeJoined", (attendee: Attendee) => { + presence.attendees.events.on("attendeeConnected", (attendee: Attendee) => { const m: MessageToParent = { - event: "attendeeJoined", + event: "attendeeConnected", attendeeId: attendee.attendeeId, }; send(m); }); - presence.events.on("attendeeDisconnected", (attendee: Attendee) => { + presence.attendees.events.on("attendeeDisconnected", (attendee: Attendee) => { const m: MessageToParent = { event: "attendeeDisconnected", attendeeId: attendee.attendeeId, }; send(m); }); - send({ event: "ready", containerId, attendeeId: presence.getMyself().attendeeId }); + send({ + event: "ready", + containerId, + attendeeId: presence.attendees.getMyself().attendeeId, + }); break; } @@ -192,7 +196,7 @@ class MessageHandler { this.container.disconnect(); send({ event: "disconnectedSelf", - attendeeId: this.presence.getMyself().attendeeId, + attendeeId: this.presence.attendees.getMyself().attendeeId, }); break; diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts index a7d55abb688f..42ee16e5f9eb 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/messageTypes.ts @@ -20,7 +20,7 @@ interface DisconnectSelfCommand { export type MessageFromChild = | AttendeeDisconnectedEvent - | AttendeeJoinedEvent + | attendeeConnectedEvent | ReadyEvent | DisconnectedSelfEvent | ErrorEvent; @@ -29,8 +29,8 @@ interface AttendeeDisconnectedEvent { attendeeId: AttendeeId; } -interface AttendeeJoinedEvent { - event: "attendeeJoined"; +interface attendeeConnectedEvent { + event: "attendeeConnected"; attendeeId: AttendeeId; } diff --git a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts index 4a0f185d6d52..acb25f95ee8d 100644 --- a/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts +++ b/packages/service-clients/end-to-end-tests/azure-client/src/test/multiprocess/presenceTest.spec.ts @@ -33,7 +33,7 @@ import type { MessageFromChild, MessageToChild } from "./messageTypes.js"; * - Send response messages including any relevant data back to the orchestrator to verify expected behavior. * * This particular test suite tests the following E2E functionality for Presence: - * - Announce 'attendeeJoined' when remote client joins session. + * - Announce 'attendeeConnected' when remote client joins session. * - Announce 'attendeeDisconnected' when remote client disconnects. */ describe(`Presence with AzureClient`, () => { @@ -119,13 +119,13 @@ describe(`Presence with AzureClient`, () => { afterCleanUp.length = 0; }); - it("announces 'attendeeJoined' when remote client joins session and 'attendeeDisconnected' when remote client disconnects", async () => { + it("announces 'attendeeConnected' when remote client joins session and 'attendeeDisconnected' when remote client disconnects", async () => { // Setup - const attendeeJoinedPromise = timeoutPromise( + const attendeeConnectedPromise = timeoutPromise( (resolve) => { let attendeesJoinedEvents = 0; children[0].on("message", (msg: MessageFromChild) => { - if (msg.event === "attendeeJoined") { + if (msg.event === "attendeeConnected") { attendeesJoinedEvents++; if (attendeesJoinedEvents === numClients - 1) { resolve(); @@ -135,15 +135,15 @@ describe(`Presence with AzureClient`, () => { }, { durationMs, - errorMsg: "did not receive all 'attendeeJoined' events", + errorMsg: "did not receive all 'attendeeConnected' events", }, ); // Act - connect all child processes const creatorSessionId = await connectChildProcesses(children); - // Verify - wait for all 'attendeeJoined' events - await Promise.race([attendeeJoinedPromise, childErrorPromise]); + // Verify - wait for all 'attendeeConnected' events + await Promise.race([attendeeConnectedPromise, childErrorPromise]); // Setup const waitForDisconnected = children From 90c351917b8d6f0d34d543b2c0df21f0c4cf2e91 Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Wed, 16 Apr 2025 18:45:18 +0200 Subject: [PATCH 4/6] Update examples/service-clients/azure-client/external-controller/src/presence.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../azure-client/external-controller/src/presence.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/service-clients/azure-client/external-controller/src/presence.ts b/examples/service-clients/azure-client/external-controller/src/presence.ts index d6622b4f282d..57cf05dd5cfc 100644 --- a/examples/service-clients/azure-client/external-controller/src/presence.ts +++ b/examples/service-clients/azure-client/external-controller/src/presence.ts @@ -27,7 +27,7 @@ export interface DiceValues { * If any part of the data is updated, then the entire data structure is shared. This means * keeping a local copy of the data structure or recomposing it each time making an update. * - * The second state, lastDiceRolls, is using {@link @fluidframework/presence#LatestMap| LatesttMap} where + * The second state, lastDiceRolls, is using {@link @fluidframework/presence#LatestMap| LatestMap} where * each die is updated independently. This allows for more granular updates, but also requires * more verbose setting/reading logic and use of boxed values (e.g. `{ value: DieValue}`). This * pattern more directly lends itself to handling arbitrary numbers of dice. From ac08bd0dd216e408ff0009dd7ee913cfbc327b58 Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Wed, 16 Apr 2025 20:11:19 +0200 Subject: [PATCH 5/6] refactor(client-presence): Rename LatestEvents and LatestMapEvents to include `remote` (#24387) ## Description | Original | New | |----------|-----| | `LatestEvents.updated` | `LatestEvents.remoteUpdated` | | `LatestMapEvents.itemRemoved` | `LatestMapEvents.remoteItemRemoved` | | `LatestMapEvents.itemUpdated` | `LatestMapEvents.remoteItemUpdated` | | `LatestMapEvents.updated` | `LatestMapEvents.remoteUpdated` | --- .changeset/sour-mirrors-wave.md | 4 ++++ examples/apps/ai-collab/src/app/presence.ts | 2 +- .../apps/presence-tracker/src/FocusTracker.ts | 2 +- .../apps/presence-tracker/src/MouseTracker.ts | 2 +- .../external-controller/src/app.ts | 2 +- .../external-controller/src/view.ts | 2 +- .../presence/api-report/presence.alpha.api.md | 12 +++++----- .../presence/src/latestMapValueManager.ts | 12 +++++----- .../presence/src/latestValueManager.ts | 4 ++-- .../presence/src/test/eventing.spec.ts | 22 +++++++++---------- .../src/test/latestMapValueManager.spec.ts | 6 ++--- .../src/test/latestValueManager.spec.ts | 2 +- 12 files changed, 38 insertions(+), 34 deletions(-) diff --git a/.changeset/sour-mirrors-wave.md b/.changeset/sour-mirrors-wave.md index d2f4d3c774ec..e5ec4cf41427 100644 --- a/.changeset/sour-mirrors-wave.md +++ b/.changeset/sour-mirrors-wave.md @@ -25,8 +25,12 @@ The following API changes have been made to improve clarity and consistency: | `ISessionClient` | `Attendee` | | `Latest` (import) | `StateFactory` | | `Latest` (call) | `StateFactory.latest` | +| `LatestEvents.updated` | `LatestEvents.remoteUpdated` | | `LatestMap` (import) | `StateFactory` | | `LatestMap` (call) | `StateFactory.latestMap` | +| `LatestMapEvents.itemRemoved` | `LatestMapEvents.remoteItemRemoved` | +| `LatestMapEvents.itemUpdated` | `LatestMapEvents.remoteItemUpdated` | +| `LatestMapEvents.updated` | `LatestMapEvents.remoteUpdated` | | `LatestMapItemValueClientData` | `LatestMapItemUpdatedClientData` | | `LatestMapValueClientData` | `LatestMapClientData` | | `LatestMapValueManager` | `LatestMap` | diff --git a/examples/apps/ai-collab/src/app/presence.ts b/examples/apps/ai-collab/src/app/presence.ts index ab5b2bc21853..0f27f7b4c631 100644 --- a/examples/apps/ai-collab/src/app/presence.ts +++ b/examples/apps/ai-collab/src/app/presence.ts @@ -48,7 +48,7 @@ export class PresenceManager { ); // Listen for updates to the userInfo property in the presence state - this.usersState.props.onlineUsers.events.on("updated", (update) => { + this.usersState.props.onlineUsers.events.on("remoteUpdated", (update) => { // The remote client that updated the userInfo property const remoteSessionClient = update.attendee; // The new value of the userInfo property diff --git a/examples/apps/presence-tracker/src/FocusTracker.ts b/examples/apps/presence-tracker/src/FocusTracker.ts index 5b30823bdbbb..17127b391fcb 100644 --- a/examples/apps/presence-tracker/src/FocusTracker.ts +++ b/examples/apps/presence-tracker/src/FocusTracker.ts @@ -64,7 +64,7 @@ export class FocusTracker extends TypedEventEmitter { this.focus = statesWorkspace.props.focus; // When the focus state is updated, the FocusTracker should emit the focusChanged event. - this.focus.events.on("updated", ({ attendee, value }) => { + this.focus.events.on("remoteUpdated", ({ attendee, value }) => { this.emit("focusChanged", this.focus.local); }); diff --git a/examples/apps/presence-tracker/src/MouseTracker.ts b/examples/apps/presence-tracker/src/MouseTracker.ts index 8d01e15bbd41..251d7dd6c571 100644 --- a/examples/apps/presence-tracker/src/MouseTracker.ts +++ b/examples/apps/presence-tracker/src/MouseTracker.ts @@ -61,7 +61,7 @@ export class MouseTracker extends TypedEventEmitter { this.cursor = statesWorkspace.props.cursor; // When the cursor state is updated, the MouseTracker should emit the mousePositionChanged event. - this.cursor.events.on("updated", () => { + this.cursor.events.on("remoteUpdated", () => { this.emit("mousePositionChanged"); }); 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 c3347449fe7d..2e0b15e3d8ae 100644 --- a/examples/service-clients/azure-client/external-controller/src/app.ts +++ b/examples/service-clients/azure-client/external-controller/src/app.ts @@ -216,7 +216,7 @@ async function start(): Promise { // lastDiceRolls is here just to demonstrate an example of LatestMap // Its updates are only logged to the console. - states.lastDiceRolls.events.on("itemUpdated", (update) => { + states.lastDiceRolls.events.on("remoteItemUpdated", (update) => { console.log( `Client ${update.attendee.attendeeId.slice(0, 8)}'s ${update.key} rolled to ${update.value.value}`, ); 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 b780d6ad2b6f..ad8c29d77b95 100644 --- a/examples/service-clients/azure-client/external-controller/src/view.ts +++ b/examples/service-clients/azure-client/external-controller/src/view.ts @@ -186,7 +186,7 @@ function makePresenceView( } logDiv.append(logHeaderDiv, logContentDiv); - presenceConfig.lastRoll.events.on("updated", (update) => { + presenceConfig.lastRoll.events.on("remoteUpdated", (update) => { const connected = update.attendee.getConnectionStatus() === "Connected" ? "🔗" : "⛓️‍💥"; const updateText = `updated ${update.attendee.attendeeId.slice(0, 8)}'s ${connected} last rolls to ${JSON.stringify(update.value)}`; addLogEntry(logContentDiv, updateText); diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 4d75684fb5d7..9b108fe2f340 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -170,7 +170,7 @@ export interface LatestEvents { value: InternalUtilityTypes.FullyReadonly & JsonDeserialized>; }) => void; // @eventProperty - updated: (update: LatestClientData) => void; + remoteUpdated: (update: LatestClientData) => void; } // @alpha @sealed @@ -197,10 +197,6 @@ export interface LatestMapClientData { - // @eventProperty - itemRemoved: (removedItem: LatestMapItemRemovedClientData) => void; - // @eventProperty - itemUpdated: (updatedItem: LatestMapItemUpdatedClientData) => void; // @eventProperty localItemRemoved: (removedItem: { key: K; @@ -211,7 +207,11 @@ export interface LatestMapEvents { key: K; }) => void; // @eventProperty - updated: (updates: LatestMapClientData) => void; + remoteItemRemoved: (removedItem: LatestMapItemRemovedClientData) => void; + // @eventProperty + remoteItemUpdated: (updatedItem: LatestMapItemUpdatedClientData) => void; + // @eventProperty + remoteUpdated: (updates: LatestMapClientData) => void; } // @alpha @sealed diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 61adc236603e..840905de1c28 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -81,7 +81,7 @@ export interface LatestMapEvents { * * @eventProperty */ - updated: (updates: LatestMapClientData) => void; + remoteUpdated: (updates: LatestMapClientData) => void; /** * Raised when specific item's value of remote client is updated. @@ -89,7 +89,7 @@ export interface LatestMapEvents { * * @eventProperty */ - itemUpdated: (updatedItem: LatestMapItemUpdatedClientData) => void; + remoteItemUpdated: (updatedItem: LatestMapItemUpdatedClientData) => void; /** * Raised when specific item of remote client is removed. @@ -97,7 +97,7 @@ export interface LatestMapEvents { * * @eventProperty */ - itemRemoved: (removedItem: LatestMapItemRemovedClientData) => void; + remoteItemRemoved: (removedItem: LatestMapItemRemovedClientData) => void; /** * Raised when specific local item's value is updated. @@ -462,11 +462,11 @@ class LatestMapValueManagerImpl< value: itemValue, metadata, }; - postUpdateActions.push(() => this.events.emit("itemUpdated", updatedItem)); + postUpdateActions.push(() => this.events.emit("remoteItemUpdated", updatedItem)); allUpdates.items.set(key, { value: itemValue, metadata }); } else if (hadPriorValue !== undefined) { postUpdateActions.push(() => - this.events.emit("itemRemoved", { + this.events.emit("remoteItemRemoved", { attendee, key, metadata, @@ -475,7 +475,7 @@ class LatestMapValueManagerImpl< } } this.datastore.update(this.key, attendeeId, currentState); - postUpdateActions.push(() => this.events.emit("updated", allUpdates)); + postUpdateActions.push(() => this.events.emit("remoteUpdated", allUpdates)); return postUpdateActions; } } diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 2c8943d2dc12..7fe34e4e5578 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -32,7 +32,7 @@ export interface LatestEvents { * * @eventProperty */ - updated: (update: LatestClientData) => void; + remoteUpdated: (update: LatestClientData) => void; /** * Raised when local client's value is updated, which may be the same value. @@ -163,7 +163,7 @@ class LatestValueManagerImpl this.datastore.update(this.key, attendeeId, value); return [ () => - this.events.emit("updated", { + this.events.emit("remoteUpdated", { attendee, value: value.value, metadata: { revision: value.rev, timestamp: value.timestamp }, diff --git a/packages/framework/presence/src/test/eventing.spec.ts b/packages/framework/presence/src/test/eventing.spec.ts index 779d39afa2e8..5e1a283c6d19 100644 --- a/packages/framework/presence/src/test/eventing.spec.ts +++ b/packages/framework/presence/src/test/eventing.spec.ts @@ -344,9 +344,9 @@ describe("Presence", () => { itemUpdatedEventSpy = spy(verify); atteendeeEventSpy = spy(verify); - latest.events.on("updated", latestUpdatedEventSpy); - latestMap.events.on("updated", latestMapUpdatedEventSpy); - latestMap.events.on("itemUpdated", itemUpdatedEventSpy); + latest.events.on("remoteUpdated", latestUpdatedEventSpy); + latestMap.events.on("remoteUpdated", latestMapUpdatedEventSpy); + latestMap.events.on("remoteItemUpdated", itemUpdatedEventSpy); presence.attendees.events.on("attendeeConnected", atteendeeEventSpy); } @@ -423,9 +423,9 @@ describe("Presence", () => { itemRemovedEventSpy = spy(verify); latestUpdatedEventSpy = spy(verify); latestMapUpdatedEventSpy = spy(verify); - latest.events.on("updated", latestUpdatedEventSpy); - latestMap.events.on("updated", latestMapUpdatedEventSpy); - latestMap.events.on("itemRemoved", itemRemovedEventSpy); + latest.events.on("remoteUpdated", latestUpdatedEventSpy); + latestMap.events.on("remoteUpdated", latestMapUpdatedEventSpy); + latestMap.events.on("remoteItemRemoved", itemRemovedEventSpy); } function assertSpies(): void { @@ -495,9 +495,9 @@ describe("Presence", () => { latestMapUpdatedEventSpy = spy(verify); itemUpdatedEventSpy = spy(verify); - latestMap.events.on("updated", latestMapUpdatedEventSpy); - latestMap.events.on("itemUpdated", itemUpdatedEventSpy); - latestMap.events.on("itemRemoved", itemRemovedEventSpy); + latestMap.events.on("remoteUpdated", latestMapUpdatedEventSpy); + latestMap.events.on("remoteItemUpdated", itemUpdatedEventSpy); + latestMap.events.on("remoteItemRemoved", itemRemovedEventSpy); } function assertSpies(): void { @@ -574,8 +574,8 @@ describe("Presence", () => { latestMapSpy = spy(verify); notificationManager.notifications.on("newId", notificationSpy); - latest.events.on("updated", latestSpy); - latestMap.events.on("updated", latestMapSpy); + latest.events.on("remoteUpdated", latestSpy); + latestMap.events.on("remoteUpdated", latestMapSpy); presence.attendees.events.on("attendeeConnected", attendeeSpy); } diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index aef6cf4daa3b..b8a024bdd378 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -154,7 +154,7 @@ export function checkCompiles(): void { localPointers.set("pen", { x: 1, y: 2 }); - const pointerItemUpdatedOff = pointers.events.on("itemUpdated", logClientValue); + const pointerItemUpdatedOff = pointers.events.on("remoteItemUpdated", logClientValue); pointerItemUpdatedOff(); for (const attendee of pointers.getStateAttendees()) { @@ -168,11 +168,11 @@ export function checkCompiles(): void { for (const [key, { value }] of items.entries()) logClientValue({ attendee, key, value }); } - pointers.events.on("itemRemoved", ({ attendee, key }) => + pointers.events.on("remoteItemRemoved", ({ attendee, key }) => logClientValue({ attendee, key, value: "" }), ); - pointers.events.on("updated", ({ attendee, items }) => { + pointers.events.on("remoteUpdated", ({ attendee, items }) => { for (const [key, { value }] of items.entries()) logClientValue({ attendee, key, value }); }); } diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index 533b01773611..39718f9e85dc 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -134,7 +134,7 @@ export function checkCompiles(): void { cursor.local = { x: 1, y: 2 }; // Listen to others cursor updates - const cursorUpdatedOff = cursor.events.on("updated", ({ attendee, value }) => + const cursorUpdatedOff = cursor.events.on("remoteUpdated", ({ attendee, value }) => console.log(`attendee ${attendee.attendeeId}'s cursor is now at (${value.x},${value.y})`), ); cursorUpdatedOff(); From e11ef4be4b3f1d04690da9b822da544382428633 Mon Sep 17 00:00:00 2001 From: Jason Hartman Date: Wed, 16 Apr 2025 23:31:18 -0700 Subject: [PATCH 6/6] docs(client-presence): apply review suggestions and one more correction --- .changeset/sour-mirrors-wave.md | 11 +++++------ docs/docs/build/presence.md | 16 ++++++++-------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/.changeset/sour-mirrors-wave.md b/.changeset/sour-mirrors-wave.md index e5ec4cf41427..89784ab23d5d 100644 --- a/.changeset/sour-mirrors-wave.md +++ b/.changeset/sour-mirrors-wave.md @@ -1,15 +1,13 @@ --- "@fluidframework/presence": minor ---- ---- -"section": other +"__section": other --- -Presence API renames +Presence APIs have been renamed The following API changes have been made to improve clarity and consistency: -| Original | New | +| Before 2.33.0 | 2.33.0 | |----------|-----| | `acquirePresence` | `getPresence` | | `acquirePresenceViaDataObject` | `getPresenceViaDataObject` | @@ -58,4 +56,5 @@ The following API changes have been made to improve clarity and consistency: | `SessionClientStatus` | `AttendeeStatus` | | `ValueMap` | `StateMap` | -Note: To fully replace OLD `Latest` and `LatestMap` functions, you should import `StateFactory` and call `StateFactory.latest` and `StateFactory.latestMap` respectively. NEW `Latest` and `LatestMap` APIs replace `LatestValueManager` and `LatestMapValueManager`. +> [!NOTE] +> To fully replace the former `Latest` and `LatestMap` functions, you should import `StateFactory` and call `StateFactory.latest` and `StateFactory.latestMap` respectively. The new `Latest` and `LatestMap` APIs replace `LatestValueManager` and `LatestMapValueManager` respectively. diff --git a/docs/docs/build/presence.md b/docs/docs/build/presence.md index dfcfa876575a..a77de4274d99 100644 --- a/docs/docs/build/presence.md +++ b/docs/docs/build/presence.md @@ -19,13 +19,13 @@ The key scenarios that the new Presence APIs are suitable for includes: ## Concepts -A session is a period of time when one or more clients are connected to a Fluid service. Session data and messages may be exchanged among clients, but will disappear once the no clients remain. (More specifically once no clients remain that have acquired the session `Presence` interface.) Once fully implemented, no client will require container write permissions to use Presence features. +A session is a period of time when one or more clients are connected to a Fluid service. Session data and messages may be exchanged among clients, but will disappear once no clients remain. (More specifically once no clients remain that have acquired the session `Presence` interface.) Once fully implemented, no client will require container write permissions to use Presence features. ### Attendees For the lifetime of a session, each client connecting will be established as a unique and stable `Attendee`. The representation is stable because it will remain the same `Attendee` instance independent of connection drops and reconnections. -Client Ids maintained by `Attendee` may be used to associate `Attendee` with quorum, audience, and service audience members. +Client IDs maintained by `Attendee` may be used to associate `Attendee` with quorum, audience, and service audience members. ### Workspaces @@ -35,25 +35,25 @@ There are two types of workspaces: States and Notifications. #### States Workspace -A `StatesWorkspace`, allows sharing of simple data across attendees where each attendee maintains their own data values that others may read, but not change. This is distinct from a Fluid DDS where data values might be manipulated by multiple clients and one ultimate value is derived. Shared, independent values are maintained by State objects that specialize in incrementality and history of values. +A `StatesWorkspace` allows sharing of simple data across attendees where each attendee maintains their own data values that others may read, but not change. This is distinct from a Fluid DDS where data values might be manipulated by multiple clients and one ultimate value is derived. Shared, independent values are maintained by State objects that specialize in incrementality and history of values. #### Notifications Workspace -A `NotificationsWorkspace`, is similar to states workspace, but is dedicated to notification use-cases via `NotificationsManager`. +A `NotificationsWorkspace` is similar to states workspace, but is dedicated to notification use-cases via `NotificationsManager`. ### States #### Latest -`Latest` retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `StatesWorkspace`. +`Latest` retains the most recent atomic value each attendee has shared. Use `StateFactory.latest` to add one to `StatesWorkspace`. #### LatestMap -`LatestMap` retains the most recent atomic value each attendee has shared under arbitrary keys. Values associated with a key may be nullified (appears as deleted). Use `StateFactory.latest` to add one to `StatesWorkspace`. +`LatestMap` retains the most recent atomic value each attendee has shared under arbitrary keys. Values associated with a key may be nullified to represent deletetion. Use `StateFactory.latestMap` to add one to a `StatesWorkspace`. #### NotificationsManager -Notifications are special case where no data is retained during a session and all interactions appear as events that are sent and received. Notifications may be mixed into a `StatesWorkspace` for convenience. `NotificationsManager` is the only presence object permitted in a `NotificationsWorkspace`. Use `Notifications` to add one to `NotificationsWorkspace` or `StatesWorkspace`. +Notifications are special case where no data is retained during a session and all interactions appear as events that are sent and received. Notifications may be mixed into a `StatesWorkspace` for convenience. `NotificationsManager` is the only presence object permitted in a `NotificationsWorkspace`. Use `Notifications` to add one to a `NotificationsWorkspace` or `StatesWorkspace`. ## Onboarding @@ -114,7 +114,7 @@ Notifications are fundamentally unreliable at this time as there are no built-in Presence updates are grouped together and throttled to prevent flooding the network with messages when presence values are rapidly updated. This means the presence infrastructure will not immediately broadcast updates but will broadcast them after a configurable delay. -The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling grouping with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a [States Workspace](#states-workspace) or [States](#states) and/or (2) updated later using the `controls` member of Workspace or States. [States Workspace](#states-workspace) configuration applies when States do not have their own setting. +The `allowableUpdateLatencyMs` property configures how long a local update may be delayed under normal circumstances, enabling grouping with other updates. The default `allowableUpdateLatencyMs` is **60 milliseconds** but may be (1) specified during configuration of a [States Workspace](#states-workspace) or [States](#states) and/or (2) updated later using the `controls` member of Workspace or States. The [States Workspace](#states-workspace) configuration is used when States do not have their own setting. Notifications are never queued; they effectively always have an `allowableUpdateLatencyMs` of 0. However, they may be grouped with other updates that were already queued.