diff --git a/.changeset/giant-news-appear.md b/.changeset/giant-news-appear.md new file mode 100644 index 000000000000..0802a19fd9f6 --- /dev/null +++ b/.changeset/giant-news-appear.md @@ -0,0 +1,13 @@ +--- +"@fluidframework/presence": minor +"__section": other +--- +Presence object is now accessible from Workspaces and State objects + +Users can now access the `Presence` object through `.presence` on all Workspaces and State objects: + +`Latest.presence` +`LatestMap.presence` +`Notifications.presence` +`NotificationsWorkspace.presence` +`StatesWorkspace.presence` diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 9b108fe2f340..508fcdbe60a3 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -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) @@ -272,6 +275,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 +335,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/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 840905de1c28..951201bff386 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"; @@ -311,6 +311,11 @@ class ValueMapImpl implements StateMap { * @alpha */ export interface LatestMap { + /** + * Containing {@link Presence} + */ + readonly presence: Presence; + /** * Events for LatestMap. */ @@ -372,6 +377,10 @@ class LatestMapValueManagerImpl< ); } + public get presence(): Presence { + return this.datastore.presence; + } + public readonly local: StateMap; public *getRemotes(): IterableIterator> { diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 7fe34e4e5578..0c1c2456eb1d 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"; @@ -54,6 +54,11 @@ export interface LatestEvents { * @alpha */ export interface Latest { + /** + * Containing {@link Presence} + */ + readonly presence: Presence; + /** * Events for Latest. */ @@ -102,6 +107,10 @@ class LatestValueManagerImpl this.controls = new OptionalBroadcastControl(controlSettings); } + public get presence(): Presence { + return this.datastore.presence; + } + public get local(): InternalUtilityTypes.FullyReadonly> { return this.value.value; } diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index 7fc38e8d0a18..8c85d979b997 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"; @@ -130,6 +130,11 @@ export interface NotificationEmitter, > { + /** + * Containing {@link Presence} + */ + readonly presence: Presence; + /** * Events for Notifications manager. */ @@ -222,6 +227,10 @@ class NotificationsManagerImpl< } } + public get presence(): Presence { + return this.datastore.presence; + } + public update( attendee: Attendee, _received: number, diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index c875ecf59c67..9802997cbc70 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, ) { @@ -219,6 +220,7 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { const entry = createPresenceStates( { + presence: this.presence, attendeeId: this.attendeeId, lookupClient: this.lookupClient, localUpdate, 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, ); diff --git a/packages/framework/presence/src/presenceStates.ts b/packages/framework/presence/src/presenceStates.ts index 55d19d6bd259..a1651d057c33 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"; @@ -51,6 +51,7 @@ export interface RuntimeLocalUpdateOptions { * @internal */ export interface PresenceRuntime { + readonly presence: Presence; readonly attendeeId: AttendeeId; lookupClient(clientId: ClientConnectionId): Attendee; localUpdate( @@ -306,6 +307,10 @@ class PresenceStatesImpl } } + public get presence(): Presence { + return this.runtime.presence; + } + public knownValues( key: Key, ): { diff --git a/packages/framework/presence/src/stateDatastore.ts b/packages/framework/presence/src/stateDatastore.ts index 1bec30785b81..ef971a8ab62e 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, > { + readonly presence: Presence; localUpdate( key: TKey, value: TValue & { diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index b8a024bdd378..b7dc5923a359 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(".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 } }), + }); + + 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..8ef15bbe117d 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(".presence provides Presence it was created under", () => { + 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..86d277aac9bf 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(".presence provides Presence it was created under", () => { + 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..862fe4ec738b 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", () => { @@ -21,7 +29,15 @@ 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", () => { + const presence = createPresenceManager(new MockEphemeralRuntime()); + const states = presence.states.getWorkspace(testWorkspaceName, { + obj: StateFactory.latest({}), + }); + assert.strictEqual(states.presence, presence); }); }); }); diff --git a/packages/framework/presence/src/types.ts b/packages/framework/presence/src/types.ts index b3d7da22f57c..1ddbb72102ed 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; + + /** + * Containing {@link Presence} + */ + readonly presence: Presence; } // #endregion StatesWorkspace @@ -161,6 +167,11 @@ export interface NotificationsWorkspace; + + /** + * Containing {@link Presence} + */ + readonly presence: Presence; } // #endregion NotificationsWorkspace