Skip to content

feat(client-presence): Expose Presence at Workspaces and State objects #24396

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 13 commits into from
Apr 21, 2025
13 changes: 13 additions & 0 deletions .changeset/giant-news-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@fluidframework/presence": minor
"__section": other
---
Expose `Presence` at Workspaces and State objects

Users can now access Presence through `.presence()` all Workspaces and State objects:

`Latest.presence`
`LatestMap.presence`
`Notifications.presence`
`NotificationsWorkspace.presence`
`StatesWorkspace.presence`
7 changes: 0 additions & 7 deletions .changeset/sour-mirrors-wave.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"__section": other
---

Presence APIs have been renamed

Check failure on line 6 in .changeset/sour-mirrors-wave.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'APIs'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'APIs'?", "location": {"path": ".changeset/sour-mirrors-wave.md", "range": {"start": {"line": 6, "column": 10}}}, "severity": "ERROR"}

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

Expand Down Expand Up @@ -55,13 +55,6 @@
| `PresenceWorkspaceEntry` | `StatesWorkspaceEntry` |
| `SessionClientStatus` | `AttendeeStatus` |
| `ValueMap` | `StateMap` |
| - | `Latest.presence`|
| - | `LatestMap.presence`|
| - | `Notifications.presence`|
| - | `NotificationsWorkspace.presence`|
| - | `StatesWorkspace.presence`|



> [!NOTE]

Check failure on line 59 in .changeset/sour-mirrors-wave.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [proselint.Annotations] 'NOTE' left in text. Raw Output: {"message": "[proselint.Annotations] 'NOTE' left in text.", "location": {"path": ".changeset/sour-mirrors-wave.md", "range": {"start": {"line": 59, "column": 5}}}, "severity": "ERROR"}
> 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.

