Skip to content

Commit ea95ef0

Browse files
WillieHabiCopilotjason-ha
authored
refactor(client-presence): Refactor/Rename APIs (#24384)
## Description This change implements all the renaming/reshaping of Presence API presented in this table (All changes made to API surface can be found in .changest/sour-mirrors-wave): | Before 2.33.0 | 2.33.0 | |----------|-----| | `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` | | `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` | | `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` | --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Jason Hartman <[email protected]>
1 parent 689bbcd commit ea95ef0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1249
-1159
lines changed

.changeset/sour-mirrors-wave.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@fluidframework/presence": minor
3+
"__section": other
4+
---
5+
6+
Presence APIs have been renamed
7+
8+
The following API changes have been made to improve clarity and consistency:
9+
10+
| Before 2.33.0 | 2.33.0 |
11+
|----------|-----|
12+
| `acquirePresence` | `getPresence` |
13+
| `acquirePresenceViaDataObject` | `getPresenceViaDataObject` |
14+
| `ClientSessionId` | `AttendeeId` |
15+
| `IPresence` | `Presence` |
16+
| `IPresence.events["attendeeJoined"]` | `Presence.attendees.events["attendeeConnected"]` |
17+
| `IPresence.events["attendeeDisconnected"]` | `Presence.attendees.events["attendeeDisconnected"]` |
18+
| `IPresence.getAttendee` | `Presence.attendees.getAttendee` |
19+
| `IPresence.getAttendees` | `Presence.attendees.getAttendees` |
20+
| `IPresence.getMyself` | `Presence.attendees.getMyself` |
21+
| `IPresence.getNotifications` | `Presence.notifications.getWorkspace` |
22+
| `IPresence.getStates` | `Presence.states.getWorkspace` |
23+
| `ISessionClient` | `Attendee` |
24+
| `Latest` (import) | `StateFactory` |
25+
| `Latest` (call) | `StateFactory.latest` |
26+
| `LatestEvents.updated` | `LatestEvents.remoteUpdated` |
27+
| `LatestMap` (import) | `StateFactory` |
28+
| `LatestMap` (call) | `StateFactory.latestMap` |
29+
| `LatestMapEvents.itemRemoved` | `LatestMapEvents.remoteItemRemoved` |
30+
| `LatestMapEvents.itemUpdated` | `LatestMapEvents.remoteItemUpdated` |
31+
| `LatestMapEvents.updated` | `LatestMapEvents.remoteUpdated` |
32+
| `LatestMapItemValueClientData` | `LatestMapItemUpdatedClientData` |
33+
| `LatestMapValueClientData` | `LatestMapClientData` |
34+
| `LatestMapValueManager` | `LatestMap` |
35+
| `LatestMapValueManager.clients` | `LatestMap.getStateAttendees` |
36+
| `LatestMapValueManager.clientValue` | `LatestMap.getRemote` |
37+
| `LatestMapValueManager.clientValues` | `LatestMap.getRemotes` |
38+
| `LatestMapValueManagerEvents` | `LatestMapEvents` |
39+
| `LatestValueClientData` | `LatestClientData` |
40+
| `LatestValueData` | `LatestData` |
41+
| `LatestValueManager` | `Latest` |
42+
| `LatestValueManager.clients` | `Latest.getStateAttendees` |
43+
| `LatestValueManager.clientValue` | `Latest.getRemote` |
44+
| `LatestValueManager.clientValues` | `Latest.getRemotes` |
45+
| `LatestValueManagerEvents` | `LatestEvents` |
46+
| `LatestValueMetadata` | `LatestMetadata` |
47+
| `PresenceEvents.attendeeDisconnected` | `AttendeesEvents.attendeeDisconnected`|
48+
| `PresenceEvents.attendeeJoined` | `AttendeesEvents.attendeeConnected`|
49+
| `PresenceNotifications` | `NotificationsWorkspace` |
50+
| `PresenceNotificationsSchema` | `NotificationsWorkspaceSchema` |
51+
| `PresenceStates` | `StatesWorkspace` |
52+
| `PresenceStatesEntries` | `StatesWorkspaceEntries` |
53+
| `PresenceStatesSchema` | `StatesWorkspaceSchema` |
54+
| `PresenceWorkspaceAddress` | `WorkspaceAddress` |
55+
| `PresenceWorkspaceEntry` | `StatesWorkspaceEntry` |
56+
| `SessionClientStatus` | `AttendeeStatus` |
57+
| `ValueMap` | `StateMap` |
58+
59+
> [!NOTE]
60+
> 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.

docs/docs/build/presence.md

+24-24
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ The key scenarios that the new Presence APIs are suitable for includes:
1919

2020
## Concepts
2121

22-
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.
22+
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.
2323

2424
### Attendees
2525

26-
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.
26+
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.
2727

28-
Client Ids maintained by `ISessionClient` may be used to associate `ISessionClient` with quorum, audience, and service audience members.
28+
Client IDs maintained by `Attendee` may be used to associate `Attendee` with quorum, audience, and service audience members.
2929

3030
### Workspaces
3131

@@ -35,33 +35,33 @@ There are two types of workspaces: States and Notifications.
3535

3636
#### States Workspace
3737

38-
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.
38+
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.
3939

4040
#### Notifications Workspace
4141

42-
A notifications workspace, `PresenceNotifications`, is similar to states workspace, but is dedicated to notification use-cases via `NotificationsManager`.
42+
A `NotificationsWorkspace` is similar to states workspace, but is dedicated to notification use-cases via `NotificationsManager`.
4343

44-
### Value Managers
44+
### States
4545

46-
#### LatestValueManager
46+
#### Latest
4747

48-
Latest value manager retains the most recent atomic value each attendee has shared. Use `Latest` to add one to `PresenceStates` workspace.
48+
`Latest` retains the most recent atomic value each attendee has shared. Use `StateFactory.latest` to add one to `StatesWorkspace`.
4949

50-
#### LatestMapValueManager
50+
#### LatestMap
5151

52-
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.
52+
`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`.
5353

5454
#### NotificationsManager
5555

56-
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.
56+
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`.
5757

5858
## Onboarding
5959

6060
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.
6161

6262
```typescript
6363
import {
64-
acquirePresenceViaDataObject,
64+
getPresenceViaDataObject,
6565
ExperimentalPresenceManager,
6666
} from "@fluidframework/presence/alpha";
6767

@@ -71,7 +71,7 @@ const containerSchema = {
7171
},
7272
} satisfies ContainerSchema;
7373

