Skip to content

refactor(presence): Pass arguments as an object to StateFactory.latest/latestMap #24414

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 25 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/apps/presence-tracker/src/FocusTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ export class FocusTracker extends TypedEventEmitter<IFocusTrackerEvents> {
// window.
statesWorkspace.add(
"focus",
StateFactory.latest<IFocusState>({ hasFocus: window.document.hasFocus() }),
StateFactory.latest<IFocusState>({
initialValue: { hasFocus: window.document.hasFocus() },
}),
);

// Save a reference to the focus state for easy access within the FocusTracker.
Expand Down
5 changes: 4 additions & 1 deletion examples/apps/presence-tracker/src/MouseTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export class MouseTracker extends TypedEventEmitter<IMouseTrackerEvents> {
super();

// Create a Latest state object to track the mouse position.
statesWorkspace.add("cursor", StateFactory.latest<IMousePosition>({ x: 0, y: 0 }));
statesWorkspace.add(
"cursor",
StateFactory.latest<IMousePosition>({ initialValue: { x: 0, y: 0 } }),
);

// Save a reference to the cursor state for easy access within the MouseTracker.
this.cursor = statesWorkspace.props.cursor;
Expand Down
26 changes: 22 additions & 4 deletions packages/framework/presence/api-report/presence.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export interface Latest<T> {
}

// @alpha
export function latest<T extends object, Key extends string = string>(initialValue: JsonSerializable<T> & JsonDeserialized<T> & object, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, Latest<T>>;
export function latest<T extends object, Key extends string = string>(props: LatestProps<T>): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, Latest<T>>;

// @alpha @sealed
export interface LatestClientData<T> extends LatestData<T> {
Expand Down Expand Up @@ -186,9 +186,7 @@ export interface LatestMap<T, Keys extends string | number = string | number> {
}

// @alpha
export function latestMap<T extends object, Keys extends string | number = string | number, RegistrationKey extends string = string>(initialValues?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
}, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory<RegistrationKey, InternalTypes.MapValueState<T, Keys>, LatestMap<T, Keys>>;
export function latestMap<T extends object, Keys extends string | number = string | number, RegistrationKey extends string = string>(props: LatestMapProps<T, Keys>): InternalTypes.ManagerFactory<RegistrationKey, InternalTypes.MapValueState<T, Keys>, LatestMap<T, Keys>>;

// @alpha @sealed
export interface LatestMapClientData<T, Keys extends string | number, SpecificAttendeeId extends AttendeeId = AttendeeId> {
Expand Down Expand Up @@ -232,12 +230,26 @@ export interface LatestMapItemUpdatedClientData<T, K extends string | number> ex
key: K;
}

// @alpha
export interface LatestMapProps<T extends object, Keys extends string | number = string | number> extends PresenceStateOptions {
// (undocumented)
initialValues?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
};
}

// @alpha @sealed
export interface LatestMetadata {
revision: number;
timestamp: number;
}

// @alpha (undocumented)
export interface LatestProps<T extends object> extends PresenceStateOptions {
// (undocumented)
initialValue: JsonSerializable<T> & JsonDeserialized<T> & object;
}

// @alpha @sealed
export interface NotificationEmitter<E extends InternalUtilityTypes.NotificationListeners<E>> {
broadcast<K extends string & keyof InternalUtilityTypes.NotificationListeners<E>>(notificationName: K, ...args: Parameters<E[K]>): void;
Expand Down Expand Up @@ -310,6 +322,12 @@ export interface PresenceEvents {
workspaceActivated: (workspaceAddress: WorkspaceAddress, type: "States" | "Notifications" | "Unknown") => void;
}

// @alpha
export interface PresenceStateOptions {
// (undocumented)
controls?: BroadcastControlSettings | undefined;
}

// @alpha
export const StateFactory: {
latest: typeof latest;
Expand Down
3 changes: 3 additions & 0 deletions packages/framework/presence/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,20 @@ export type {
LatestMapEvents,
LatestMapItemRemovedClientData,
LatestMapItemUpdatedClientData,
LatestMapProps,
StateMap,
} from "./latestMapValueManager.js";
export type {
latest,
Latest,
LatestProps,
LatestEvents,
} from "./latestValueManager.js";
export type {
LatestClientData,
LatestData,
LatestMetadata,
PresenceStateOptions,
} from "./latestValueTypes.js";

export {
Expand Down
34 changes: 28 additions & 6 deletions packages/framework/presence/src/latestMapValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import type { InternalTypes } from "./exposedInternalTypes.js";
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 {
LatestClientData,
LatestData,
LatestMetadata,
PresenceStateOptions,
} from "./latestValueTypes.js";
import type { AttendeeId, Attendee, Presence, SpecificAttendee } from "./presence.js";
import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js";
import { brandIVM } from "./valueManager.js";
Expand Down Expand Up @@ -489,6 +494,20 @@ class LatestMapValueManagerImpl<
}
}

/**
* Props passed to the {@link latestMap} function.
*
* @alpha
*/
export interface LatestMapProps<
T extends object,
Keys extends string | number = string | number,
> extends PresenceStateOptions {
initialValues?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
};
}

/**
* Factory for creating a {@link LatestMap} State object.
*
Expand All @@ -499,15 +518,14 @@ export function latestMap<
Keys extends string | number = string | number,
RegistrationKey extends string = string,
>(
initialValues?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
},
controls?: BroadcastControlSettings,
props: LatestMapProps<T, Keys>,
): InternalTypes.ManagerFactory<
RegistrationKey,
InternalTypes.MapValueState<T, Keys>,
LatestMap<T, Keys>
> {
const { controls, initialValues } = props;

const timestamp = Date.now();
const value: InternalTypes.MapValueState<
T,
Expand All @@ -517,7 +535,11 @@ export function latestMap<
// LatestMap takes ownership of values within initialValues.
if (initialValues !== undefined) {
for (const key of objectKeys(initialValues)) {
value.items[key] = { rev: 0, timestamp, value: initialValues[key] };
value.items[key] = {
rev: 0,
timestamp,
value: initialValues[key],
};
}
}
const factory = (
Expand Down
18 changes: 15 additions & 3 deletions packages/framework/presence/src/latestValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import type { InternalTypes } from "./exposedInternalTypes.js";
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 {
LatestClientData,
LatestData,
PresenceStateOptions,
} from "./latestValueTypes.js";
import type { Attendee, Presence } from "./presence.js";
import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js";
import { brandIVM } from "./valueManager.js";
Expand Down Expand Up @@ -181,15 +185,23 @@ class LatestValueManagerImpl<T, Key extends string>
}
}

/**
* @alpha
*/
export interface LatestProps<T extends object> extends PresenceStateOptions {
initialValue: JsonSerializable<T> & JsonDeserialized<T> & object;
}

/**
* Factory for creating a {@link Latest} State object.
*
* @alpha
*/
export function latest<T extends object, Key extends string = string>(
initialValue: JsonSerializable<T> & JsonDeserialized<T> & object,
controls?: BroadcastControlSettings,
props: LatestProps<T>,
): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, Latest<T>> {
const { controls, initialValue } = props;

// Latest takes ownership of initialValue but makes a shallow
// copy for basic protection.
const value: InternalTypes.ValueRequiredState<T> = {
Expand Down
10 changes: 10 additions & 0 deletions packages/framework/presence/src/latestValueTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import type { JsonDeserialized } from "@fluidframework/core-interfaces/internal/exposedUtilityTypes";

import type { BroadcastControlSettings } from "./broadcastControls.js";
import type { InternalUtilityTypes } from "./exposedUtilityTypes.js";
import type { Attendee } from "./presence.js";

Expand Down Expand Up @@ -46,3 +47,12 @@ export interface LatestData<T> {
export interface LatestClientData<T> extends LatestData<T> {
attendee: Attendee;
}

/**
* Options that can be provided to a Presence state manager. TODO: Add details.
*
* @alpha
*/
export interface PresenceStateOptions {
controls?: BroadcastControlSettings | undefined;
}
53 changes: 42 additions & 11 deletions packages/framework/presence/src/test/batching.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ describe("Presence", () => {
// Configure a state workspace
// SIGNAL #1 - intial data is sent immediately
const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", {
count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }),
count: StateFactory.latest({
initialValue: { num: 0 },
controls: { allowableUpdateLatencyMs: 0 },
}),
});

const { count } = stateWorkspace.props;
Expand Down Expand Up @@ -193,7 +196,9 @@ describe("Presence", () => {

// Configure a state workspace
presence.states.getWorkspace("name:testStateWorkspace", {
count: StateFactory.latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */),
count: StateFactory.latest({
initialValue: { num: 0 } /* default allowableUpdateLatencyMs = 60 */,
}),
}); // will be queued; deadline is now 1070

// SIGNAL #1
Expand Down Expand Up @@ -267,7 +272,9 @@ describe("Presence", () => {

// Configure a state workspace
const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", {
count: StateFactory.latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */),
count: StateFactory.latest({
initialValue: { num: 0 } /* default allowableUpdateLatencyMs = 60 */,
}),
}); // will be queued; deadline is now 1070

const { count } = stateWorkspace.props;
Expand Down Expand Up @@ -369,7 +376,10 @@ describe("Presence", () => {

// Configure a state workspace
const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", {
count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }),
count: StateFactory.latest({
initialValue: { num: 0 },
controls: { allowableUpdateLatencyMs: 100 },
}),
});

const { count } = stateWorkspace.props;
Expand Down Expand Up @@ -488,8 +498,14 @@ describe("Presence", () => {
// SIGNAL #1 - this signal is not queued because it contains a State object with a latency of 0,
// so the initial data will be sent immediately.
const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", {
count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }),
immediateUpdate: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }),
count: StateFactory.latest({
initialValue: { num: 0 },
controls: { allowableUpdateLatencyMs: 100 },
}),
immediateUpdate: StateFactory.latest({
initialValue: { num: 0 },
controls: { allowableUpdateLatencyMs: 0 },
}),
});