Check failure on line 60 in .changeset/sour-mirrors-wave.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'APIs'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'APIs'?", "location": {"path": ".changeset/sour-mirrors-wave.md", "range": {"start": {"line": 60, "column": 207}}}, "severity": "ERROR"}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function getPresenceViaDataObject(fluidLoadable: ExperimentalPresenceDO):
export namespace InternalTypes {
export type ManagerFactory<TKey extends string, TValue extends ValueDirectoryOrState<any>, TManager> = {
instanceBase: new (...args: any[]) => any;
} & ((key: TKey, datastoreHandle: StateDatastoreHandle<TKey, TValue>, presence: Presence) => {
} & ((key: TKey, datastoreHandle: StateDatastoreHandle<TKey, TValue>) => {
initialData?: {
value: TValue;
allowableUpdateLatencyMs: number | undefined;
Expand Down
3 changes: 0 additions & 3 deletions packages/framework/presence/src/exposedInternalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import type {
JsonSerializable,
} from "@fluidframework/core-interfaces/internal/exposedUtilityTypes";

import type { Presence } from "./presence.js";

/**
* Collection of value types that are not intended to be used/imported
* directly outside of this package.
Expand Down Expand Up @@ -113,7 +111,6 @@ export namespace InternalTypes {
> = { instanceBase: new (...args: any[]) => any } & ((
key: TKey,
datastoreHandle: StateDatastoreHandle<TKey, TValue>,
presence: Presence,
) => {
initialData?: { value: TValue; allowableUpdateLatencyMs: number | undefined };
manager: StateValue<TManager>;
Expand Down
16 changes: 8 additions & 8 deletions packages/framework/presence/src/latestMapValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,11 @@ class ValueMapImpl<T, K extends string | number> implements StateMap<K, T> {
* @alpha
*/
export interface LatestMap<T, Keys extends string | number = string | number> {
/**
* Containing {@link Presence}
*/
readonly presence: Presence;

/**
* Events for LatestMap.
*/
Expand All @@ -321,11 +326,6 @@ export interface LatestMap<T, Keys extends string | number = string | number> {
*/
readonly controls: BroadcastControls;

/**
* Root presence object
*/
readonly presence: Presence;

/**
* Current value map for this client.
*/
Expand Down Expand Up @@ -361,7 +361,6 @@ class LatestMapValueManagerImpl<
RegistrationKey,
InternalTypes.MapValueState<T, Keys>
>,
public readonly presence: Presence,
public readonly value: InternalTypes.MapValueState<T, Keys>,
controlSettings: BroadcastControlSettings | undefined,
) {
Expand All @@ -380,6 +379,9 @@ class LatestMapValueManagerImpl<

public readonly local: StateMap<Keys, T>;

public get presence(): Presence {
return this.datastore.presence;
}
public *getRemotes(): IterableIterator<LatestMapClientData<T, Keys>> {
const allKnownStates = this.datastore.knownValues(this.key);
for (const attendeeId of objectKeys(allKnownStates.states)) {
Expand Down Expand Up @@ -523,7 +525,6 @@ export function latestMap<
RegistrationKey,
InternalTypes.MapValueState<T, Keys>
>,
presence: Presence,
): {
initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined };
manager: InternalTypes.StateValue<LatestMap<T, Keys>>;
Expand All @@ -537,7 +538,6 @@ export function latestMap<
new LatestMapValueManagerImpl(
key,
datastoreFromHandle(datastoreHandle),
presence,
value,
controls,
),
Expand Down
24 changes: 10 additions & 14 deletions packages/framework/presence/src/latestValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export interface LatestEvents<T> {
* @alpha
*/
export interface Latest<T> {
/**
* Containing {@link Presence}
*/
readonly presence: Presence;

/**
* Events for Latest.
*/
Expand All @@ -64,11 +69,6 @@ export interface Latest<T> {
*/
readonly controls: BroadcastControls;

/**
* Root presence object
*/
readonly presence: Presence;

/**
* Current state for this client.
* State for this client that will be transmitted to all other connected clients.
Expand Down Expand Up @@ -102,12 +102,15 @@ class LatestValueManagerImpl<T, Key extends string>
private readonly key: Key,
private readonly datastore: StateDatastore<Key, InternalTypes.ValueRequiredState<T>>,
public readonly value: InternalTypes.ValueRequiredState<T>,
public readonly presence: Presence,
controlSettings: BroadcastControlSettings | undefined,
) {
this.controls = new OptionalBroadcastControl(controlSettings);
}

public get presence(): Presence {
return this.datastore.presence;
}

public get local(): InternalUtilityTypes.FullyReadonly<JsonDeserialized<T>> {
return this.value.value;
}
Expand Down Expand Up @@ -200,20 +203,13 @@ export function latest<T extends object, Key extends string = string>(
Key,
InternalTypes.ValueRequiredState<T>
>,
presence: Presence,
): {
initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined };
manager: InternalTypes.StateValue<Latest<T>>;
} => ({
initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs },
manager: brandIVM<LatestValueManagerImpl<T, Key>, T, InternalTypes.ValueRequiredState<T>>(
new LatestValueManagerImpl(
key,
datastoreFromHandle(datastoreHandle),
value,
presence,
controls,
),
new LatestValueManagerImpl(key, datastoreFromHandle(datastoreHandle), value, controls),
),
});
return Object.assign(factory, { instanceBase: LatestValueManagerImpl });
Expand Down
7 changes: 4 additions & 3 deletions packages/framework/presence/src/notificationsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ class NotificationsManagerImpl<
Key,
InternalTypes.ValueRequiredState<InternalTypes.NotificationType>
>,
public readonly presence: Presence,
initialSubscriptions: Partial<NotificationSubscriptions<T>>,
) {
// Add event listeners provided at instantiation
Expand All @@ -228,6 +227,10 @@ class NotificationsManagerImpl<
}
}

public get presence(): Presence {
return this.datastore.presence;
}

public update(
attendee: Attendee,
_received: number,
Expand Down Expand Up @@ -281,7 +284,6 @@ export function Notifications<
Key,
InternalTypes.ValueRequiredState<InternalTypes.NotificationType>
>,
presence: Presence,
): {
manager: InternalTypes.StateValue<NotificationsManager<T>>;
} => ({
Expand All @@ -293,7 +295,6 @@ export function Notifications<
new NotificationsManagerImpl(
key,
datastoreFromHandle(datastoreHandle),
presence,
initialSubscriptions,
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,14 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager {

const entry = createPresenceStates(
{
presence: this.presence,
attendeeId: this.attendeeId,
lookupClient: this.lookupClient,
localUpdate,
},
workspaceDatastore,
requestedContent,
controls,
this.presence,
);

this.workspaces.set(internalWorkspaceAddress, entry);
Expand Down
19 changes: 8 additions & 11 deletions packages/framework/presence/src/presenceStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface RuntimeLocalUpdateOptions {
* @internal
*/
export interface PresenceRuntime {
readonly presence: Presence;
readonly attendeeId: AttendeeId;
lookupClient(clientId: ClientConnectionId): Attendee;
localUpdate(
Expand Down Expand Up @@ -258,7 +259,6 @@ class PresenceStatesImpl<TSchema extends StatesWorkspaceSchema>
public constructor(
private readonly runtime: PresenceRuntime,
private readonly datastore: ValueElementMap<TSchema>,
public readonly presence: Presence,
initialContent: TSchema,
controlsSettings: BroadcastControlSettings | undefined,
) {
Expand All @@ -277,7 +277,7 @@ class PresenceStatesImpl<TSchema extends StatesWorkspaceSchema>
const newValues: { [key: string]: InternalTypes.ValueDirectoryOrState<unknown> } = {};
let cumulativeAllowableUpdateLatencyMs: number | undefined;
for (const [key, nodeFactory] of Object.entries(initialContent)) {
const newNodeData = nodeFactory(key, handleFromDatastore(this), this.presence);
const newNodeData = nodeFactory(key, handleFromDatastore(this));
nodes[key as keyof TSchema] = newNodeData.manager;
if ("initialData" in newNodeData) {
const { value, allowableUpdateLatencyMs } = newNodeData.initialData;
Expand Down Expand Up @@ -307,6 +307,10 @@ class PresenceStatesImpl<TSchema extends StatesWorkspaceSchema>
}
}

public get presence(): Presence {
return this.runtime.presence;
}

public knownValues<Key extends keyof TSchema & string>(
key: Key,
): {
Expand Down Expand Up @@ -362,7 +366,7 @@ class PresenceStatesImpl<TSchema extends StatesWorkspaceSchema>
TSchema & Record<TKey, InternalTypes.ManagerFactory<TKey, TValue, TValueManager>>
> {
assert(!(key in this.nodes), 0xa3c /* Already have entry for key in map */);
const nodeData = nodeFactory(key, handleFromDatastore(this), this.presence);
const nodeData = nodeFactory(key, handleFromDatastore(this));
this.nodes[key] = nodeData.manager;
if ("initialData" in nodeData) {
const { value, allowableUpdateLatencyMs } = nodeData.initialData;
Expand Down Expand Up @@ -437,15 +441,8 @@ export function createPresenceStates<TSchema extends StatesWorkspaceSchema>(
datastore: ValueElementMap<StatesWorkspaceSchema>,
initialContent: TSchema,
controls: BroadcastControlSettings | undefined,
presence: Presence,
): { public: StatesWorkspace<TSchema>; internal: PresenceStatesInternal } {
const impl = new PresenceStatesImpl<TSchema>(
runtime,
datastore,
presence,
initialContent,
controls,
);
const impl = new PresenceStatesImpl<TSchema>(runtime, datastore, initialContent, controls);

return {
public: impl,
Expand Down
3 changes: 2 additions & 1 deletion packages/framework/presence/src/stateDatastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import type { ClientConnectionId } from "./baseTypes.js";
import type { InternalTypes } from "./exposedInternalTypes.js";
import type { ClientRecord } from "./internalTypes.js";
import type { Attendee, AttendeeId } from "./presence.js";
import type { Attendee, AttendeeId, Presence } from "./presence.js";

// type StateDatastoreSchemaNode<
// TValue extends InternalTypes.ValueDirectoryOrState<any> = InternalTypes.ValueDirectoryOrState<unknown>,
Expand Down Expand Up @@ -42,6 +42,7 @@ export interface StateDatastore<
TKey extends string,
TValue extends InternalTypes.ValueDirectoryOrState<any>,
> {
presence: Presence;
localUpdate(
key: TKey,
value: TValue & {
Expand Down
Loading