74-
const presence = await acquirePresenceViaDataObject(container.initialObjects.presence);
74+
const presence = await getPresenceViaDataObject(container.initialObjects.presence);
7575
```
7676

7777
## Limitations
@@ -87,19 +87,19 @@ Current API does not provide a mechanism to validate that state and notification
8787
Example:
8888

8989
```typescript
90-
presence.getStates("app:v1states", { myState: Latest({ x: 0 }) });
90+
presence.states.getWorkspace("app:v1states", { myState: StateFactory.latest({ x: 0 }) });
9191
```
9292

9393
is incompatible with
9494

9595
```typescript
96-
presence.getStates("app:v1states", { myState: Latest({ x: "text" }) });
96+
presence.states.getWorkspace("app:v1states", { myState: StateFactory.latest({ x: "text" }) });
9797
```
9898

9999
as "app:v1states"+"myState" have different value type expectations: `{x: number}` versus `{x: string}`.
100100

101101
```typescript
102-
presence.getStates("app:v1states", { myState2: Latest({ x: true }) });
102+
presence.states.getWorkspace("app:v1states", { myState2: StateFactory.latest({ x: true }) });
103103
```
104104

105105
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
114114

115115
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.
116116

117-
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.
117+
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.
118118

119119
Notifications are never queued; they effectively always have an `allowableUpdateLatencyMs` of 0. However, they may be grouped with other updates that were already queued.
120120

121121
Note that due to throttling, clients will not receive updates for every intermediate value set by another client. For example,
122-
with `Latest*ValueManagers`, the only value sent is the value at the time the outgoing grouped message is sent. Previous
122+
with `Latest` and `LatestMap`, the only value sent is the value at the time the outgoing grouped message is sent. Previous
123123
values set by the client will not be broadcast or seen by other clients.
124124

125125
#### Example
@@ -128,15 +128,15 @@ You can configure the grouping and throttling behavior using the `allowableUpdat
128128