const { count, immediateUpdate } = stateWorkspace.props;
Expand Down Expand Up @@ -575,8 +591,14 @@ describe("Presence", () => {

// Configure a state workspace
const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", {
count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }),
note: StateFactory.latest({ message: "" }, { allowableUpdateLatencyMs: 50 }),
count: StateFactory.latest({
initialValue: { num: 0 },
controls: { allowableUpdateLatencyMs: 100 },
}),
note: StateFactory.latest({
initialValue: { message: "" },
controls: { allowableUpdateLatencyMs: 50 },
}),
}); // will be queued, deadline is set to 1060

const { count, note } = stateWorkspace.props;
Expand Down Expand Up @@ -647,11 +669,17 @@ describe("Presence", () => {

// Configure two state workspaces
const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", {
count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }),
count: StateFactory.latest({
initialValue: { num: 0 },
controls: { allowableUpdateLatencyMs: 100 },
}),
}); // will be queued, deadline is 1110

const stateWorkspace2 = presence.states.getWorkspace("name:testStateWorkspace2", {
note: StateFactory.latest({ message: "" }, { allowableUpdateLatencyMs: 60 }),
note: StateFactory.latest({
initialValue: { message: "" },
controls: { allowableUpdateLatencyMs: 60 },
}),
}); // will be queued, deadline is 1070

