From 4c1709795aa8cab4b95bc55871658b5aecb2cb9c Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Thu, 17 Apr 2025 19:12:24 +0200 Subject: [PATCH 01/12] add presence to datastoremanager --- packages/framework/presence/src/presenceDatastoreManager.ts | 3 ++- packages/framework/presence/src/presenceManager.ts | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index c875ecf59c67..1db18b479c9d 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 { AttendeeId, Attendee, PresenceEvents } from "./presence.js"; +import type { AttendeeId, Attendee, Presence, PresenceEvents } from "./presence.js"; import type { ClientUpdateEntry, RuntimeLocalUpdateOptions, @@ -153,6 +153,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { private readonly lookupClient: (clientId: AttendeeId) => Attendee, private readonly logger: ITelemetryLoggerExt | undefined, private readonly events: IEmitter, + private readonly presence: Presence, systemWorkspaceDatastore: SystemWorkspaceDatastore, systemWorkspace: StatesWorkspaceEntry, ) { diff --git a/packages/framework/presence/src/presenceManager.ts b/packages/framework/presence/src/presenceManager.ts index e5a233009802..cb8ab184ff71 100644 --- a/packages/framework/presence/src/presenceManager.ts +++ b/packages/framework/presence/src/presenceManager.ts @@ -77,6 +77,7 @@ class PresenceManager implements Presence, PresenceExtensionInterface { runtime, this.events, this.mc?.logger, + this, ); this.attendees = this.systemWorkspace; @@ -138,6 +139,7 @@ function setupSubComponents( events: Listenable & IEmitter, logger: ITelemetryLoggerExt | undefined, + presence: Presence, ): [PresenceDatastoreManager, SystemWorkspace] { const systemWorkspaceDatastore: SystemWorkspaceDatastore = { clientToSessionId: {}, @@ -154,6 +156,7 @@ function setupSubComponents( systemWorkspaceConfig.workspace.getAttendee.bind(systemWorkspaceConfig.workspace), logger, events, + presence, systemWorkspaceDatastore, systemWorkspaceConfig.statesEntry, ); From ee52a67e755468769930eab7887889a79d19304e Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Thu, 17 Apr 2025 19:20:35 +0200 Subject: [PATCH 02/12] add presence to workspace level --- .../presence/api-report/presence.alpha.api.md | 2 ++ .../presence/src/presenceDatastoreManager.ts | 1 + packages/framework/presence/src/presenceStates.ts | 12 ++++++++++-- packages/framework/presence/src/types.ts | 11 +++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 9b108fe2f340..331bacacce2c 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -272,6 +272,7 @@ export type NotificationSubscriptions { add, TManager extends NotificationsManager>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is NotificationsWorkspace>>; + readonly presence: Presence; readonly props: StatesWorkspaceEntries; } @@ -331,6 +332,7 @@ export interface StateMap { export interface StatesWorkspace { add, TManager extends TManagerConstraints>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is StatesWorkspace>, TManagerConstraints>; readonly controls: BroadcastControls; + readonly presence: Presence; readonly props: StatesWorkspaceEntries; } diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index 1db18b479c9d..cb8e8d261553 100644 --- a/packages/framework/presence/src/presenceDatastoreManager.ts +++ b/packages/framework/presence/src/presenceDatastoreManager.ts @@ -227,6 +227,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { workspaceDatastore, requestedContent, controls, + this.presence, ); this.workspaces.set(internalWorkspaceAddress, entry); diff --git a/packages/framework/presence/src/presenceStates.ts b/packages/framework/presence/src/presenceStates.ts index 55d19d6bd259..15fd0e88dfee 100644 --- a/packages/framework/presence/src/presenceStates.ts +++ b/packages/framework/presence/src/presenceStates.ts @@ -12,7 +12,7 @@ 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 { AttendeeId, Attendee } from "./presence.js"; +import type { AttendeeId, Attendee, Presence } from "./presence.js"; import type { LocalStateUpdateOptions, StateDatastore } from "./stateDatastore.js"; import { handleFromDatastore } from "./stateDatastore.js"; import type { StatesWorkspace, StatesWorkspaceSchema } from "./types.js"; @@ -258,6 +258,7 @@ class PresenceStatesImpl public constructor( private readonly runtime: PresenceRuntime, private readonly datastore: ValueElementMap, + public readonly presence: Presence, initialContent: TSchema, controlsSettings: BroadcastControlSettings | undefined, ) { @@ -436,8 +437,15 @@ export function createPresenceStates( datastore: ValueElementMap, initialContent: TSchema, controls: BroadcastControlSettings | undefined, + presence: Presence, ): { public: StatesWorkspace; internal: PresenceStatesInternal } { - const impl = new PresenceStatesImpl(runtime, datastore, initialContent, controls); + const impl = new PresenceStatesImpl( + runtime, + datastore, + presence, + initialContent, + controls, + ); return { public: impl, diff --git a/packages/framework/presence/src/types.ts b/packages/framework/presence/src/types.ts index b3d7da22f57c..0ce5c6b60ee7 100644 --- a/packages/framework/presence/src/types.ts +++ b/packages/framework/presence/src/types.ts @@ -6,6 +6,7 @@ import type { BroadcastControls } from "./broadcastControls.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; import type { NotificationsManager } from "./notificationsManager.js"; +import type { Presence } from "./presence.js"; /** * Unique address within a session. @@ -106,6 +107,11 @@ export interface StatesWorkspace< * Default controls for management of broadcast updates. */ readonly controls: BroadcastControls; + + /** + * Root Presence object. + */ + readonly presence: Presence; } // #endregion StatesWorkspace @@ -161,6 +167,11 @@ export interface NotificationsWorkspace; + + /** + * Root Presence object. + */ + readonly presence: Presence; } // #endregion NotificationsWorkspace From 5d3134de1fb0317801e2eb1b7b4c47d1249bda5a Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Thu, 17 Apr 2025 19:54:03 +0200 Subject: [PATCH 03/12] add presence to value manager level --- .../presence/api-report/presence.alpha.api.md | 5 ++++- .../presence/src/exposedInternalTypes.ts | 3 +++ .../presence/src/latestMapValueManager.ts | 10 +++++++++- .../presence/src/latestValueManager.ts | 17 +++++++++++++++-- .../presence/src/notificationsManager.ts | 10 +++++++++- .../framework/presence/src/presenceStates.ts | 4 ++-- 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 331bacacce2c..e40f10c9e89b 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -63,7 +63,7 @@ export function getPresenceViaDataObject(fluidLoadable: ExperimentalPresenceDO): export namespace InternalTypes { export type ManagerFactory, TManager> = { instanceBase: new (...args: any[]) => any; - } & ((key: TKey, datastoreHandle: StateDatastoreHandle) => { + } & ((key: TKey, datastoreHandle: StateDatastoreHandle, presence: Presence) => { initialData?: { value: TValue; allowableUpdateLatencyMs: number | undefined; @@ -144,6 +144,7 @@ export interface Latest { getStateAttendees(): Attendee[]; get local(): InternalUtilityTypes.FullyReadonly>; set local(value: JsonSerializable & JsonDeserialized); + readonly presence: Presence; } // @alpha @@ -181,6 +182,7 @@ export interface LatestMap { getRemotes(): IterableIterator>; getStateAttendees(): Attendee[]; readonly local: StateMap; + readonly presence: Presence; } // @alpha @@ -256,6 +258,7 @@ export interface NotificationsManager; readonly events: Listenable; readonly notifications: NotificationListenable; + readonly presence: Presence; } // @alpha @sealed (undocumented) diff --git a/packages/framework/presence/src/exposedInternalTypes.ts b/packages/framework/presence/src/exposedInternalTypes.ts index 6d86d9b2f71f..5f6161666753 100644 --- a/packages/framework/presence/src/exposedInternalTypes.ts +++ b/packages/framework/presence/src/exposedInternalTypes.ts @@ -8,6 +8,8 @@ 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. @@ -111,6 +113,7 @@ export namespace InternalTypes { > = { instanceBase: new (...args: any[]) => any } & (( key: TKey, datastoreHandle: StateDatastoreHandle, + presence: Presence, ) => { initialData?: { value: TValue; allowableUpdateLatencyMs: number | undefined }; manager: StateValue; diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 840905de1c28..9471d6eecb67 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -18,7 +18,7 @@ import type { InternalUtilityTypes } from "./exposedUtilityTypes.js"; import type { PostUpdateAction, ValueManager } from "./internalTypes.js"; import { objectEntries, objectKeys } from "./internalUtils.js"; import type { LatestClientData, LatestData, LatestMetadata } from "./latestValueTypes.js"; -import type { AttendeeId, Attendee, SpecificAttendee } from "./presence.js"; +import type { AttendeeId, Attendee, Presence, SpecificAttendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -321,6 +321,11 @@ export interface LatestMap { */ readonly controls: BroadcastControls; + /** + * Root presence object + */ + readonly presence: Presence; + /** * Current value map for this client. */ @@ -356,6 +361,7 @@ class LatestMapValueManagerImpl< RegistrationKey, InternalTypes.MapValueState >, + public readonly presence: Presence, public readonly value: InternalTypes.MapValueState, controlSettings: BroadcastControlSettings | undefined, ) { @@ -517,6 +523,7 @@ export function latestMap< RegistrationKey, InternalTypes.MapValueState >, + presence: Presence, ): { initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; manager: InternalTypes.StateValue>; @@ -530,6 +537,7 @@ export function latestMap< new LatestMapValueManagerImpl( key, datastoreFromHandle(datastoreHandle), + presence, value, controls, ), diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 7fe34e4e5578..8a16f9af1b79 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 { LatestClientData, LatestData } from "./latestValueTypes.js"; -import type { Attendee } from "./presence.js"; +import type { Attendee, Presence } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -64,6 +64,11 @@ export interface Latest { */ 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. @@ -97,6 +102,7 @@ class LatestValueManagerImpl private readonly key: Key, private readonly datastore: StateDatastore>, public readonly value: InternalTypes.ValueRequiredState, + public readonly presence: Presence, controlSettings: BroadcastControlSettings | undefined, ) { this.controls = new OptionalBroadcastControl(controlSettings); @@ -194,13 +200,20 @@ export function latest( Key, InternalTypes.ValueRequiredState >, + presence: Presence, ): { initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; manager: InternalTypes.StateValue>; } => ({ initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs }, manager: brandIVM, T, InternalTypes.ValueRequiredState>( - new LatestValueManagerImpl(key, datastoreFromHandle(datastoreHandle), value, controls), + new LatestValueManagerImpl( + key, + datastoreFromHandle(datastoreHandle), + value, + presence, + controls, + ), ), }); return Object.assign(factory, { instanceBase: LatestValueManagerImpl }); diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index 7fc38e8d0a18..de3fcd675134 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 { Attendee } from "./presence.js"; +import type { Attendee, Presence } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -144,6 +144,11 @@ export interface NotificationsManager< * Provides subscription to notifications from other clients. */ readonly notifications: NotificationListenable; + + /** + * Root presence object + */ + readonly presence: Presence; } /** @@ -205,6 +210,7 @@ class NotificationsManagerImpl< Key, InternalTypes.ValueRequiredState >, + public readonly presence: Presence, initialSubscriptions: Partial>, ) { // Add event listeners provided at instantiation @@ -275,6 +281,7 @@ export function Notifications< Key, InternalTypes.ValueRequiredState >, + presence: Presence, ): { manager: InternalTypes.StateValue>; } => ({ @@ -286,6 +293,7 @@ export function Notifications< new NotificationsManagerImpl( key, datastoreFromHandle(datastoreHandle), + presence, initialSubscriptions, ), ), diff --git a/packages/framework/presence/src/presenceStates.ts b/packages/framework/presence/src/presenceStates.ts index 15fd0e88dfee..1b3ab98425ea 100644 --- a/packages/framework/presence/src/presenceStates.ts +++ b/packages/framework/presence/src/presenceStates.ts @@ -277,7 +277,7 @@ class PresenceStatesImpl const newValues: { [key: string]: InternalTypes.ValueDirectoryOrState } = {}; let cumulativeAllowableUpdateLatencyMs: number | undefined; for (const [key, nodeFactory] of Object.entries(initialContent)) { - const newNodeData = nodeFactory(key, handleFromDatastore(this)); + const newNodeData = nodeFactory(key, handleFromDatastore(this), this.presence); nodes[key as keyof TSchema] = newNodeData.manager; if ("initialData" in newNodeData) { const { value, allowableUpdateLatencyMs } = newNodeData.initialData; @@ -362,7 +362,7 @@ class PresenceStatesImpl TSchema & Record> > { assert(!(key in this.nodes), 0xa3c /* Already have entry for key in map */); - const nodeData = nodeFactory(key, handleFromDatastore(this)); + const nodeData = nodeFactory(key, handleFromDatastore(this), this.presence); this.nodes[key] = nodeData.manager; if ("initialData" in nodeData) { const { value, allowableUpdateLatencyMs } = nodeData.initialData; From 65a31bf55f0acf57aedad7ba3092df7b08d90a64 Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Thu, 17 Apr 2025 20:01:25 +0200 Subject: [PATCH 04/12] changeset --- .changeset/sour-mirrors-wave.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.changeset/sour-mirrors-wave.md b/.changeset/sour-mirrors-wave.md index 89784ab23d5d..0894b11b47dc 100644 --- a/.changeset/sour-mirrors-wave.md +++ b/.changeset/sour-mirrors-wave.md @@ -55,6 +55,13 @@ The following API changes have been made to improve clarity and consistency: | `PresenceWorkspaceEntry` | `StatesWorkspaceEntry` | | `SessionClientStatus` | `AttendeeStatus` | | `ValueMap` | `StateMap` | +| - | `Latest.presence`| +| - | `LatestMap.presence`| +| - | `Notifications.presence`| +| - | `NotificationsWorkspace.presence`| +| - | `StatesWorkspace.presence`| + + > [!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. From a2b41a82d30ef098bb2821fbf13923451c374425 Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Fri, 18 Apr 2025 17:19:04 +0200 Subject: [PATCH 05/12] tests --- .../src/test/latestMapValueManager.spec.ts | 9 +++++++++ .../src/test/latestValueManager.spec.ts | 8 ++++++++ .../src/test/notificationsManager.spec.ts | 19 +++++++++++++++++++ .../presence/src/test/presenceStates.spec.ts | 16 ++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index b8a024bdd378..9ffe1958f008 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -87,6 +87,15 @@ describe("Presence", () => { mapVM.local.delete("key1"); assert.strictEqual(localRemovalCount, 1); }); + + it("can acces root presence object", () => { + const presence = createPresenceManager(new MockEphemeralRuntime()); + const states = presence.states.getWorkspace(testWorkspaceName, { + fixedMap: StateFactory.latestMap({ key1: { x: 0, y: 0 } }), + }); + + assert.strictEqual(states.props.fixedMap.presence, presence); + }); }); }); diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index 39718f9e85dc..7cc2e4cfbc03 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -71,6 +71,14 @@ describe("Presence", () => { }); assert.deepStrictEqual(states.props.arr.local, [1, 2, 3]); }); + + it("can access root presence object", () => { + const states = presence.states.getWorkspace(testWorkspaceName, { + camera: StateFactory.latest({ x: 0, y: 0, z: 0 }), + }); + + assert.strictEqual(states.props.camera.presence, presence); + }); }); addControlsTests(createLatestManager); diff --git a/packages/framework/presence/src/test/notificationsManager.spec.ts b/packages/framework/presence/src/test/notificationsManager.spec.ts index b67e168d4d3a..141e001ee706 100644 --- a/packages/framework/presence/src/test/notificationsManager.spec.ts +++ b/packages/framework/presence/src/test/notificationsManager.spec.ts @@ -487,5 +487,24 @@ describe("Presence", () => { // Verify assert(originalEventHandlerCalled, "originalEventHandler not called"); }); + + it("can access root presence object", () => { + notificationsWorkspace.add( + "testEvents", + Notifications< + // Below explicit generic specification should not be required. + { + newId: (id: number) => void; + }, + "testEvents" + >( + // A default handler is not required + {}, + ), + ); + + assert.strictEqual(notificationsWorkspace.props.testEvents.presence, presence); + assert.strictEqual(notificationsWorkspace.presence, presence); + }); }); }); diff --git a/packages/framework/presence/src/test/presenceStates.spec.ts b/packages/framework/presence/src/test/presenceStates.spec.ts index 880b5713c142..60ed458817f8 100644 --- a/packages/framework/presence/src/test/presenceStates.spec.ts +++ b/packages/framework/presence/src/test/presenceStates.spec.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. */ +import { strict as assert } from "node:assert"; + import type { JsonDeserialized, JsonSerializable, @@ -10,8 +12,14 @@ import type { import type { InternalTypes } from "../exposedInternalTypes.js"; import type { Presence } from "../presence.js"; +import { createPresenceManager } from "../presenceManager.js"; import { addControlsTests } from "./broadcastControlsTests.js"; +import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; + +import { StateFactory } from "@fluidframework/presence/alpha"; + +const testWorkspaceName = "name:testWorkspaceA"; describe("Presence", () => { describe("StatesWorkspace", () => { @@ -23,6 +31,14 @@ describe("Presence", () => { addControlsTests((presence, controlSettings) => { return presence.states.getWorkspace("name:testWorkspaceA", {}, controlSettings); }); + + it("can access root presence object", () => { + const presence = createPresenceManager(new MockEphemeralRuntime()); + const states = presence.states.getWorkspace(testWorkspaceName, { + obj: StateFactory.latest({}), + }); + assert.strictEqual(states.presence, presence); + }); }); }); From a2faa87a3c8942a604a15b102bf5c2d1777d6c6f Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Fri, 18 Apr 2025 21:07:56 +0200 Subject: [PATCH 06/12] req changes --- .changeset/giant-news-appear.md | 13 ++++++++++ .changeset/sour-mirrors-wave.md | 7 ------ .../presence/api-report/presence.alpha.api.md | 2 +- .../presence/src/exposedInternalTypes.ts | 3 --- .../presence/src/latestMapValueManager.ts | 16 ++++++------- .../presence/src/latestValueManager.ts | 24 ++++++++----------- .../presence/src/notificationsManager.ts | 7 +++--- .../presence/src/presenceDatastoreManager.ts | 2 +- .../framework/presence/src/presenceStates.ts | 19 +++++++-------- .../framework/presence/src/stateDatastore.ts | 3 ++- 10 files changed, 47 insertions(+), 49 deletions(-) create mode 100644 .changeset/giant-news-appear.md diff --git a/.changeset/giant-news-appear.md b/.changeset/giant-news-appear.md new file mode 100644 index 000000000000..e336fc1edd04 --- /dev/null +++ b/.changeset/giant-news-appear.md @@ -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` diff --git a/.changeset/sour-mirrors-wave.md b/.changeset/sour-mirrors-wave.md index 0894b11b47dc..89784ab23d5d 100644 --- a/.changeset/sour-mirrors-wave.md +++ b/.changeset/sour-mirrors-wave.md @@ -55,13 +55,6 @@ The following API changes have been made to improve clarity and consistency: | `PresenceWorkspaceEntry` | `StatesWorkspaceEntry` | | `SessionClientStatus` | `AttendeeStatus` | | `ValueMap` | `StateMap` | -| - | `Latest.presence`| -| - | `LatestMap.presence`| -| - | `Notifications.presence`| -| - | `NotificationsWorkspace.presence`| -| - | `StatesWorkspace.presence`| - - > [!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/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index e40f10c9e89b..508fcdbe60a3 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -63,7 +63,7 @@ export function getPresenceViaDataObject(fluidLoadable: ExperimentalPresenceDO): export namespace InternalTypes { export type ManagerFactory, TManager> = { instanceBase: new (...args: any[]) => any; - } & ((key: TKey, datastoreHandle: StateDatastoreHandle, presence: Presence) => { + } & ((key: TKey, datastoreHandle: StateDatastoreHandle) => { initialData?: { value: TValue; allowableUpdateLatencyMs: number | undefined; diff --git a/packages/framework/presence/src/exposedInternalTypes.ts b/packages/framework/presence/src/exposedInternalTypes.ts index 5f6161666753..6d86d9b2f71f 100644 --- a/packages/framework/presence/src/exposedInternalTypes.ts +++ b/packages/framework/presence/src/exposedInternalTypes.ts @@ -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. @@ -113,7 +111,6 @@ export namespace InternalTypes { > = { instanceBase: new (...args: any[]) => any } & (( key: TKey, datastoreHandle: StateDatastoreHandle, - presence: Presence, ) => { initialData?: { value: TValue; allowableUpdateLatencyMs: number | undefined }; manager: StateValue; diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 9471d6eecb67..ef51c821173c 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -311,6 +311,11 @@ class ValueMapImpl implements StateMap { * @alpha */ export interface LatestMap { + /** + * Containing {@link Presence} + */ + readonly presence: Presence; + /** * Events for LatestMap. */ @@ -321,11 +326,6 @@ export interface LatestMap { */ readonly controls: BroadcastControls; - /** - * Root presence object - */ - readonly presence: Presence; - /** * Current value map for this client. */ @@ -361,7 +361,6 @@ class LatestMapValueManagerImpl< RegistrationKey, InternalTypes.MapValueState >, - public readonly presence: Presence, public readonly value: InternalTypes.MapValueState, controlSettings: BroadcastControlSettings | undefined, ) { @@ -380,6 +379,9 @@ class LatestMapValueManagerImpl< public readonly local: StateMap; + public get presence(): Presence { + return this.datastore.presence; + } public *getRemotes(): IterableIterator> { const allKnownStates = this.datastore.knownValues(this.key); for (const attendeeId of objectKeys(allKnownStates.states)) { @@ -523,7 +525,6 @@ export function latestMap< RegistrationKey, InternalTypes.MapValueState >, - presence: Presence, ): { initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; manager: InternalTypes.StateValue>; @@ -537,7 +538,6 @@ export function latestMap< new LatestMapValueManagerImpl( key, datastoreFromHandle(datastoreHandle), - presence, value, controls, ), diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 8a16f9af1b79..0c1c2456eb1d 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -54,6 +54,11 @@ export interface LatestEvents { * @alpha */ export interface Latest { + /** + * Containing {@link Presence} + */ + readonly presence: Presence; + /** * Events for Latest. */ @@ -64,11 +69,6 @@ export interface Latest { */ 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. @@ -102,12 +102,15 @@ class LatestValueManagerImpl private readonly key: Key, private readonly datastore: StateDatastore>, public readonly value: InternalTypes.ValueRequiredState, - 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> { return this.value.value; } @@ -200,20 +203,13 @@ export function latest( Key, InternalTypes.ValueRequiredState >, - presence: Presence, ): { initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; manager: InternalTypes.StateValue>; } => ({ initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs }, manager: brandIVM, T, InternalTypes.ValueRequiredState>( - new LatestValueManagerImpl( - key, - datastoreFromHandle(datastoreHandle), - value, - presence, - controls, - ), + new LatestValueManagerImpl(key, datastoreFromHandle(datastoreHandle), value, controls), ), }); return Object.assign(factory, { instanceBase: LatestValueManagerImpl }); diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index de3fcd675134..5269b6769d68 100644 --- a/packages/framework/presence/src/notificationsManager.ts +++ b/packages/framework/presence/src/notificationsManager.ts @@ -210,7 +210,6 @@ class NotificationsManagerImpl< Key, InternalTypes.ValueRequiredState >, - public readonly presence: Presence, initialSubscriptions: Partial>, ) { // Add event listeners provided at instantiation @@ -228,6 +227,10 @@ class NotificationsManagerImpl< } } + public get presence(): Presence { + return this.datastore.presence; + } + public update( attendee: Attendee, _received: number, @@ -281,7 +284,6 @@ export function Notifications< Key, InternalTypes.ValueRequiredState >, - presence: Presence, ): { manager: InternalTypes.StateValue>; } => ({ @@ -293,7 +295,6 @@ export function Notifications< new NotificationsManagerImpl( key, datastoreFromHandle(datastoreHandle), - presence, initialSubscriptions, ), ), diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index cb8e8d261553..9802997cbc70 100644 --- a/packages/framework/presence/src/presenceDatastoreManager.ts +++ b/packages/framework/presence/src/presenceDatastoreManager.ts @@ -220,6 +220,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { const entry = createPresenceStates( { + presence: this.presence, attendeeId: this.attendeeId, lookupClient: this.lookupClient, localUpdate, @@ -227,7 +228,6 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { workspaceDatastore, requestedContent, controls, - this.presence, ); this.workspaces.set(internalWorkspaceAddress, entry); diff --git a/packages/framework/presence/src/presenceStates.ts b/packages/framework/presence/src/presenceStates.ts index 1b3ab98425ea..a1651d057c33 100644 --- a/packages/framework/presence/src/presenceStates.ts +++ b/packages/framework/presence/src/presenceStates.ts @@ -51,6 +51,7 @@ export interface RuntimeLocalUpdateOptions { * @internal */ export interface PresenceRuntime { + readonly presence: Presence; readonly attendeeId: AttendeeId; lookupClient(clientId: ClientConnectionId): Attendee; localUpdate( @@ -258,7 +259,6 @@ class PresenceStatesImpl public constructor( private readonly runtime: PresenceRuntime, private readonly datastore: ValueElementMap, - public readonly presence: Presence, initialContent: TSchema, controlsSettings: BroadcastControlSettings | undefined, ) { @@ -277,7 +277,7 @@ class PresenceStatesImpl const newValues: { [key: string]: InternalTypes.ValueDirectoryOrState } = {}; 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; @@ -307,6 +307,10 @@ class PresenceStatesImpl } } + public get presence(): Presence { + return this.runtime.presence; + } + public knownValues( key: Key, ): { @@ -362,7 +366,7 @@ class PresenceStatesImpl TSchema & Record> > { 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; @@ -437,15 +441,8 @@ export function createPresenceStates( datastore: ValueElementMap, initialContent: TSchema, controls: BroadcastControlSettings | undefined, - presence: Presence, ): { public: StatesWorkspace; internal: PresenceStatesInternal } { - const impl = new PresenceStatesImpl( - runtime, - datastore, - presence, - initialContent, - controls, - ); + const impl = new PresenceStatesImpl(runtime, datastore, initialContent, controls); return { public: impl, diff --git a/packages/framework/presence/src/stateDatastore.ts b/packages/framework/presence/src/stateDatastore.ts index 1bec30785b81..44fddb65de9c 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 { Attendee, AttendeeId } from "./presence.js"; +import type { Attendee, AttendeeId, Presence } from "./presence.js"; // type StateDatastoreSchemaNode< // TValue extends InternalTypes.ValueDirectoryOrState = InternalTypes.ValueDirectoryOrState, @@ -42,6 +42,7 @@ export interface StateDatastore< TKey extends string, TValue extends InternalTypes.ValueDirectoryOrState, > { + presence: Presence; localUpdate( key: TKey, value: TValue & { From a14f364fdcd578739314f6c4c31004f4161ebf2a Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:36:18 +0200 Subject: [PATCH 07/12] Update .changeset/giant-news-appear.md Co-authored-by: Jason Hartman --- .changeset/giant-news-appear.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/giant-news-appear.md b/.changeset/giant-news-appear.md index e336fc1edd04..abd65ca60a7c 100644 --- a/.changeset/giant-news-appear.md +++ b/.changeset/giant-news-appear.md @@ -4,7 +4,7 @@ --- Expose `Presence` at Workspaces and State objects -Users can now access Presence through `.presence()` all Workspaces and State objects: +Users can now access `Presence` through `.presence` in all Workspaces and State objects: `Latest.presence` `LatestMap.presence` From 071c5a7af954ae5d75548fcefec1fbeb27e909eb Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Fri, 18 Apr 2025 22:46:33 +0200 Subject: [PATCH 08/12] changes --- .../framework/presence/src/notificationsManager.ts | 10 +++++----- .../presence/src/test/latestMapValueManager.spec.ts | 2 +- .../presence/src/test/latestValueManager.spec.ts | 2 +- .../presence/src/test/notificationsManager.spec.ts | 2 +- .../framework/presence/src/test/presenceStates.spec.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index 5269b6769d68..8c85d979b997 100644 --- a/packages/framework/presence/src/notificationsManager.ts +++ b/packages/framework/presence/src/notificationsManager.ts @@ -130,6 +130,11 @@ export interface NotificationEmitter, > { + /** + * Containing {@link Presence} + */ + readonly presence: Presence; + /** * Events for Notifications manager. */ @@ -144,11 +149,6 @@ export interface NotificationsManager< * Provides subscription to notifications from other clients. */ readonly notifications: NotificationListenable; - - /** - * Root presence object - */ - readonly presence: Presence; } /** diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index 9ffe1958f008..b7dc5923a359 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -88,7 +88,7 @@ describe("Presence", () => { assert.strictEqual(localRemovalCount, 1); }); - it("can acces root presence object", () => { + it(".presence provides Presence it was created under", () => { const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { fixedMap: StateFactory.latestMap({ key1: { x: 0, y: 0 } }), diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index 7cc2e4cfbc03..8ef15bbe117d 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -72,7 +72,7 @@ describe("Presence", () => { assert.deepStrictEqual(states.props.arr.local, [1, 2, 3]); }); - it("can access root presence object", () => { + it(".presence provides Presence it was created under", () => { const states = presence.states.getWorkspace(testWorkspaceName, { camera: StateFactory.latest({ x: 0, y: 0, z: 0 }), }); diff --git a/packages/framework/presence/src/test/notificationsManager.spec.ts b/packages/framework/presence/src/test/notificationsManager.spec.ts index 141e001ee706..86d277aac9bf 100644 --- a/packages/framework/presence/src/test/notificationsManager.spec.ts +++ b/packages/framework/presence/src/test/notificationsManager.spec.ts @@ -488,7 +488,7 @@ describe("Presence", () => { assert(originalEventHandlerCalled, "originalEventHandler not called"); }); - it("can access root presence object", () => { + it(".presence provides Presence it was created under", () => { notificationsWorkspace.add( "testEvents", Notifications< diff --git a/packages/framework/presence/src/test/presenceStates.spec.ts b/packages/framework/presence/src/test/presenceStates.spec.ts index 60ed458817f8..e74030636fd5 100644 --- a/packages/framework/presence/src/test/presenceStates.spec.ts +++ b/packages/framework/presence/src/test/presenceStates.spec.ts @@ -32,7 +32,7 @@ describe("Presence", () => { return presence.states.getWorkspace("name:testWorkspaceA", {}, controlSettings); }); - it("can access root presence object", () => { + it(".presence provides Presence it was created under", () => { const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { obj: StateFactory.latest({}), From 5f745939ab0faa934f2ebb04342f5e34cd5c0f50 Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Sat, 19 Apr 2025 00:12:04 +0200 Subject: [PATCH 09/12] cleanup --- packages/framework/presence/src/test/presenceStates.spec.ts | 2 +- packages/framework/presence/src/types.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/framework/presence/src/test/presenceStates.spec.ts b/packages/framework/presence/src/test/presenceStates.spec.ts index e74030636fd5..862fe4ec738b 100644 --- a/packages/framework/presence/src/test/presenceStates.spec.ts +++ b/packages/framework/presence/src/test/presenceStates.spec.ts @@ -29,7 +29,7 @@ describe("Presence", () => { it("API use compiles", () => {}); addControlsTests((presence, controlSettings) => { - return presence.states.getWorkspace("name:testWorkspaceA", {}, controlSettings); + return presence.states.getWorkspace(testWorkspaceName, {}, controlSettings); }); it(".presence provides Presence it was created under", () => { diff --git a/packages/framework/presence/src/types.ts b/packages/framework/presence/src/types.ts index 0ce5c6b60ee7..1ddbb72102ed 100644 --- a/packages/framework/presence/src/types.ts +++ b/packages/framework/presence/src/types.ts @@ -109,7 +109,7 @@ export interface StatesWorkspace< readonly controls: BroadcastControls; /** - * Root Presence object. + * Containing {@link Presence} */ readonly presence: Presence; } @@ -169,7 +169,7 @@ export interface NotificationsWorkspace; /** - * Root Presence object. + * Containing {@link Presence} */ readonly presence: Presence; } From 1d6fb37b53c0c8ccf3806ae99ed5e427d43f0207 Mon Sep 17 00:00:00 2001 From: Willie Habimana Date: Mon, 21 Apr 2025 17:29:10 +0200 Subject: [PATCH 10/12] req changes --- packages/framework/presence/src/latestMapValueManager.ts | 5 +++-- packages/framework/presence/src/stateDatastore.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index ef51c821173c..951201bff386 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -377,11 +377,12 @@ class LatestMapValueManagerImpl< ); } - public readonly local: StateMap; - public get presence(): Presence { return this.datastore.presence; } + + public readonly local: StateMap; + public *getRemotes(): IterableIterator> { const allKnownStates = this.datastore.knownValues(this.key); for (const attendeeId of objectKeys(allKnownStates.states)) { diff --git a/packages/framework/presence/src/stateDatastore.ts b/packages/framework/presence/src/stateDatastore.ts index 44fddb65de9c..ef971a8ab62e 100644 --- a/packages/framework/presence/src/stateDatastore.ts +++ b/packages/framework/presence/src/stateDatastore.ts @@ -42,7 +42,7 @@ export interface StateDatastore< TKey extends string, TValue extends InternalTypes.ValueDirectoryOrState, > { - presence: Presence; + readonly presence: Presence; localUpdate( key: TKey, value: TValue & { From d1397d56b2a18cc58593b2dab660334f1881c1b2 Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:19:15 +0100 Subject: [PATCH 11/12] Update .changeset/giant-news-appear.md Co-authored-by: Tyler Butler --- .changeset/giant-news-appear.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/giant-news-appear.md b/.changeset/giant-news-appear.md index abd65ca60a7c..5fc83d6913a6 100644 --- a/.changeset/giant-news-appear.md +++ b/.changeset/giant-news-appear.md @@ -2,7 +2,7 @@ "@fluidframework/presence": minor "__section": other --- -Expose `Presence` at Workspaces and State objects +Presence object is now accessible from Workspaces and State objects Users can now access `Presence` through `.presence` in all Workspaces and State objects: From d4860f03e26d4378e87d9c21d8ef60ee1d44ed9b Mon Sep 17 00:00:00 2001 From: WillieHabi <143546745+WillieHabi@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:19:24 +0100 Subject: [PATCH 12/12] Update .changeset/giant-news-appear.md Co-authored-by: Tyler Butler --- .changeset/giant-news-appear.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/giant-news-appear.md b/.changeset/giant-news-appear.md index 5fc83d6913a6..0802a19fd9f6 100644 --- a/.changeset/giant-news-appear.md +++ b/.changeset/giant-news-appear.md @@ -4,7 +4,7 @@ --- Presence object is now accessible from Workspaces and State objects -Users can now access `Presence` through `.presence` in all Workspaces and State objects: +Users can now access the `Presence` object through `.presence` on all Workspaces and State objects: `Latest.presence` `LatestMap.presence`