129129
```ts
130130
// Configure a states workspace
131-
const stateWorkspace = presence.getStates(
131+
const stateWorkspace = presence.states.getWorkspace(
132132
"app:v1states",
133133
{
134-
// This value manager has an allowable latency of 100ms.
135-
position: Latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }),
136-
// This value manager uses the workspace default.
137-
count: Latest({ num: 0 }),
134+
// This Latest state has an allowable latency of 100ms.
135+
position: StateFactory.latest({ x: 0, y: 0 }, { allowableUpdateLatencyMs: 100 }),
136+
// This Latest state uses the workspace default.
137+
count: StateFactory.latest({ num: 0 }),
138138
},
139-
// Specify the default for all value managers in this workspace to 200ms,
139+
// Specify the default for all state in this workspace to 200ms,
140140
// overriding the default value of 60ms.
141141
{ allowableUpdateLatencyMs: 200 },
142142
);

examples/apps/ai-collab/src/app/page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
"use client";
77

8-
import { acquirePresenceViaDataObject } from "@fluidframework/presence/alpha";
8+
import { getPresenceViaDataObject } from "@fluidframework/presence/alpha";
99
import {
1010
Box,
1111
Button,
@@ -63,7 +63,7 @@ export default function TasksListPage(): JSX.Element {
6363
const _treeView = fluidContainer.initialObjects.appState.viewWith(TREE_CONFIGURATION);
6464
setTreeView(_treeView);
6565

66-
const presence = acquirePresenceViaDataObject(fluidContainer.initialObjects.presence);
66+
const presence = getPresenceViaDataObject(fluidContainer.initialObjects.presence);
6767
setPresenceManagerContext(new PresenceManager(presence));
6868
return { sharedTree: _treeView };
6969
},

examples/apps/ai-collab/src/app/presence.ts

+26-23
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
*/
55

66
import {
7-
IPresence,
8-
Latest,
9-
type ISessionClient,
10-
type PresenceStates,
11-
type PresenceStatesSchema,
7+
Presence,
8+
StateFactory,
9+
type Attendee,
10+
type StatesWorkspace,
11+
type StatesWorkspaceSchema,
1212
} from "@fluidframework/presence/alpha";
1313

1414
import { getProfilePhoto } from "@/infra/authHelper";
@@ -18,39 +18,39 @@ export interface User {
1818
}
1919

2020
const statesSchema = {
21-
onlineUsers: Latest({ photo: "" } satisfies User),
22-
} satisfies PresenceStatesSchema;
21+
onlineUsers: StateFactory.latest({ photo: "" } satisfies User),
22+
} satisfies StatesWorkspaceSchema;
2323

24-
export type UserPresence = PresenceStates<typeof statesSchema>;
24+
export type UserPresence = StatesWorkspace<typeof statesSchema>;
2525

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

3232
export class PresenceManager {
3333
// A PresenceState object to manage the presence of users within the app
3434
private readonly usersState: UserPresence;
3535
// A map of SessionClient to UserInfo, where users can share their info with other users
36-
private readonly userInfoMap: Map<ISessionClient, User> = new Map();
36+
private readonly userInfoMap: Map<Attendee, User> = new Map();
3737
// A callback method to get updates when remote UserInfo changes
38-
private userInfoCallback: (userInfoMap: Map<ISessionClient, User>) => void = () => {};
38+
private userInfoCallback: (userInfoMap: Map<Attendee, User>) => void = () => {};
3939

40-
constructor(private readonly presence: IPresence) {
40+
constructor(private readonly presence: Presence) {
4141
// Address for the presence state, this is used to organize the presence states and avoid conflicts
4242
const appSelectionWorkspaceAddress = "aiCollab:workspace";
4343

4444
// Initialize presence state for the app selection workspace
45-
this.usersState = presence.getStates(
45+
this.usersState = presence.states.getWorkspace(
4646
appSelectionWorkspaceAddress, // Workspace address
4747
statesSchema, // Workspace schema
4848
);
4949

5050
// Listen for updates to the userInfo property in the presence state
51-
this.usersState.props.onlineUsers.events.on("updated", (update) => {
51+
this.usersState.props.onlineUsers.events.on("remoteUpdated", (update) => {
5252
// The remote client that updated the userInfo property
53-
const remoteSessionClient = update.client;
53+
const remoteSessionClient = update.attendee;
5454
// The new value of the userInfo property
5555
const remoteUserInfo = update.value;
5656

@@ -77,38 +77,41 @@ export class PresenceManager {
7777
this.usersState.props.onlineUsers.local = { photo: photoUrl };
7878
}
7979

80-
this.userInfoMap.set(this.presence.getMyself(), this.usersState.props.onlineUsers.local);
80+
this.userInfoMap.set(
81+
this.presence.attendees.getMyself(),
82+
this.usersState.props.onlineUsers.local,
83+
);
8184
this.userInfoCallback(this.userInfoMap);
8285
}
8386

8487
// Returns the presence object
85-
getPresence(): IPresence {
88+
getPresence(): Presence {
8689
return this.presence;
8790
}
8891

8992
// Allows the app to listen for updates to the userInfoMap
90-
setUserInfoUpdateListener(callback: (userInfoMap: Map<ISessionClient, User>) => void): void {
93+
setUserInfoUpdateListener(callback: (userInfoMap: Map<Attendee, User>) => void): void {
9194
this.userInfoCallback = callback;
9295
}
9396

9497
// Returns the UserInfo of given session clients
95-
getUserInfo(sessionList: ISessionClient[]): User[] {
98+
getUserInfo(sessionList: Attendee[]): User[] {
9699
const userInfoList: User[] = [];
97100

98101
for (const sessionClient of sessionList) {
99102
// If local user or remote user is connected, then only add it to the list
100103
try {
101-
const userInfo = this.usersState.props.onlineUsers.clientValue(sessionClient).value;
104+
const userInfo = this.usersState.props.onlineUsers.getRemote(sessionClient).value;
102105
// If the user is local user, then add it to the beginning of the list
103-
if (sessionClient.sessionId === this.presence.getMyself().sessionId) {
106+
if (sessionClient.attendeeId === this.presence.attendees.getMyself().attendeeId) {
104107
userInfoList.push(userInfo);
105108
} else {
106109
// If the user is remote user, then add it to the end of the list
107110
userInfoList.unshift(userInfo);
108111
}
109112
} catch (error) {
110113
console.error(
111-
`Error: ${error} when getting user info for session client: ${sessionClient.sessionId}`,
114+
`Error: ${error} when getting user info for session client: ${sessionClient.attendeeId}`,
112115
);
113116
}
114117
}

examples/apps/ai-collab/src/components/UserPresenceGroup.tsx

+10-8
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ const UserPresenceGroup: React.FC<UserPresenceProps> = ({ presenceManager }): JS
1818
const [invalidations, setInvalidations] = useState(0);
1919

2020
useEffect(() => {
21-
// Listen to the attendeeJoined event and update the presence group when a new attendee joins
22-
const unsubJoin = presenceManager.getPresence().events.on("attendeeJoined", () => {
23-
setInvalidations(invalidations + Math.random());
24-
});
21+
// Listen to the attendeeConnected event and update the presence group when a new attendee joins
22+
const unsubJoin = presenceManager
23+
.getPresence()
24+
.attendees.events.on("attendeeConnected", () => {
25+
setInvalidations(invalidations + Math.random());
26+
});
2527
// Listen to the attendeeDisconnected event and update the presence group when an attendee leaves
2628
const unsubDisconnect = presenceManager
2729
.getPresence()
28-
.events.on("attendeeDisconnected", () => {
30+
.attendees.events.on("attendeeDisconnected", () => {
2931
setInvalidations(invalidations + Math.random());
3032
});
3133
// Listen to the userInfoUpdate event and update the presence group when the user info is updated
@@ -41,9 +43,9 @@ const UserPresenceGroup: React.FC<UserPresenceProps> = ({ presenceManager }): JS
4143
});
4244

4345
// Get the list of connected attendees
44-
const connectedAttendees = [...presenceManager.getPresence().getAttendees()].filter(
45-
(attendee) => attendee.getConnectionStatus() === "Connected",
46-
);
46+
const connectedAttendees = [
47+
...presenceManager.getPresence().attendees.getAttendees(),
48+
].filter((attendee) => attendee.getConnectionStatus() === "Connected");
4749

4850
// Get the user info for the connected attendees
4951
const userInfoList = presenceManager.getUserInfo(connectedAttendees);

0 commit comments

Comments
 (0)