const { count } = stateWorkspace.props;
Expand Down Expand Up @@ -840,7 +868,10 @@ describe("Presence", () => {

// Configure a state workspace
const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", {
count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }),
count: StateFactory.latest({
initialValue: { num: 0 },
controls: { allowableUpdateLatencyMs: 100 },
}),
}); // will be queued, deadline is 1110

// eslint-disable-next-line @typescript-eslint/ban-types
Expand Down
12 changes: 8 additions & 4 deletions packages/framework/presence/src/test/eventing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,10 @@ describe("Presence", () => {
notifications,
}: { notifications?: true } = {}): void {
const states = presence.states.getWorkspace("name:testWorkspace", {
latest: StateFactory.latest({ x: 0, y: 0, z: 0 }),
latestMap: StateFactory.latestMap({ key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }),
latest: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }),
latestMap: StateFactory.latestMap({
initialValues: { key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } },
}),
});
latest = states.props.latest;
latestMap = states.props.latestMap;
Expand All @@ -261,10 +263,12 @@ describe("Presence", () => {

function setupMultipleStatesWorkspaces(): void {
const latestsStates = presence.states.getWorkspace("name:testWorkspace1", {
latest: StateFactory.latest({ x: 0, y: 0, z: 0 }),
latest: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }),
});
const latesetMapStates = presence.states.getWorkspace("name:testWorkspace2", {
latestMap: StateFactory.latestMap({ key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }),
latestMap: StateFactory.latestMap({
initialValues: { key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } },
}),
});
latest = latestsStates.props.latest;
latestMap = latesetMapStates.props.latestMap;
Expand Down
Loading
Loading