Skip to content

refactor(client-presence): Refactor/Rename APIs #24384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .changeset/sour-mirrors-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
"@fluidframework/presence": minor
---
---
"section": other
---

Presence API renames

The following API changes have been made to improve clarity and consistency:

| Original | New |
|----------|-----|
| `acquirePresence` | `getPresence` |
| `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` |
| `LatestMap` (import) | `StateFactory` |
| `LatestMap` (call) | `StateFactory.latestMap` |
| `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` |
| `PresenceStatesEntries` | `StatesWorkspaceEntries` |
| `PresenceStatesSchema` | `StatesWorkspaceSchema` |
| `PresenceWorkspaceAddress` | `WorkspaceAddress` |
| `PresenceWorkspaceEntry` | `StatesWorkspaceEntry` |
| `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`.
48 changes: 24 additions & 24 deletions docs/docs/build/presence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -35,33 +35,33 @@ 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 State objects 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
### States

#### LatestValueManager
#### Latest

Latest value manager retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `PresenceStates` workspace.
`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 `PresenceStates` workspace.
`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 `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 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

While this package is developing as experimental and other Fluid Framework internals are being updated to accommodate it, a temporary Shared Object must be added within container to gain access.

```typescript
import {
acquirePresenceViaDataObject,
getPresenceViaDataObject,
ExperimentalPresenceManager,
} from "@fluidframework/presence/alpha";

Expand All @@ -71,7 +71,7 @@ const containerSchema = {
},
} satisfies ContainerSchema;

const presence = await acquirePresenceViaDataObject(container.initialObjects.presence);
const presence = await getPresenceViaDataObject(container.initialObjects.presence);
```

## Limitations
Expand All @@ -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.states.getWorkspace("app:v1states", { myState: StateFactory.latest({ x: 0 }) });
```

is incompatible with

```typescript
presence.getStates("app:v1states", { myState: 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: 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.
Expand All @@ -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
Expand All @@ -128,15 +128,15 @@ 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 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 },
);
Expand Down
4 changes: 2 additions & 2 deletions examples/apps/ai-collab/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

"use client";

import { acquirePresenceViaDataObject } from "@fluidframework/presence/alpha";
import { getPresenceViaDataObject } from "@fluidframework/presence/alpha";
import {
Box,
Button,
Expand Down Expand Up @@ -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 };
},
Expand Down
47 changes: 25 additions & 22 deletions examples/apps/ai-collab/src/app/presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
*/

import {
IPresence,
Latest,
type ISessionClient,
type PresenceStates,
type PresenceStatesSchema,
Presence,
StateFactory,
type Attendee,
type StatesWorkspace,
type StatesWorkspaceSchema,
} from "@fluidframework/presence/alpha";

import { getProfilePhoto } from "@/infra/authHelper";
Expand All @@ -18,39 +18,39 @@ export interface User {
}

const statesSchema = {
onlineUsers: Latest({ photo: "" } satisfies User),
} satisfies PresenceStatesSchema;
onlineUsers: StateFactory.latest({ photo: "" } satisfies User),
} satisfies StatesWorkspaceSchema;

export type UserPresence = PresenceStates<typeof statesSchema>;
export type UserPresence = StatesWorkspace<typeof statesSchema>;

// Takes a presence object and returns the user presence object that contains the shared object states
export function buildUserPresence(presence: IPresence): UserPresence {
const states = presence.getStates(`name:user-avatar-states`, statesSchema);
export function buildUserPresence(presence: Presence): UserPresence {
const states = presence.states.getWorkspace(`name:user-avatar-states`, statesSchema);
return states;
}

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<ISessionClient, User> = new Map();
private readonly userInfoMap: Map<Attendee, User> = new Map();
// A callback method to get updates when remote UserInfo changes
private userInfoCallback: (userInfoMap: Map<ISessionClient, User>) => void = () => {};
private userInfoCallback: (userInfoMap: Map<Attendee, User>) => 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";

// Initialize presence state for the app selection workspace
this.usersState = presence.getStates(
this.usersState = presence.states.getWorkspace(
appSelectionWorkspaceAddress, // Workspace address
statesSchema, // Workspace schema
);

// 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;

Expand All @@ -77,38 +77,41 @@ 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);
}

// 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<ISessionClient, User>) => void): void {
setUserInfoUpdateListener(callback: (userInfoMap: Map<Attendee, User>) => 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) {
// 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.sessionId === this.presence.getMyself().sessionId) {
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
userInfoList.unshift(userInfo);
}
} 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}`,
);
}
}
Expand Down
18 changes: 10 additions & 8 deletions examples/apps/ai-collab/src/components/UserPresenceGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ const UserPresenceGroup: React.FC<UserPresenceProps> = ({ 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
Expand All @@ -41,9 +43,9 @@ const UserPresenceGroup: React.FC<UserPresenceProps> = ({ 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);
Expand Down
Loading
Loading