From a47e462bbc34e603c2dd3833365e8ce6b022a006 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 14:39:04 -0700 Subject: [PATCH 01/18] wip --- .../presence/api-report/presence.alpha.api.md | 24 +- .../presence/src/exposedInternalTypes.ts | 8 + packages/framework/presence/src/index.ts | 3 + .../presence/src/latestMapValueManager.ts | 66 +++-- .../presence/src/latestValueManager.ts | 52 +++- .../presence/src/latestValueTypes.ts | 46 +++- .../presence/src/test/batching.spec.ts | 53 +++- .../src/test/latestMapValueManager.spec.ts | 3 +- .../src/test/latestValueManager.spec.ts | 6 +- .../src/test/schemaValidation.spec.ts | 230 ++++++++++++++++++ .../framework/presence/src/test/testUtils.ts | 40 ++- 11 files changed, 491 insertions(+), 40 deletions(-) create mode 100644 packages/framework/presence/src/test/schemaValidation.spec.ts diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 9b108fe2f340..aa83c212242a 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -105,11 +105,13 @@ export namespace InternalTypes { export type ValueDirectoryOrState = ValueRequiredState | ValueDirectory; // (undocumented) export interface ValueOptionalState extends ValueStateMetadata { + valid?: TValue | undefined; // (undocumented) value?: JsonDeserialized; } // (undocumented) export interface ValueRequiredState extends ValueStateMetadata { + valid?: TValue | undefined; // (undocumented) value: JsonDeserialized; } @@ -147,7 +149,7 @@ export interface Latest { } // @alpha -export function latest(initialValue: JsonSerializable & JsonDeserialized & object, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, Latest>; +export function latest(initialValue: JsonSerializable & JsonDeserialized & object, options?: PresenceStateOptions): InternalTypes.ManagerFactory, Latest>; // @alpha @sealed export interface LatestClientData extends LatestData { @@ -160,7 +162,7 @@ export interface LatestData { // (undocumented) metadata: LatestMetadata; // (undocumented) - value: InternalUtilityTypes.FullyReadonly>; + value: InternalUtilityTypes.FullyReadonly> | undefined; } // @alpha @sealed (undocumented) @@ -186,7 +188,7 @@ export interface LatestMap { // @alpha export function latestMap(initialValues?: { [K in Keys]: JsonSerializable & JsonDeserialized; -}, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, LatestMap>; +}, options?: PresenceStateOptions | undefined): InternalTypes.ManagerFactory, LatestMap>; // @alpha @sealed export interface LatestMapClientData { @@ -306,6 +308,14 @@ export interface PresenceEvents { workspaceActivated: (workspaceAddress: WorkspaceAddress, type: "States" | "Notifications" | "Unknown") => void; } +// @alpha +export interface PresenceStateOptions { + // (undocumented) + controls?: BroadcastControlSettings | undefined; + // (undocumented) + validator?: StateSchemaValidator | undefined; +} + // @alpha export const StateFactory: { latest: typeof latest; @@ -327,6 +337,14 @@ export interface StateMap { readonly size: number; } +// @alpha +export type StateSchemaValidator = (unvalidatedData: unknown, metadata?: StateSchemaValidatorMetadata) => T | undefined; + +// @alpha +export interface StateSchemaValidatorMetadata { + key?: string | number; +} + // @alpha @sealed export interface StatesWorkspace { add, TManager extends TManagerConstraints>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is StatesWorkspace>, TManagerConstraints>; diff --git a/packages/framework/presence/src/exposedInternalTypes.ts b/packages/framework/presence/src/exposedInternalTypes.ts index 6d86d9b2f71f..abfdfc4dd85c 100644 --- a/packages/framework/presence/src/exposedInternalTypes.ts +++ b/packages/framework/presence/src/exposedInternalTypes.ts @@ -30,6 +30,10 @@ export namespace InternalTypes { */ export interface ValueOptionalState extends ValueStateMetadata { value?: JsonDeserialized; + /** + * Contains the validated data, or `undefined` if the value has not been validated. + */ + valid?: TValue | undefined; } /** @@ -37,6 +41,10 @@ export namespace InternalTypes { */ export interface ValueRequiredState extends ValueStateMetadata { value: JsonDeserialized; + /** + * Contains the validated data, or `undefined` if the value has not been validated. + */ + valid?: TValue | undefined; } /** diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 8fc483c90bbd..2f7ab9de35ce 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -63,6 +63,9 @@ export type { LatestClientData, LatestData, LatestMetadata, + StateSchemaValidator, + StateSchemaValidatorMetadata, + PresenceStateOptions, } from "./latestValueTypes.js"; export { diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 840905de1c28..c430e27915ca 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -17,7 +17,13 @@ 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, + StateSchemaValidator, + PresenceStateOptions, +} from "./latestValueTypes.js"; import type { AttendeeId, Attendee, SpecificAttendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -218,6 +224,7 @@ class ValueMapImpl implements StateMap { string | number >, ) => void, + private readonly validator: StateSchemaValidator | undefined, ) { // All initial items are expected to be defined. // TODO assert all defined and/or update type. @@ -264,6 +271,7 @@ class ValueMapImpl implements StateMap { ) => void, thisArg?: unknown, ): void { + // TODO: This is a data read, so we need to validate. for (const [key, item] of objectEntries(this.value.items)) { if (item.value !== undefined) { callbackfn(item.value, key, this); @@ -271,7 +279,13 @@ class ValueMapImpl implements StateMap { } } public get(key: K): InternalUtilityTypes.FullyReadonly> | undefined { - return this.value.items[key]?.value; + const data = this.value.items[key]?.value; + if (this.validator === undefined) { + return data; + } + const maybeValid = this.validator(data, { key }); + // TODO: Cast shouldn't be necessary. + return maybeValid as InternalUtilityTypes.FullyReadonly> | undefined; } public has(key: K): boolean { return this.value.items[key]?.value !== undefined; @@ -357,6 +371,7 @@ class LatestMapValueManagerImpl< InternalTypes.MapValueState >, public readonly value: InternalTypes.MapValueState, + validator: StateSchemaValidator | undefined, controlSettings: BroadcastControlSettings | undefined, ) { this.controls = new OptionalBroadcastControl(controlSettings); @@ -369,6 +384,7 @@ class LatestMapValueManagerImpl< allowableUpdateLatencyMs: this.controls.allowableUpdateLatencyMs, }); }, + validator, ); } @@ -401,12 +417,14 @@ class LatestMapValueManagerImpl< } const items = new Map>(); for (const [key, item] of objectEntries(clientStateMap.items)) { - const value = item.value; - if (value !== undefined) { - items.set(key, { - value, - metadata: { revision: item.rev, timestamp: item.timestamp }, - }); + if (item.value !== undefined) { + const value = item.value; + if (value !== undefined) { + items.set(key, { + value, + metadata: { revision: item.rev, timestamp: item.timestamp }, + }); + } } } return items; @@ -453,7 +471,10 @@ class LatestMapValueManagerImpl< const item = value.items[key]!; const hadPriorValue = currentState.items[key]?.value; currentState.items[key] = item; - const metadata = { revision: item.rev, timestamp: item.timestamp }; + const metadata = { + revision: item.rev, + timestamp: item.timestamp, + }; if (item.value !== undefined) { const itemValue = item.value; const updatedItem = { @@ -480,6 +501,18 @@ class LatestMapValueManagerImpl< } } +/** + * Props passed to the {@link latestMap} function to + */ +export interface LatestMapProps< +T extends object, +Keys extends string | number = string | number, +> extends PresenceStateOptions { + initialValues?: { + [K in Keys]: JsonSerializable & JsonDeserialized; + }, +} + /** * Factory for creating a {@link LatestMap} State object. * @@ -490,15 +523,15 @@ export function latestMap< Keys extends string | number = string | number, RegistrationKey extends string = string, >( - initialValues?: { - [K in Keys]: JsonSerializable & JsonDeserialized; - }, - controls?: BroadcastControlSettings, + props: LatestMapProps ): InternalTypes.ManagerFactory< RegistrationKey, InternalTypes.MapValueState, LatestMap > { + const {controls, initialValues, validator} = props; + + const timestamp = Date.now(); const value: InternalTypes.MapValueState< T, @@ -508,7 +541,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 = ( @@ -531,6 +568,7 @@ export function latestMap< key, datastoreFromHandle(datastoreHandle), value, + validator, controls, ), ), diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 7fe34e4e5578..33dc10365630 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -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 } from "./internalUtils.js"; -import type { LatestClientData, LatestData } from "./latestValueTypes.js"; +import type { + LatestClientData, + LatestData, + StateSchemaValidator, + PresenceStateOptions, +} from "./latestValueTypes.js"; import type { Attendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -97,6 +102,7 @@ class LatestValueManagerImpl private readonly key: Key, private readonly datastore: StateDatastore>, public readonly value: InternalTypes.ValueRequiredState, + private readonly validator: StateSchemaValidator | undefined, controlSettings: BroadcastControlSettings | undefined, ) { this.controls = new OptionalBroadcastControl(controlSettings); @@ -121,9 +127,14 @@ class LatestValueManagerImpl const allKnownStates = this.datastore.knownValues(this.key); for (const [attendeeId, value] of objectEntries(allKnownStates.states)) { if (attendeeId !== allKnownStates.self) { + if (value.valid === true && this.validator !== undefined) { + const validData = this.validator(value.value); + value.valid = validData; + } + yield { attendee: this.datastore.lookupClient(attendeeId), - value: value.value, + value: value.valid === undefined ? undefined : value.value, metadata: { revision: value.rev, timestamp: value.timestamp }, }; } @@ -143,8 +154,15 @@ class LatestValueManagerImpl if (clientState === undefined) { throw new Error("No entry for clientId"); } + + // If + if (clientState.valid !== true && this.validator !== undefined) { + const validData = this.validator(clientState); + clientState.valid = validData; + } + return { - value: clientState.value, + value: clientState.valid === undefined ? undefined : clientState.value, metadata: { revision: clientState.rev, timestamp: Date.now() }, }; } @@ -172,15 +190,26 @@ class LatestValueManagerImpl } } +/** + * + */ +export interface LatestProps< +T extends object, +> extends PresenceStateOptions { + initialValue: JsonSerializable & JsonDeserialized & object, +} + + /** * Factory for creating a {@link Latest} State object. * * @alpha */ export function latest( - initialValue: JsonSerializable & JsonDeserialized & object, - controls?: BroadcastControlSettings, + props: LatestProps, ): InternalTypes.ManagerFactory, Latest> { + const {controls, initialValue, validator} = props; + // Latest takes ownership of initialValue but makes a shallow // copy for basic protection. const value: InternalTypes.ValueRequiredState = { @@ -198,9 +227,18 @@ export function latest( initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; manager: InternalTypes.StateValue>; } => ({ - initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs }, + initialData: { + value, + allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs, + }, manager: brandIVM, T, InternalTypes.ValueRequiredState>( - new LatestValueManagerImpl(key, datastoreFromHandle(datastoreHandle), value, controls), + new LatestValueManagerImpl( + key, + datastoreFromHandle(datastoreHandle), + value, + validator, + controls, + ), ), }); return Object.assign(factory, { instanceBase: LatestValueManagerImpl }); diff --git a/packages/framework/presence/src/latestValueTypes.ts b/packages/framework/presence/src/latestValueTypes.ts index 0328eb3c0bc1..d8ac22bd5341 100644 --- a/packages/framework/presence/src/latestValueTypes.ts +++ b/packages/framework/presence/src/latestValueTypes.ts @@ -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"; @@ -33,7 +34,7 @@ export interface LatestMetadata { * @alpha */ export interface LatestData { - value: InternalUtilityTypes.FullyReadonly>; + value: InternalUtilityTypes.FullyReadonly> | undefined; metadata: LatestMetadata; } @@ -46,3 +47,46 @@ export interface LatestData { export interface LatestClientData extends LatestData { attendee: Attendee; } + +/** + * A validator function that can optionally be provided to do runtime validation of the custom data stored in a + * presence workspace and managed by a value manager. + * + * @alpha + */ +export type StateSchemaValidator = ( + unvalidatedData: unknown, + metadata?: StateSchemaValidatorMetadata, +) => T | undefined; + +/** + * Optional metadata that is passed to a {@link StateSchemaValidator}. + * + * @alpha + * + * TODO: What else needs to be in the metadata? + */ +export interface StateSchemaValidatorMetadata { + /** + * If the value being validated is a LatestValueMap value, this will be set to the value of the corresponding key. + */ + key?: string | number; +} + +/** + * Type guard that checks if a value is a state schema validator. + * @param fn - A function that may be a schema validator. + */ +export function isStateSchemaValidator(fn: unknown): fn is StateSchemaValidator { + return typeof fn === "function"; +} + +/** + * Options that can be provided to a Presence state manager. TODO: Add details. + * + * @alpha + */ +export interface PresenceStateOptions { + validator?: StateSchemaValidator | undefined; + controls?: BroadcastControlSettings | undefined; +} diff --git a/packages/framework/presence/src/test/batching.spec.ts b/packages/framework/presence/src/test/batching.spec.ts index 4e11e6aa16bc..45dcf36f8af9 100644 --- a/packages/framework/presence/src/test/batching.spec.ts +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -12,7 +12,13 @@ import { Notifications, StateFactory } from "../index.js"; import type { createPresenceManager } from "../presenceManager.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; -import { assertFinalExpectations, prepareConnectedPresence } from "./testUtils.js"; +import { + assertFinalExpectations, + createNullValidator, + prepareConnectedPresence, +} from "./testUtils.js"; + +const validator = createNullValidator(); describe("Presence", () => { describe("batching", () => { @@ -144,7 +150,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( + { num: 0 }, + { validator, controls: { allowableUpdateLatencyMs: 0 } }, + ), }); const { count } = stateWorkspace.props; @@ -369,7 +378,10 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest( + { num: 0 }, + { controls: { allowableUpdateLatencyMs: 100 } }, + ), }); const { count } = stateWorkspace.props; @@ -488,8 +500,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( + { num: 0 }, + { controls: { allowableUpdateLatencyMs: 100 } }, + ), + immediateUpdate: StateFactory.latest( + { num: 0 }, + { controls: { allowableUpdateLatencyMs: 0 } }, + ), }); const { count, immediateUpdate } = stateWorkspace.props; @@ -575,8 +593,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( + { num: 0 }, + { controls: { allowableUpdateLatencyMs: 100 } }, + ), + note: StateFactory.latest( + { message: "" }, + { controls: { allowableUpdateLatencyMs: 50 } }, + ), }); // will be queued, deadline is set to 1060 const { count, note } = stateWorkspace.props; @@ -647,11 +671,17 @@ describe("Presence", () => { // Configure two state workspaces const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest( + { 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( + { message: "" }, + { controls: { allowableUpdateLatencyMs: 60 } }, + ), }); // will be queued, deadline is 1070 const { count } = stateWorkspace.props; @@ -840,7 +870,10 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest( + { num: 0 }, + { controls: { allowableUpdateLatencyMs: 100 } }, + ), }); // will be queued, deadline is 1110 // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index b8a024bdd378..165ea0eb1824 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -9,6 +9,7 @@ import { createPresenceManager } from "../presenceManager.js"; import { addControlsTests } from "./broadcastControlsTests.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; +import { createNullValidator } from "./testUtils.js"; import type { BroadcastControlSettings, @@ -28,7 +29,7 @@ function createLatestMapManager( const states = presence.states.getWorkspace(testWorkspaceName, { fixedMap: StateFactory.latestMap( { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, - valueControlSettings, + { validator: createNullValidator(), controls: valueControlSettings }, ), }); return states.props.fixedMap; diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index 39718f9e85dc..aa58a8193e2e 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -25,7 +25,7 @@ function createLatestManager( valueControlSettings?: BroadcastControlSettings, ) { const states = presence.states.getWorkspace(testWorkspaceName, { - camera: StateFactory.latest({ x: 0, y: 0, z: 0 }, valueControlSettings), + camera: StateFactory.latest({ x: 0, y: 0, z: 0 }, { controls: valueControlSettings }), }); return states.props.camera; } @@ -135,7 +135,9 @@ export function checkCompiles(): void { // Listen to others cursor updates const cursorUpdatedOff = cursor.events.on("remoteUpdated", ({ attendee, value }) => - console.log(`attendee ${attendee.attendeeId}'s cursor is now at (${value.x},${value.y})`), + console.log( + `attendee ${attendee.attendeeId}'s cursor is now at (${value?.x},${value?.y})`, + ), ); cursorUpdatedOff(); diff --git a/packages/framework/presence/src/test/schemaValidation.spec.ts b/packages/framework/presence/src/test/schemaValidation.spec.ts new file mode 100644 index 000000000000..896a9415aa2a --- /dev/null +++ b/packages/framework/presence/src/test/schemaValidation.spec.ts @@ -0,0 +1,230 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { strict as assert } from "node:assert"; + +import { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal"; +import { describe, it, after, afterEach, before, beforeEach } from "mocha"; +import { useFakeTimers, type SinonFakeTimers } from "sinon"; + +import { + StateFactory, + // type PresenceStates, + type StateSchemaValidator, + // SessionClientStatus, + // type ClientConnectionId, + // type ISessionClient, +} from "../index.js"; +import type { createPresenceManager } from "../presenceManager.js"; + +import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; +import { + assertFinalExpectations, + createNullValidator, + createSpiedValidator, + // generateBasicClientJoin, + prepareConnectedPresence, + type ValidatorSpy, +} from "./testUtils.js"; + +describe("Presence", () => { + let runtime: MockEphemeralRuntime; + let logger: EventAndErrorTrackingLogger; + const initialTime = 1000; + let clock: SinonFakeTimers; + + before(async () => { + clock = useFakeTimers(); + }); + + beforeEach(() => { + logger = new EventAndErrorTrackingLogger(); + runtime = new MockEphemeralRuntime(logger); + clock.setSystemTime(initialTime); + }); + + afterEach(function (done: Mocha.Done) { + clock.reset(); + + // If the test passed so far, check final expectations. + if (this.currentTest?.state === "passed") { + assertFinalExpectations(runtime, logger); + } + done(); + }); + + after(() => { + clock.restore(); + }); + + describe("schema validation", () => { + let presence: ReturnType; + const afterCleanUp: (() => void)[] = []; + + beforeEach(() => { + presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); + }); + + afterEach(() => { + for (const cleanUp of afterCleanUp) { + cleanUp(); + } + afterCleanUp.length = 0; + }); + + describe("LatestValueManager", () => { + // let stateWorkspace: PresenceStates<{ num: 0 }>; + let validatorFunction: StateSchemaValidator<{ num: number }>; + let validatorSpy: ValidatorSpy; + + beforeEach(() => { + // Ignore submitted signals + runtime.submitSignal = () => {}; + + [validatorFunction, validatorSpy] = createSpiedValidator<{ num: number }>( + createNullValidator(), + ); + + assert.equal(validatorSpy.callCount, 0); + }); + + it("validator is called when data is read", () => { + // Setup + // Configure a state workspace + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { + count: StateFactory.latest( + { num: 0 }, + { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, + ), + }); + + const { count } = stateWorkspace.props; + + // Act & Verify + count.local = { num: 84 }; + + const value = count.local; + + // Reading the data should cause the validator to get called once. + assert.equal(validatorSpy.callCount, 1); + assert.equal(value.num, 84); + }); + + // TODO: test is failing + it.skip("validator is not called multiple times for the same data", () => { + // Setup + // Configure a state workspace + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { + count: StateFactory.latest( + { num: 0 }, + { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, + ), + }); + + const { count } = stateWorkspace.props; + count.local = { num: 84 }; + + // Act & Verify + // Reading the data should cause the validator to get called once. + let value = count.getRemote(presence.attendees.getMyself()); + + // Subsequent reads should not call the validator when there is no new data. + value = count.getRemote(presence.attendees.getMyself()); + value = count.getRemote(presence.attendees.getMyself()); + assert.equal(validatorSpy.callCount, 1); + assert.equal(value.value?.num, 84); + }); + + // TODO: test is failing + it("throws on invalid data", () => { + // Setup + // Configure a state workspace + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { + count: StateFactory.latest( + { num: 0 }, + { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, + ), + }); + + const { count } = stateWorkspace.props; + count.local = 84 as unknown as { num: number }; + + // Act & Verify + // Reading the data should cause the validator to get called once. + let value = count.getRemote(presence.attendees.getMyself()); + + // Subsequent reads should not call the validator when there is no new data. + value = count.getRemote(presence.attendees.getMyself()); + value = count.getRemote(presence.attendees.getMyself()); + assert.equal(value.value?.num, 84); + assert.equal(validatorSpy.callCount, 1); + }); + }); + + // TODO: tests are failing + describe.skip("LatestMapValueManager", () => { + // let stateWorkspace: PresenceStates<{ num: 0 }>; + let validatorFunction: StateSchemaValidator<{ num: number }>; + let validatorSpy: ValidatorSpy; + + beforeEach(() => { + // Ignore submitted signals + runtime.submitSignal = () => {}; + + [validatorFunction, validatorSpy] = createSpiedValidator<{ num: number }>( + createNullValidator(), + ); + + assert.equal(validatorSpy.callCount, 0); + }); + + it("validator is called when data is read", () => { + // Setup + // Configure a state workspace + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { + count: StateFactory.latestMap( + { "key1": { num: 0 } }, + { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, + ), + }); + + const { count } = stateWorkspace.props; + + // Act & Verify + count.local.set("key1", { num: 84 }); + + const value = count.getRemote(presence.attendees.getMyself()); + + // Reading the data should cause the validator to get called once. + assert.equal(validatorSpy.callCount, 1); + assert.equal(value.get("key1")?.value?.num, 84); + }); + + it("validator is not called multiple times for the same data", () => { + // Setup + // Configure a state workspace + const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { + count: StateFactory.latestMap( + { "key1": { num: 0 } }, + { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, + ), + }); + + const { count } = stateWorkspace.props; + count.local.set("key1", { num: 84 }); + + // Act & Verify + // Reading the data should cause the validator to get called once. + let value = count.getRemote(presence.attendees.getMyself()); + + // Subsequent reads should not call the validator when there is no new data. + value = count.getRemote(presence.attendees.getMyself()); + value = count.getRemote(presence.attendees.getMyself()); + assert.equal(validatorSpy.callCount, 1); + assert.equal(value.get("key1")?.value?.num, 84); + }); + }); + }); +}); diff --git a/packages/framework/presence/src/test/testUtils.ts b/packages/framework/presence/src/test/testUtils.ts index ec1112bcb838..f3dd95c599cf 100644 --- a/packages/framework/presence/src/test/testUtils.ts +++ b/packages/framework/presence/src/test/testUtils.ts @@ -6,13 +6,17 @@ import type { InternalUtilityTypes } from "@fluidframework/core-interfaces/internal"; import type { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal"; import { getUnexpectedLogErrorException } from "@fluidframework/test-utils/internal"; -import type { SinonFakeTimers } from "sinon"; +import type { SinonFakeTimers, SinonSpy } from "sinon"; import { createPresenceManager } from "../presenceManager.js"; import type { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; -import type { ClientConnectionId, AttendeeId } from "@fluidframework/presence/alpha"; +import type { + ClientConnectionId, + AttendeeId, + StateSchemaValidator, +} from "@fluidframework/presence/alpha"; import type { IExtensionMessage } from "@fluidframework/presence/internal/container-definitions/internal"; /** @@ -153,3 +157,35 @@ export function assertFinalExpectations( // Make sure all expected signals were sent. runtime.assertAllSignalsSubmitted(); } + +/** + * Creates a null validator (one that does nothing) for a given type T. + */ +export function createNullValidator(): StateSchemaValidator { + const nullValidator: StateSchemaValidator = (data: unknown) => { + return data as T; + }; + return nullValidator; +} + +/** + * A validator function spy. + */ +export type ValidatorSpy = Pick; + +/** + * Creates a validator and a spy for test purposes. + */ +export function createSpiedValidator( + validator: StateSchemaValidator, +): [StateSchemaValidator, ValidatorSpy] { + const spy: ValidatorSpy = { + callCount: 0, + }; + + const nullValidatorSpy: StateSchemaValidator = (data: unknown) => { + spy.callCount++; + return validator(data) as T; + }; + return [nullValidatorSpy, spy]; +} From c17e6a75f2e2865b377de5a0bbaff9be822373d6 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 14:52:54 -0700 Subject: [PATCH 02/18] updates --- .../framework/presence/src/latestMapValueManager.ts | 11 +++++------ packages/framework/presence/src/latestValueManager.ts | 9 +++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index c5c25a9e37b8..2cd1e53363b7 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -514,12 +514,12 @@ class LatestMapValueManagerImpl< * Props passed to the {@link latestMap} function to */ export interface LatestMapProps< -T extends object, -Keys extends string | number = string | number, + T extends object, + Keys extends string | number = string | number, > extends PresenceStateOptions { initialValues?: { [K in Keys]: JsonSerializable & JsonDeserialized; - }, + }; } /** @@ -532,14 +532,13 @@ export function latestMap< Keys extends string | number = string | number, RegistrationKey extends string = string, >( - props: LatestMapProps + props: LatestMapProps, ): InternalTypes.ManagerFactory< RegistrationKey, InternalTypes.MapValueState, LatestMap > { - const {controls, initialValues, validator} = props; - + const { controls, initialValues, validator } = props; const timestamp = Date.now(); const value: InternalTypes.MapValueState< diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 3ee12c0d5df1..8a0cb49a95f1 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -202,13 +202,10 @@ class LatestValueManagerImpl /** * */ -export interface LatestProps< -T extends object, -> extends PresenceStateOptions { - initialValue: JsonSerializable & JsonDeserialized & object, +export interface LatestProps extends PresenceStateOptions { + initialValue: JsonSerializable & JsonDeserialized & object; } - /** * Factory for creating a {@link Latest} State object. * @@ -217,7 +214,7 @@ T extends object, export function latest( props: LatestProps, ): InternalTypes.ManagerFactory, Latest> { - const {controls, initialValue, validator} = props; + const { controls, initialValue, validator } = props; // Latest takes ownership of initialValue but makes a shallow // copy for basic protection. From 63d9e5c0902a826435c8fff9d768378d5faedd70 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 14:56:18 -0700 Subject: [PATCH 03/18] object prop updates --- packages/framework/presence/src/latestMapValueManager.ts | 4 ++-- packages/framework/presence/src/latestValueManager.ts | 4 ++-- packages/framework/presence/src/latestValueTypes.ts | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 2cd1e53363b7..2fb0efc9f8f1 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -516,7 +516,7 @@ class LatestMapValueManagerImpl< export interface LatestMapProps< T extends object, Keys extends string | number = string | number, -> extends PresenceStateOptions { +> extends PresenceStateOptions { initialValues?: { [K in Keys]: JsonSerializable & JsonDeserialized; }; @@ -538,7 +538,7 @@ export function latestMap< InternalTypes.MapValueState, LatestMap > { - const { controls, initialValues, validator } = props; + const { controls, initialValues } = props; const timestamp = Date.now(); const value: InternalTypes.MapValueState< diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 8a0cb49a95f1..90df0380a0a0 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -202,7 +202,7 @@ class LatestValueManagerImpl /** * */ -export interface LatestProps extends PresenceStateOptions { +export interface LatestProps extends PresenceStateOptions { initialValue: JsonSerializable & JsonDeserialized & object; } @@ -214,7 +214,7 @@ export interface LatestProps extends PresenceStateOptions { export function latest( props: LatestProps, ): InternalTypes.ManagerFactory, Latest> { - const { controls, initialValue, validator } = props; + const { controls, initialValue } = props; // Latest takes ownership of initialValue but makes a shallow // copy for basic protection. diff --git a/packages/framework/presence/src/latestValueTypes.ts b/packages/framework/presence/src/latestValueTypes.ts index d8ac22bd5341..aacf0d803f40 100644 --- a/packages/framework/presence/src/latestValueTypes.ts +++ b/packages/framework/presence/src/latestValueTypes.ts @@ -86,7 +86,6 @@ export function isStateSchemaValidator(fn: unknown): fn is StateSchemaValidat * * @alpha */ -export interface PresenceStateOptions { - validator?: StateSchemaValidator | undefined; +export interface PresenceStateOptions { controls?: BroadcastControlSettings | undefined; } From 9a677397bcacbccc29ceaca6d476a060886df549 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 15:26:07 -0700 Subject: [PATCH 04/18] updates --- packages/framework/presence/src/index.ts | 2 + .../presence/src/latestMapValueManager.ts | 4 +- .../presence/src/latestValueManager.ts | 15 +--- .../presence/src/test/batching.spec.ts | 88 +++++++++---------- .../presence/src/test/eventing.spec.ts | 12 ++- .../src/test/latestMapValueManager.spec.ts | 21 ++--- .../src/test/latestValueManager.spec.ts | 27 +++--- .../presence/src/test/presenceStates.spec.ts | 2 +- 8 files changed, 85 insertions(+), 86 deletions(-) diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 2f7ab9de35ce..337c179ba929 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -52,11 +52,13 @@ export type { LatestMapEvents, LatestMapItemRemovedClientData, LatestMapItemUpdatedClientData, + LatestMapProps, StateMap, } from "./latestMapValueManager.js"; export type { latest, Latest, + LatestProps, LatestEvents, } from "./latestValueManager.js"; export type { diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 2fb0efc9f8f1..28daaea42498 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -511,7 +511,9 @@ class LatestMapValueManagerImpl< } /** - * Props passed to the {@link latestMap} function to + * Props passed to the {@link latestMap} function. + * + * @alpha */ export interface LatestMapProps< T extends object, diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 90df0380a0a0..207004e74184 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -200,7 +200,7 @@ class LatestValueManagerImpl } /** - * + * @alpha */ export interface LatestProps extends PresenceStateOptions { initialValue: JsonSerializable & JsonDeserialized & object; @@ -233,18 +233,9 @@ export function latest( initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; manager: InternalTypes.StateValue>; } => ({ - initialData: { - value, - allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs, - }, + initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs }, manager: brandIVM, T, InternalTypes.ValueRequiredState>( - new LatestValueManagerImpl( - key, - datastoreFromHandle(datastoreHandle), - value, - validator, - controls, - ), + new LatestValueManagerImpl(key, datastoreFromHandle(datastoreHandle), value, controls), ), }); return Object.assign(factory, { instanceBase: LatestValueManagerImpl }); diff --git a/packages/framework/presence/src/test/batching.spec.ts b/packages/framework/presence/src/test/batching.spec.ts index 45dcf36f8af9..71e9fe2efe73 100644 --- a/packages/framework/presence/src/test/batching.spec.ts +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -12,13 +12,7 @@ import { Notifications, StateFactory } from "../index.js"; import type { createPresenceManager } from "../presenceManager.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; -import { - assertFinalExpectations, - createNullValidator, - prepareConnectedPresence, -} from "./testUtils.js"; - -const validator = createNullValidator(); +import { assertFinalExpectations, prepareConnectedPresence } from "./testUtils.js"; describe("Presence", () => { describe("batching", () => { @@ -150,10 +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 }, - { validator, controls: { allowableUpdateLatencyMs: 0 } }, - ), + count: StateFactory.latest({ + initialValue: { num: 0 }, + controls: { allowableUpdateLatencyMs: 0 }, + }), }); const { count } = stateWorkspace.props; @@ -202,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 @@ -276,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; @@ -378,10 +376,10 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest( - { num: 0 }, - { controls: { allowableUpdateLatencyMs: 100 } }, - ), + count: StateFactory.latest({ + initialValue: { num: 0 }, + controls: { allowableUpdateLatencyMs: 100 }, + }), }); const { count } = stateWorkspace.props; @@ -500,14 +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 }, - { controls: { allowableUpdateLatencyMs: 100 } }, - ), - immediateUpdate: StateFactory.latest( - { num: 0 }, - { controls: { 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; @@ -593,14 +591,14 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest( - { num: 0 }, - { controls: { allowableUpdateLatencyMs: 100 } }, - ), - note: StateFactory.latest( - { message: "" }, - { controls: { 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; @@ -671,17 +669,17 @@ describe("Presence", () => { // Configure two state workspaces const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest( - { num: 0 }, - { controls: { 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: "" }, - { controls: { allowableUpdateLatencyMs: 60 } }, - ), + note: StateFactory.latest({ + initialValue: { message: "" }, + controls: { allowableUpdateLatencyMs: 60 }, + }), }); // will be queued, deadline is 1070 const { count } = stateWorkspace.props; @@ -870,10 +868,10 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest( - { num: 0 }, - { controls: { 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 diff --git a/packages/framework/presence/src/test/eventing.spec.ts b/packages/framework/presence/src/test/eventing.spec.ts index 5e1a283c6d19..42f69793b1f9 100644 --- a/packages/framework/presence/src/test/eventing.spec.ts +++ b/packages/framework/presence/src/test/eventing.spec.ts @@ -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; @@ -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; diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index f43ed7c6675a..09ed31238fa7 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -9,7 +9,6 @@ import { createPresenceManager } from "../presenceManager.js"; import { addControlsTests } from "./broadcastControlsTests.js"; import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; -import { createNullValidator } from "./testUtils.js"; import type { BroadcastControlSettings, @@ -27,10 +26,10 @@ function createLatestMapManager( valueControlSettings?: BroadcastControlSettings, ) { const states = presence.states.getWorkspace(testWorkspaceName, { - fixedMap: StateFactory.latestMap( - { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, - { validator: createNullValidator(), controls: valueControlSettings }, - ), + fixedMap: StateFactory.latestMap({ + initialValues: { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, + controls: valueControlSettings, + }), }); return states.props.fixedMap; } @@ -53,7 +52,7 @@ describe("Presence", () => { > { const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { - fixedMap: StateFactory.latestMap({ key1: { x: 0, y: 0 } }), + fixedMap: StateFactory.latestMap({ initialValues: { key1: { x: 0, y: 0 } } }), }); return states.props.fixedMap; } @@ -92,7 +91,7 @@ describe("Presence", () => { 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 } }), + fixedMap: StateFactory.latestMap({ initialValues: { key1: { x: 0, y: 0 } } }), }); assert.strictEqual(states.props.fixedMap.presence, presence); @@ -112,8 +111,10 @@ export function checkCompiles(): void { "name:testStatesWorkspaceWithLatestMap", { fixedMap: StateFactory.latestMap({ - key1: { x: 0, y: 0 }, - key2: { ref: "default", someId: 0 }, + initialValues: { + key1: { x: 0, y: 0 }, + key2: { ref: "default", someId: 0 }, + }, }), }, ); @@ -146,7 +147,7 @@ export function checkCompiles(): void { tilt?: number; } - workspace.add("pointers", StateFactory.latestMap({})); + workspace.add("pointers", StateFactory.latestMap({ initialValues: {} })); const pointers = workspace.props.pointers; const localPointers = pointers.local; diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index e608b8908c9a..e86592f342ad 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -25,7 +25,10 @@ function createLatestManager( valueControlSettings?: BroadcastControlSettings, ) { const states = presence.states.getWorkspace(testWorkspaceName, { - camera: StateFactory.latest({ x: 0, y: 0, z: 0 }, { controls: valueControlSettings }), + camera: StateFactory.latest({ + initialValue: { x: 0, y: 0, z: 0 }, + controls: valueControlSettings, + }), }); return states.props.camera; } @@ -46,35 +49,35 @@ describe("Presence", () => { it("can set and get empty object as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - obj: StateFactory.latest({}), + obj: StateFactory.latest({ initialValue: {} }), }); assert.deepStrictEqual(states.props.obj.local, {}); }); it("can set and get object with properties as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - obj: StateFactory.latest({ x: 0, y: 0, z: 0 }), + obj: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), }); assert.deepStrictEqual(states.props.obj.local, { x: 0, y: 0, z: 0 }); }); it("can set and get empty array as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - arr: StateFactory.latest([]), + arr: StateFactory.latest({ initialValue: [] }), }); assert.deepStrictEqual(states.props.arr.local, []); }); it("can set and get array with elements as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - arr: StateFactory.latest([1, 2, 3]), + arr: StateFactory.latest({ initialValue: [1, 2, 3] }), }); 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 }), + camera: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), }); assert.strictEqual(states.props.camera.presence, presence); @@ -87,7 +90,7 @@ describe("Presence", () => { // Setup const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { - camera: StateFactory.latest({ x: 0, y: 0, z: 0 }), + camera: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), }); const camera = states.props.camera; @@ -113,14 +116,14 @@ export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const presence = {} as Presence; const statesWorkspace = presence.states.getWorkspace("name:testStatesWorkspaceWithLatest", { - cursor: StateFactory.latest({ x: 0, y: 0 }), - camera: StateFactory.latest({ x: 0, y: 0, z: 0 }), + cursor: StateFactory.latest({ initialValue: { x: 0, y: 0 } }), + camera: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), }); // Workaround ts(2775): Assertions require every name in the call target to be declared with an explicit type annotation. const workspace: typeof statesWorkspace = statesWorkspace; const props = workspace.props; - workspace.add("caret", StateFactory.latest({ id: "", pos: 0 })); + workspace.add("caret", StateFactory.latest({ initialValue: { id: "", pos: 0 } })); const fakeAdd = workspace.props.caret.local.pos + props.camera.local.z + props.cursor.local.x; @@ -143,9 +146,7 @@ export function checkCompiles(): void { // Listen to others cursor updates const cursorUpdatedOff = cursor.events.on("remoteUpdated", ({ attendee, value }) => - console.log( - `attendee ${attendee.attendeeId}'s cursor is now at (${value?.x},${value?.y})`, - ), + console.log(`attendee ${attendee.attendeeId}'s cursor is now at (${value.x},${value.y})`), ); cursorUpdatedOff(); diff --git a/packages/framework/presence/src/test/presenceStates.spec.ts b/packages/framework/presence/src/test/presenceStates.spec.ts index 862fe4ec738b..8570efe3fe3b 100644 --- a/packages/framework/presence/src/test/presenceStates.spec.ts +++ b/packages/framework/presence/src/test/presenceStates.spec.ts @@ -35,7 +35,7 @@ describe("Presence", () => { it(".presence provides Presence it was created under", () => { const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { - obj: StateFactory.latest({}), + obj: StateFactory.latest({ initialValue: {} }), }); assert.strictEqual(states.presence, presence); }); From 0ecefe0e5dca0ce1b685013742485a99489aaba7 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 15:26:51 -0700 Subject: [PATCH 05/18] remove validator changes --- .../presence/api-report/presence.alpha.api.md | 36 +-- .../presence/src/exposedInternalTypes.ts | 8 - packages/framework/presence/src/index.ts | 2 - .../presence/src/latestMapValueManager.ts | 33 +-- .../presence/src/latestValueManager.ts | 18 +- .../presence/src/latestValueTypes.ts | 35 +-- .../src/test/schemaValidation.spec.ts | 230 ------------------ .../framework/presence/src/test/testUtils.ts | 40 +-- 8 files changed, 31 insertions(+), 371 deletions(-) delete mode 100644 packages/framework/presence/src/test/schemaValidation.spec.ts diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 9c3cab038861..6fd821b522cc 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -105,13 +105,11 @@ export namespace InternalTypes { export type ValueDirectoryOrState = ValueRequiredState | ValueDirectory; // (undocumented) export interface ValueOptionalState extends ValueStateMetadata { - valid?: TValue | undefined; // (undocumented) value?: JsonDeserialized; } // (undocumented) export interface ValueRequiredState extends ValueStateMetadata { - valid?: TValue | undefined; // (undocumented) value: JsonDeserialized; } @@ -150,7 +148,7 @@ export interface Latest { } // @alpha -export function latest(initialValue: JsonSerializable & JsonDeserialized & object, options?: PresenceStateOptions): InternalTypes.ManagerFactory, Latest>; +export function latest(props: LatestProps): InternalTypes.ManagerFactory, Latest>; // @alpha @sealed export interface LatestClientData extends LatestData { @@ -163,7 +161,7 @@ export interface LatestData { // (undocumented) metadata: LatestMetadata; // (undocumented) - value: InternalUtilityTypes.FullyReadonly> | undefined; + value: InternalUtilityTypes.FullyReadonly>; } // @alpha @sealed (undocumented) @@ -188,9 +186,7 @@ export interface LatestMap { } // @alpha -export function latestMap(initialValues?: { - [K in Keys]: JsonSerializable & JsonDeserialized; -}, options?: PresenceStateOptions | undefined): InternalTypes.ManagerFactory, LatestMap>; +export function latestMap(props: LatestMapProps): InternalTypes.ManagerFactory, LatestMap>; // @alpha @sealed export interface LatestMapClientData { @@ -234,12 +230,26 @@ export interface LatestMapItemUpdatedClientData ex key: K; } +// @alpha +export interface LatestMapProps extends PresenceStateOptions { + // (undocumented) + initialValues?: { + [K in Keys]: JsonSerializable & JsonDeserialized; + }; +} + // @alpha @sealed export interface LatestMetadata { revision: number; timestamp: number; } +// @alpha (undocumented) +export interface LatestProps extends PresenceStateOptions { + // (undocumented) + initialValue: JsonSerializable & JsonDeserialized & object; +} + // @alpha @sealed export interface NotificationEmitter> { broadcast>(notificationName: K, ...args: Parameters): void; @@ -313,11 +323,9 @@ export interface PresenceEvents { } // @alpha -export interface PresenceStateOptions { +export interface PresenceStateOptions { // (undocumented) controls?: BroadcastControlSettings | undefined; - // (undocumented) - validator?: StateSchemaValidator | undefined; } // @alpha @@ -341,14 +349,6 @@ export interface StateMap { readonly size: number; } -// @alpha -export type StateSchemaValidator = (unvalidatedData: unknown, metadata?: StateSchemaValidatorMetadata) => T | undefined; - -// @alpha -export interface StateSchemaValidatorMetadata { - key?: string | number; -} - // @alpha @sealed export interface StatesWorkspace { add, TManager extends TManagerConstraints>(key: TKey, manager: InternalTypes.ManagerFactory): asserts this is StatesWorkspace>, TManagerConstraints>; diff --git a/packages/framework/presence/src/exposedInternalTypes.ts b/packages/framework/presence/src/exposedInternalTypes.ts index abfdfc4dd85c..6d86d9b2f71f 100644 --- a/packages/framework/presence/src/exposedInternalTypes.ts +++ b/packages/framework/presence/src/exposedInternalTypes.ts @@ -30,10 +30,6 @@ export namespace InternalTypes { */ export interface ValueOptionalState extends ValueStateMetadata { value?: JsonDeserialized; - /** - * Contains the validated data, or `undefined` if the value has not been validated. - */ - valid?: TValue | undefined; } /** @@ -41,10 +37,6 @@ export namespace InternalTypes { */ export interface ValueRequiredState extends ValueStateMetadata { value: JsonDeserialized; - /** - * Contains the validated data, or `undefined` if the value has not been validated. - */ - valid?: TValue | undefined; } /** diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 337c179ba929..5bb3a67f26be 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -65,8 +65,6 @@ export type { LatestClientData, LatestData, LatestMetadata, - StateSchemaValidator, - StateSchemaValidatorMetadata, PresenceStateOptions, } from "./latestValueTypes.js"; diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 28daaea42498..a4a97b33bb6e 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -21,7 +21,6 @@ import type { LatestClientData, LatestData, LatestMetadata, - StateSchemaValidator, PresenceStateOptions, } from "./latestValueTypes.js"; import type { AttendeeId, Attendee, Presence, SpecificAttendee } from "./presence.js"; @@ -224,7 +223,6 @@ class ValueMapImpl implements StateMap { string | number >, ) => void, - private readonly validator: StateSchemaValidator | undefined, ) { // All initial items are expected to be defined. // TODO assert all defined and/or update type. @@ -271,7 +269,6 @@ class ValueMapImpl implements StateMap { ) => void, thisArg?: unknown, ): void { - // TODO: This is a data read, so we need to validate. for (const [key, item] of objectEntries(this.value.items)) { if (item.value !== undefined) { callbackfn(item.value, key, this); @@ -279,13 +276,7 @@ class ValueMapImpl implements StateMap { } } public get(key: K): InternalUtilityTypes.FullyReadonly> | undefined { - const data = this.value.items[key]?.value; - if (this.validator === undefined) { - return data; - } - const maybeValid = this.validator(data, { key }); - // TODO: Cast shouldn't be necessary. - return maybeValid as InternalUtilityTypes.FullyReadonly> | undefined; + return this.value.items[key]?.value; } public has(key: K): boolean { return this.value.items[key]?.value !== undefined; @@ -376,7 +367,6 @@ class LatestMapValueManagerImpl< InternalTypes.MapValueState >, public readonly value: InternalTypes.MapValueState, - validator: StateSchemaValidator | undefined, controlSettings: BroadcastControlSettings | undefined, ) { this.controls = new OptionalBroadcastControl(controlSettings); @@ -389,7 +379,6 @@ class LatestMapValueManagerImpl< allowableUpdateLatencyMs: this.controls.allowableUpdateLatencyMs, }); }, - validator, ); } @@ -426,14 +415,12 @@ class LatestMapValueManagerImpl< } const items = new Map>(); for (const [key, item] of objectEntries(clientStateMap.items)) { - if (item.value !== undefined) { - const value = item.value; - if (value !== undefined) { - items.set(key, { - value, - metadata: { revision: item.rev, timestamp: item.timestamp }, - }); - } + const value = item.value; + if (value !== undefined) { + items.set(key, { + value, + metadata: { revision: item.rev, timestamp: item.timestamp }, + }); } } return items; @@ -480,10 +467,7 @@ class LatestMapValueManagerImpl< const item = value.items[key]!; const hadPriorValue = currentState.items[key]?.value; currentState.items[key] = item; - const metadata = { - revision: item.rev, - timestamp: item.timestamp, - }; + const metadata = { revision: item.rev, timestamp: item.timestamp }; if (item.value !== undefined) { const itemValue = item.value; const updatedItem = { @@ -578,7 +562,6 @@ export function latestMap< key, datastoreFromHandle(datastoreHandle), value, - validator, controls, ), ), diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 207004e74184..b74efb130f30 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -20,7 +20,6 @@ import { objectEntries } from "./internalUtils.js"; import type { LatestClientData, LatestData, - StateSchemaValidator, PresenceStateOptions, } from "./latestValueTypes.js"; import type { Attendee, Presence } from "./presence.js"; @@ -107,7 +106,6 @@ class LatestValueManagerImpl private readonly key: Key, private readonly datastore: StateDatastore>, public readonly value: InternalTypes.ValueRequiredState, - private readonly validator: StateSchemaValidator | undefined, controlSettings: BroadcastControlSettings | undefined, ) { this.controls = new OptionalBroadcastControl(controlSettings); @@ -136,14 +134,9 @@ class LatestValueManagerImpl const allKnownStates = this.datastore.knownValues(this.key); for (const [attendeeId, value] of objectEntries(allKnownStates.states)) { if (attendeeId !== allKnownStates.self) { - if (value.valid === true && this.validator !== undefined) { - const validData = this.validator(value.value); - value.valid = validData; - } - yield { attendee: this.datastore.lookupClient(attendeeId), - value: value.valid === undefined ? undefined : value.value, + value: value.value, metadata: { revision: value.rev, timestamp: value.timestamp }, }; } @@ -163,15 +156,8 @@ class LatestValueManagerImpl if (clientState === undefined) { throw new Error("No entry for clientId"); } - - // If - if (clientState.valid !== true && this.validator !== undefined) { - const validData = this.validator(clientState); - clientState.valid = validData; - } - return { - value: clientState.valid === undefined ? undefined : clientState.value, + value: clientState.value, metadata: { revision: clientState.rev, timestamp: Date.now() }, }; } diff --git a/packages/framework/presence/src/latestValueTypes.ts b/packages/framework/presence/src/latestValueTypes.ts index aacf0d803f40..d35f703a14b9 100644 --- a/packages/framework/presence/src/latestValueTypes.ts +++ b/packages/framework/presence/src/latestValueTypes.ts @@ -34,7 +34,7 @@ export interface LatestMetadata { * @alpha */ export interface LatestData { - value: InternalUtilityTypes.FullyReadonly> | undefined; + value: InternalUtilityTypes.FullyReadonly>; metadata: LatestMetadata; } @@ -48,39 +48,6 @@ export interface LatestClientData extends LatestData { attendee: Attendee; } -/** - * A validator function that can optionally be provided to do runtime validation of the custom data stored in a - * presence workspace and managed by a value manager. - * - * @alpha - */ -export type StateSchemaValidator = ( - unvalidatedData: unknown, - metadata?: StateSchemaValidatorMetadata, -) => T | undefined; - -/** - * Optional metadata that is passed to a {@link StateSchemaValidator}. - * - * @alpha - * - * TODO: What else needs to be in the metadata? - */ -export interface StateSchemaValidatorMetadata { - /** - * If the value being validated is a LatestValueMap value, this will be set to the value of the corresponding key. - */ - key?: string | number; -} - -/** - * Type guard that checks if a value is a state schema validator. - * @param fn - A function that may be a schema validator. - */ -export function isStateSchemaValidator(fn: unknown): fn is StateSchemaValidator { - return typeof fn === "function"; -} - /** * Options that can be provided to a Presence state manager. TODO: Add details. * diff --git a/packages/framework/presence/src/test/schemaValidation.spec.ts b/packages/framework/presence/src/test/schemaValidation.spec.ts deleted file mode 100644 index 896a9415aa2a..000000000000 --- a/packages/framework/presence/src/test/schemaValidation.spec.ts +++ /dev/null @@ -1,230 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import { strict as assert } from "node:assert"; - -import { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal"; -import { describe, it, after, afterEach, before, beforeEach } from "mocha"; -import { useFakeTimers, type SinonFakeTimers } from "sinon"; - -import { - StateFactory, - // type PresenceStates, - type StateSchemaValidator, - // SessionClientStatus, - // type ClientConnectionId, - // type ISessionClient, -} from "../index.js"; -import type { createPresenceManager } from "../presenceManager.js"; - -import { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; -import { - assertFinalExpectations, - createNullValidator, - createSpiedValidator, - // generateBasicClientJoin, - prepareConnectedPresence, - type ValidatorSpy, -} from "./testUtils.js"; - -describe("Presence", () => { - let runtime: MockEphemeralRuntime; - let logger: EventAndErrorTrackingLogger; - const initialTime = 1000; - let clock: SinonFakeTimers; - - before(async () => { - clock = useFakeTimers(); - }); - - beforeEach(() => { - logger = new EventAndErrorTrackingLogger(); - runtime = new MockEphemeralRuntime(logger); - clock.setSystemTime(initialTime); - }); - - afterEach(function (done: Mocha.Done) { - clock.reset(); - - // If the test passed so far, check final expectations. - if (this.currentTest?.state === "passed") { - assertFinalExpectations(runtime, logger); - } - done(); - }); - - after(() => { - clock.restore(); - }); - - describe("schema validation", () => { - let presence: ReturnType; - const afterCleanUp: (() => void)[] = []; - - beforeEach(() => { - presence = prepareConnectedPresence(runtime, "sessionId-2", "client2", clock, logger); - }); - - afterEach(() => { - for (const cleanUp of afterCleanUp) { - cleanUp(); - } - afterCleanUp.length = 0; - }); - - describe("LatestValueManager", () => { - // let stateWorkspace: PresenceStates<{ num: 0 }>; - let validatorFunction: StateSchemaValidator<{ num: number }>; - let validatorSpy: ValidatorSpy; - - beforeEach(() => { - // Ignore submitted signals - runtime.submitSignal = () => {}; - - [validatorFunction, validatorSpy] = createSpiedValidator<{ num: number }>( - createNullValidator(), - ); - - assert.equal(validatorSpy.callCount, 0); - }); - - it("validator is called when data is read", () => { - // Setup - // Configure a state workspace - const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest( - { num: 0 }, - { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, - ), - }); - - const { count } = stateWorkspace.props; - - // Act & Verify - count.local = { num: 84 }; - - const value = count.local; - - // Reading the data should cause the validator to get called once. - assert.equal(validatorSpy.callCount, 1); - assert.equal(value.num, 84); - }); - - // TODO: test is failing - it.skip("validator is not called multiple times for the same data", () => { - // Setup - // Configure a state workspace - const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest( - { num: 0 }, - { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, - ), - }); - - const { count } = stateWorkspace.props; - count.local = { num: 84 }; - - // Act & Verify - // Reading the data should cause the validator to get called once. - let value = count.getRemote(presence.attendees.getMyself()); - - // Subsequent reads should not call the validator when there is no new data. - value = count.getRemote(presence.attendees.getMyself()); - value = count.getRemote(presence.attendees.getMyself()); - assert.equal(validatorSpy.callCount, 1); - assert.equal(value.value?.num, 84); - }); - - // TODO: test is failing - it("throws on invalid data", () => { - // Setup - // Configure a state workspace - const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest( - { num: 0 }, - { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, - ), - }); - - const { count } = stateWorkspace.props; - count.local = 84 as unknown as { num: number }; - - // Act & Verify - // Reading the data should cause the validator to get called once. - let value = count.getRemote(presence.attendees.getMyself()); - - // Subsequent reads should not call the validator when there is no new data. - value = count.getRemote(presence.attendees.getMyself()); - value = count.getRemote(presence.attendees.getMyself()); - assert.equal(value.value?.num, 84); - assert.equal(validatorSpy.callCount, 1); - }); - }); - - // TODO: tests are failing - describe.skip("LatestMapValueManager", () => { - // let stateWorkspace: PresenceStates<{ num: 0 }>; - let validatorFunction: StateSchemaValidator<{ num: number }>; - let validatorSpy: ValidatorSpy; - - beforeEach(() => { - // Ignore submitted signals - runtime.submitSignal = () => {}; - - [validatorFunction, validatorSpy] = createSpiedValidator<{ num: number }>( - createNullValidator(), - ); - - assert.equal(validatorSpy.callCount, 0); - }); - - it("validator is called when data is read", () => { - // Setup - // Configure a state workspace - const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latestMap( - { "key1": { num: 0 } }, - { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, - ), - }); - - const { count } = stateWorkspace.props; - - // Act & Verify - count.local.set("key1", { num: 84 }); - - const value = count.getRemote(presence.attendees.getMyself()); - - // Reading the data should cause the validator to get called once. - assert.equal(validatorSpy.callCount, 1); - assert.equal(value.get("key1")?.value?.num, 84); - }); - - it("validator is not called multiple times for the same data", () => { - // Setup - // Configure a state workspace - const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latestMap( - { "key1": { num: 0 } }, - { validator: validatorFunction, controls: { allowableUpdateLatencyMs: 0 } }, - ), - }); - - const { count } = stateWorkspace.props; - count.local.set("key1", { num: 84 }); - - // Act & Verify - // Reading the data should cause the validator to get called once. - let value = count.getRemote(presence.attendees.getMyself()); - - // Subsequent reads should not call the validator when there is no new data. - value = count.getRemote(presence.attendees.getMyself()); - value = count.getRemote(presence.attendees.getMyself()); - assert.equal(validatorSpy.callCount, 1); - assert.equal(value.get("key1")?.value?.num, 84); - }); - }); - }); -}); diff --git a/packages/framework/presence/src/test/testUtils.ts b/packages/framework/presence/src/test/testUtils.ts index f3dd95c599cf..ec1112bcb838 100644 --- a/packages/framework/presence/src/test/testUtils.ts +++ b/packages/framework/presence/src/test/testUtils.ts @@ -6,17 +6,13 @@ import type { InternalUtilityTypes } from "@fluidframework/core-interfaces/internal"; import type { EventAndErrorTrackingLogger } from "@fluidframework/test-utils/internal"; import { getUnexpectedLogErrorException } from "@fluidframework/test-utils/internal"; -import type { SinonFakeTimers, SinonSpy } from "sinon"; +import type { SinonFakeTimers } from "sinon"; import { createPresenceManager } from "../presenceManager.js"; import type { MockEphemeralRuntime } from "./mockEphemeralRuntime.js"; -import type { - ClientConnectionId, - AttendeeId, - StateSchemaValidator, -} from "@fluidframework/presence/alpha"; +import type { ClientConnectionId, AttendeeId } from "@fluidframework/presence/alpha"; import type { IExtensionMessage } from "@fluidframework/presence/internal/container-definitions/internal"; /** @@ -157,35 +153,3 @@ export function assertFinalExpectations( // Make sure all expected signals were sent. runtime.assertAllSignalsSubmitted(); } - -/** - * Creates a null validator (one that does nothing) for a given type T. - */ -export function createNullValidator(): StateSchemaValidator { - const nullValidator: StateSchemaValidator = (data: unknown) => { - return data as T; - }; - return nullValidator; -} - -/** - * A validator function spy. - */ -export type ValidatorSpy = Pick; - -/** - * Creates a validator and a spy for test purposes. - */ -export function createSpiedValidator( - validator: StateSchemaValidator, -): [StateSchemaValidator, ValidatorSpy] { - const spy: ValidatorSpy = { - callCount: 0, - }; - - const nullValidatorSpy: StateSchemaValidator = (data: unknown) => { - spy.callCount++; - return validator(data) as T; - }; - return [nullValidatorSpy, spy]; -} From 269725f51e23b84b0777a8e934c647d23e285b34 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 15:30:05 -0700 Subject: [PATCH 06/18] tracker --- examples/apps/presence-tracker/src/FocusTracker.ts | 4 +++- examples/apps/presence-tracker/src/MouseTracker.ts | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/apps/presence-tracker/src/FocusTracker.ts b/examples/apps/presence-tracker/src/FocusTracker.ts index 17127b391fcb..31a6141f4dfb 100644 --- a/examples/apps/presence-tracker/src/FocusTracker.ts +++ b/examples/apps/presence-tracker/src/FocusTracker.ts @@ -57,7 +57,9 @@ export class FocusTracker extends TypedEventEmitter { // window. statesWorkspace.add( "focus", - StateFactory.latest({ hasFocus: window.document.hasFocus() }), + StateFactory.latest({ + initialValue: { hasFocus: window.document.hasFocus() }, + }), ); // Save a reference to the focus state for easy access within the FocusTracker. diff --git a/examples/apps/presence-tracker/src/MouseTracker.ts b/examples/apps/presence-tracker/src/MouseTracker.ts index 251d7dd6c571..3e732bc55355 100644 --- a/examples/apps/presence-tracker/src/MouseTracker.ts +++ b/examples/apps/presence-tracker/src/MouseTracker.ts @@ -55,7 +55,10 @@ export class MouseTracker extends TypedEventEmitter { super(); // Create a Latest state object to track the mouse position. - statesWorkspace.add("cursor", StateFactory.latest({ x: 0, y: 0 })); + statesWorkspace.add( + "cursor", + StateFactory.latest({ initialValue: { x: 0, y: 0 } }), + ); // Save a reference to the cursor state for easy access within the MouseTracker. this.cursor = statesWorkspace.props.cursor; From 47f660c069cd89af1609615d932f39abbc79a3e6 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 15:53:37 -0700 Subject: [PATCH 07/18] fix optionality of args --- examples/apps/ai-collab/src/app/presence.ts | 2 +- .../azure-client/external-controller/src/presence.ts | 2 +- packages/framework/presence/api-report/presence.alpha.api.md | 2 +- packages/framework/presence/src/latestMapValueManager.ts | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/apps/ai-collab/src/app/presence.ts b/examples/apps/ai-collab/src/app/presence.ts index 0f27f7b4c631..c63591351998 100644 --- a/examples/apps/ai-collab/src/app/presence.ts +++ b/examples/apps/ai-collab/src/app/presence.ts @@ -18,7 +18,7 @@ export interface User { } const statesSchema = { - onlineUsers: StateFactory.latest({ photo: "" } satisfies User), + onlineUsers: StateFactory.latest({ initialValue: { photo: "" } satisfies User }), } satisfies StatesWorkspaceSchema; export type UserPresence = StatesWorkspace; diff --git a/examples/service-clients/azure-client/external-controller/src/presence.ts b/examples/service-clients/azure-client/external-controller/src/presence.ts index 57cf05dd5cfc..3ab5c5702ba0 100644 --- a/examples/service-clients/azure-client/external-controller/src/presence.ts +++ b/examples/service-clients/azure-client/external-controller/src/presence.ts @@ -36,7 +36,7 @@ export interface DiceValues { */ const statesSchema = { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - lastRoll: StateFactory.latest({} as DiceValues), + lastRoll: StateFactory.latest({ initialValue: {} as DiceValues }), lastDiceRolls: StateFactory.latestMap<{ value: DieValue }, `die${number}`>(), } satisfies StatesWorkspaceSchema; diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 6fd821b522cc..62ea3901e067 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -186,7 +186,7 @@ export interface LatestMap { } // @alpha -export function latestMap(props: LatestMapProps): InternalTypes.ManagerFactory, LatestMap>; +export function latestMap(props?: LatestMapProps): InternalTypes.ManagerFactory, LatestMap>; // @alpha @sealed export interface LatestMapClientData { diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index a4a97b33bb6e..25664fa2afe6 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -518,13 +518,14 @@ export function latestMap< Keys extends string | number = string | number, RegistrationKey extends string = string, >( - props: LatestMapProps, + props?: LatestMapProps, ): InternalTypes.ManagerFactory< RegistrationKey, InternalTypes.MapValueState, LatestMap > { - const { controls, initialValues } = props; + const controls = props?.controls; + const initialValues = props?.initialValues; const timestamp = Date.now(); const value: InternalTypes.MapValueState< From 597bac184a731afb58751d9d6a08e90539bb6fcb Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 16:55:42 -0700 Subject: [PATCH 08/18] remove shared interface and rename initial value to .local --- .../apps/presence-tracker/src/FocusTracker.ts | 2 +- .../apps/presence-tracker/src/MouseTracker.ts | 2 +- packages/framework/presence/src/index.ts | 1 - .../presence/src/latestMapValueManager.ts | 8 +++---- .../presence/src/latestValueManager.ts | 13 +++++------ .../presence/src/latestValueTypes.ts | 10 --------- .../presence/src/test/batching.spec.ts | 22 +++++++++---------- .../presence/src/test/eventing.spec.ts | 8 +++---- .../src/test/latestMapValueManager.spec.ts | 10 ++++----- .../src/test/latestValueManager.spec.ts | 20 ++++++++--------- .../presence/src/test/presenceStates.spec.ts | 2 +- 11 files changed, 42 insertions(+), 56 deletions(-) diff --git a/examples/apps/presence-tracker/src/FocusTracker.ts b/examples/apps/presence-tracker/src/FocusTracker.ts index 31a6141f4dfb..8c6582f6c22b 100644 --- a/examples/apps/presence-tracker/src/FocusTracker.ts +++ b/examples/apps/presence-tracker/src/FocusTracker.ts @@ -58,7 +58,7 @@ export class FocusTracker extends TypedEventEmitter { statesWorkspace.add( "focus", StateFactory.latest({ - initialValue: { hasFocus: window.document.hasFocus() }, + local: { hasFocus: window.document.hasFocus() }, }), ); diff --git a/examples/apps/presence-tracker/src/MouseTracker.ts b/examples/apps/presence-tracker/src/MouseTracker.ts index 3e732bc55355..af445d81024f 100644 --- a/examples/apps/presence-tracker/src/MouseTracker.ts +++ b/examples/apps/presence-tracker/src/MouseTracker.ts @@ -57,7 +57,7 @@ export class MouseTracker extends TypedEventEmitter { // Create a Latest state object to track the mouse position. statesWorkspace.add( "cursor", - StateFactory.latest({ initialValue: { x: 0, y: 0 } }), + StateFactory.latest({ local: { x: 0, y: 0 } }), ); // Save a reference to the cursor state for easy access within the MouseTracker. diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 5bb3a67f26be..653b3021fede 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -65,7 +65,6 @@ export type { LatestClientData, LatestData, LatestMetadata, - PresenceStateOptions, } from "./latestValueTypes.js"; export { diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 25664fa2afe6..b83c9b79595e 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -21,7 +21,6 @@ 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"; @@ -502,10 +501,11 @@ class LatestMapValueManagerImpl< export interface LatestMapProps< T extends object, Keys extends string | number = string | number, -> extends PresenceStateOptions { - initialValues?: { +> { + local?: { [K in Keys]: JsonSerializable & JsonDeserialized; }; + controls?: BroadcastControlSettings | undefined; } /** @@ -525,7 +525,7 @@ export function latestMap< LatestMap > { const controls = props?.controls; - const initialValues = props?.initialValues; + const initialValues = props?.local; const timestamp = Date.now(); const value: InternalTypes.MapValueState< diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index b74efb130f30..5ef7ae601c35 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -17,11 +17,7 @@ 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, - PresenceStateOptions, -} from "./latestValueTypes.js"; +import type { LatestClientData, LatestData } from "./latestValueTypes.js"; import type { Attendee, Presence } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -188,8 +184,9 @@ class LatestValueManagerImpl /** * @alpha */ -export interface LatestProps extends PresenceStateOptions { - initialValue: JsonSerializable & JsonDeserialized & object; +export interface LatestProps { + local: JsonSerializable & JsonDeserialized & object; + controls?: BroadcastControlSettings | undefined; } /** @@ -200,7 +197,7 @@ export interface LatestProps extends PresenceStateOptions { export function latest( props: LatestProps, ): InternalTypes.ManagerFactory, Latest> { - const { controls, initialValue } = props; + const { controls, local: initialValue } = props; // Latest takes ownership of initialValue but makes a shallow // copy for basic protection. diff --git a/packages/framework/presence/src/latestValueTypes.ts b/packages/framework/presence/src/latestValueTypes.ts index d35f703a14b9..0328eb3c0bc1 100644 --- a/packages/framework/presence/src/latestValueTypes.ts +++ b/packages/framework/presence/src/latestValueTypes.ts @@ -5,7 +5,6 @@ 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"; @@ -47,12 +46,3 @@ export interface LatestData { export interface LatestClientData extends LatestData { attendee: Attendee; } - -/** - * Options that can be provided to a Presence state manager. TODO: Add details. - * - * @alpha - */ -export interface PresenceStateOptions { - controls?: BroadcastControlSettings | undefined; -} diff --git a/packages/framework/presence/src/test/batching.spec.ts b/packages/framework/presence/src/test/batching.spec.ts index 71e9fe2efe73..bf0333614192 100644 --- a/packages/framework/presence/src/test/batching.spec.ts +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -145,7 +145,7 @@ describe("Presence", () => { // SIGNAL #1 - intial data is sent immediately const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ - initialValue: { num: 0 }, + local: { num: 0 }, controls: { allowableUpdateLatencyMs: 0 }, }), }); @@ -197,7 +197,7 @@ describe("Presence", () => { // Configure a state workspace presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ - initialValue: { num: 0 } /* default allowableUpdateLatencyMs = 60 */, + local: { num: 0 } /* default allowableUpdateLatencyMs = 60 */, }), }); // will be queued; deadline is now 1070 @@ -273,7 +273,7 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ - initialValue: { num: 0 } /* default allowableUpdateLatencyMs = 60 */, + local: { num: 0 } /* default allowableUpdateLatencyMs = 60 */, }), }); // will be queued; deadline is now 1070 @@ -377,7 +377,7 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ - initialValue: { num: 0 }, + local: { num: 0 }, controls: { allowableUpdateLatencyMs: 100 }, }), }); @@ -499,11 +499,11 @@ describe("Presence", () => { // so the initial data will be sent immediately. const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ - initialValue: { num: 0 }, + local: { num: 0 }, controls: { allowableUpdateLatencyMs: 100 }, }), immediateUpdate: StateFactory.latest({ - initialValue: { num: 0 }, + local: { num: 0 }, controls: { allowableUpdateLatencyMs: 0 }, }), }); @@ -592,11 +592,11 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ - initialValue: { num: 0 }, + local: { num: 0 }, controls: { allowableUpdateLatencyMs: 100 }, }), note: StateFactory.latest({ - initialValue: { message: "" }, + local: { message: "" }, controls: { allowableUpdateLatencyMs: 50 }, }), }); // will be queued, deadline is set to 1060 @@ -670,14 +670,14 @@ describe("Presence", () => { // Configure two state workspaces const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ - initialValue: { num: 0 }, + local: { num: 0 }, controls: { allowableUpdateLatencyMs: 100 }, }), }); // will be queued, deadline is 1110 const stateWorkspace2 = presence.states.getWorkspace("name:testStateWorkspace2", { note: StateFactory.latest({ - initialValue: { message: "" }, + local: { message: "" }, controls: { allowableUpdateLatencyMs: 60 }, }), }); // will be queued, deadline is 1070 @@ -869,7 +869,7 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ - initialValue: { num: 0 }, + local: { num: 0 }, controls: { allowableUpdateLatencyMs: 100 }, }), }); // will be queued, deadline is 1110 diff --git a/packages/framework/presence/src/test/eventing.spec.ts b/packages/framework/presence/src/test/eventing.spec.ts index 42f69793b1f9..47cebc869f5b 100644 --- a/packages/framework/presence/src/test/eventing.spec.ts +++ b/packages/framework/presence/src/test/eventing.spec.ts @@ -242,9 +242,9 @@ describe("Presence", () => { notifications, }: { notifications?: true } = {}): void { const states = presence.states.getWorkspace("name:testWorkspace", { - latest: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), + latest: StateFactory.latest({ local: { x: 0, y: 0, z: 0 } }), latestMap: StateFactory.latestMap({ - initialValues: { key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }, + local: { key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }, }), }); latest = states.props.latest; @@ -263,11 +263,11 @@ describe("Presence", () => { function setupMultipleStatesWorkspaces(): void { const latestsStates = presence.states.getWorkspace("name:testWorkspace1", { - latest: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), + latest: StateFactory.latest({ local: { x: 0, y: 0, z: 0 } }), }); const latesetMapStates = presence.states.getWorkspace("name:testWorkspace2", { latestMap: StateFactory.latestMap({ - initialValues: { key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }, + local: { key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }, }), }); latest = latestsStates.props.latest; diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index 09ed31238fa7..5a7a7f8798af 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -27,7 +27,7 @@ function createLatestMapManager( ) { const states = presence.states.getWorkspace(testWorkspaceName, { fixedMap: StateFactory.latestMap({ - initialValues: { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, + local: { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, controls: valueControlSettings, }), }); @@ -52,7 +52,7 @@ describe("Presence", () => { > { const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { - fixedMap: StateFactory.latestMap({ initialValues: { key1: { x: 0, y: 0 } } }), + fixedMap: StateFactory.latestMap({ local: { key1: { x: 0, y: 0 } } }), }); return states.props.fixedMap; } @@ -91,7 +91,7 @@ describe("Presence", () => { it(".presence provides Presence it was created under", () => { const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { - fixedMap: StateFactory.latestMap({ initialValues: { key1: { x: 0, y: 0 } } }), + fixedMap: StateFactory.latestMap({ local: { key1: { x: 0, y: 0 } } }), }); assert.strictEqual(states.props.fixedMap.presence, presence); @@ -111,7 +111,7 @@ export function checkCompiles(): void { "name:testStatesWorkspaceWithLatestMap", { fixedMap: StateFactory.latestMap({ - initialValues: { + local: { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 }, }, @@ -147,7 +147,7 @@ export function checkCompiles(): void { tilt?: number; } - workspace.add("pointers", StateFactory.latestMap({ initialValues: {} })); + workspace.add("pointers", StateFactory.latestMap({ local: {} })); const pointers = workspace.props.pointers; const localPointers = pointers.local; diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index e86592f342ad..ce34a39f1775 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -26,7 +26,7 @@ function createLatestManager( ) { const states = presence.states.getWorkspace(testWorkspaceName, { camera: StateFactory.latest({ - initialValue: { x: 0, y: 0, z: 0 }, + local: { x: 0, y: 0, z: 0 }, controls: valueControlSettings, }), }); @@ -49,35 +49,35 @@ describe("Presence", () => { it("can set and get empty object as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - obj: StateFactory.latest({ initialValue: {} }), + obj: StateFactory.latest({ local: {} }), }); assert.deepStrictEqual(states.props.obj.local, {}); }); it("can set and get object with properties as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - obj: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), + obj: StateFactory.latest({ local: { x: 0, y: 0, z: 0 } }), }); assert.deepStrictEqual(states.props.obj.local, { x: 0, y: 0, z: 0 }); }); it("can set and get empty array as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - arr: StateFactory.latest({ initialValue: [] }), + arr: StateFactory.latest({ local: [] }), }); assert.deepStrictEqual(states.props.arr.local, []); }); it("can set and get array with elements as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - arr: StateFactory.latest({ initialValue: [1, 2, 3] }), + arr: StateFactory.latest({ local: [1, 2, 3] }), }); 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({ initialValue: { x: 0, y: 0, z: 0 } }), + camera: StateFactory.latest({ local: { x: 0, y: 0, z: 0 } }), }); assert.strictEqual(states.props.camera.presence, presence); @@ -90,7 +90,7 @@ describe("Presence", () => { // Setup const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { - camera: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), + camera: StateFactory.latest({ local: { x: 0, y: 0, z: 0 } }), }); const camera = states.props.camera; @@ -116,14 +116,14 @@ export function checkCompiles(): void { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const presence = {} as Presence; const statesWorkspace = presence.states.getWorkspace("name:testStatesWorkspaceWithLatest", { - cursor: StateFactory.latest({ initialValue: { x: 0, y: 0 } }), - camera: StateFactory.latest({ initialValue: { x: 0, y: 0, z: 0 } }), + cursor: StateFactory.latest({ local: { x: 0, y: 0 } }), + camera: StateFactory.latest({ local: { x: 0, y: 0, z: 0 } }), }); // Workaround ts(2775): Assertions require every name in the call target to be declared with an explicit type annotation. const workspace: typeof statesWorkspace = statesWorkspace; const props = workspace.props; - workspace.add("caret", StateFactory.latest({ initialValue: { id: "", pos: 0 } })); + workspace.add("caret", StateFactory.latest({ local: { id: "", pos: 0 } })); const fakeAdd = workspace.props.caret.local.pos + props.camera.local.z + props.cursor.local.x; diff --git a/packages/framework/presence/src/test/presenceStates.spec.ts b/packages/framework/presence/src/test/presenceStates.spec.ts index 8570efe3fe3b..654fa1cd0a6a 100644 --- a/packages/framework/presence/src/test/presenceStates.spec.ts +++ b/packages/framework/presence/src/test/presenceStates.spec.ts @@ -35,7 +35,7 @@ describe("Presence", () => { it(".presence provides Presence it was created under", () => { const presence = createPresenceManager(new MockEphemeralRuntime()); const states = presence.states.getWorkspace(testWorkspaceName, { - obj: StateFactory.latest({ initialValue: {} }), + obj: StateFactory.latest({ local: {} }), }); assert.strictEqual(states.presence, presence); }); From 660ab31034ad74c6b8b3623c74bd4cd77bcfcfb6 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 16:58:55 -0700 Subject: [PATCH 09/18] build --- .../presence/api-report/presence.alpha.api.md | 18 +++++++----------- packages/framework/presence/src/index.ts | 2 +- .../presence/src/latestMapValueManager.ts | 9 ++++----- .../presence/src/latestValueManager.ts | 9 +++++++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 62ea3901e067..85054c70cbea 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -231,9 +231,10 @@ export interface LatestMapItemUpdatedClientData ex } // @alpha -export interface LatestMapProps extends PresenceStateOptions { +export interface LatestMapProps { // (undocumented) - initialValues?: { + controls?: BroadcastControlSettings | undefined; + local?: { [K in Keys]: JsonSerializable & JsonDeserialized; }; } @@ -244,10 +245,11 @@ export interface LatestMetadata { timestamp: number; } -// @alpha (undocumented) -export interface LatestProps extends PresenceStateOptions { +// @alpha +export interface LatestProps { // (undocumented) - initialValue: JsonSerializable & JsonDeserialized & object; + controls?: BroadcastControlSettings | undefined; + local: JsonSerializable & JsonDeserialized & object; } // @alpha @sealed @@ -322,12 +324,6 @@ 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; diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 653b3021fede..5be0e3903b59 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -58,7 +58,7 @@ export type { export type { latest, Latest, - LatestProps, + LatestArguments as LatestProps, LatestEvents, } from "./latestValueManager.js"; export type { diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index b83c9b79595e..b002e1689244 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -17,11 +17,7 @@ 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 } from "./latestValueTypes.js"; import type { AttendeeId, Attendee, Presence, SpecificAttendee } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; @@ -502,6 +498,9 @@ export interface LatestMapProps< T extends object, Keys extends string | number = string | number, > { + /** + * The initial value of the local state. + */ local?: { [K in Keys]: JsonSerializable & JsonDeserialized; }; diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 5ef7ae601c35..488316d46dcd 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -182,9 +182,14 @@ class LatestValueManagerImpl } /** + * Arguments that are passed to the {@link Latest} function. + * * @alpha */ -export interface LatestProps { +export interface LatestArguments { + /** + * The initial value of the local state. + */ local: JsonSerializable & JsonDeserialized & object; controls?: BroadcastControlSettings | undefined; } @@ -195,7 +200,7 @@ export interface LatestProps { * @alpha */ export function latest( - props: LatestProps, + props: LatestArguments, ): InternalTypes.ManagerFactory, Latest> { const { controls, local: initialValue } = props; From fac52b9c439daf1e8bc8e21fa74eaa193b4d17ef Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 17:01:47 -0700 Subject: [PATCH 10/18] rename args interfaces --- packages/framework/presence/src/index.ts | 2 +- .../framework/presence/src/latestMapValueManager.ts | 10 +++++----- packages/framework/presence/src/latestValueManager.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 5be0e3903b59..3c08cc36ba57 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -52,7 +52,7 @@ export type { LatestMapEvents, LatestMapItemRemovedClientData, LatestMapItemUpdatedClientData, - LatestMapProps, + LatestMapArguments as LatestMapProps, StateMap, } from "./latestMapValueManager.js"; export type { diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index b002e1689244..91b5f999f7ef 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -490,11 +490,11 @@ class LatestMapValueManagerImpl< } /** - * Props passed to the {@link latestMap} function. + * Arguments that are passed to the {@link latestMap} function. * * @alpha */ -export interface LatestMapProps< +export interface LatestMapArguments< T extends object, Keys extends string | number = string | number, > { @@ -517,14 +517,14 @@ export function latestMap< Keys extends string | number = string | number, RegistrationKey extends string = string, >( - props?: LatestMapProps, + args?: LatestMapArguments, ): InternalTypes.ManagerFactory< RegistrationKey, InternalTypes.MapValueState, LatestMap > { - const controls = props?.controls; - const initialValues = props?.local; + const controls = args?.controls; + const initialValues = args?.local; const timestamp = Date.now(); const value: InternalTypes.MapValueState< diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 488316d46dcd..de6e7d38650f 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -200,16 +200,16 @@ export interface LatestArguments { * @alpha */ export function latest( - props: LatestArguments, + args: LatestArguments, ): InternalTypes.ManagerFactory, Latest> { - const { controls, local: initialValue } = props; + const { controls, local } = args; - // Latest takes ownership of initialValue but makes a shallow + // Latest takes ownership of the initial local value but makes a shallow // copy for basic protection. const value: InternalTypes.ValueRequiredState = { rev: 0, timestamp: Date.now(), - value: shallowCloneObject(initialValue), + value: shallowCloneObject(local), }; const factory = ( key: Key, From f21ad912540b178b035c594d40d768259d2e007d Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 17:03:01 -0700 Subject: [PATCH 11/18] rename controls to settings --- .../presence/src/latestMapValueManager.ts | 8 ++++---- .../presence/src/latestValueManager.ts | 8 ++++---- .../presence/src/test/batching.spec.ts | 18 +++++++++--------- .../src/test/latestMapValueManager.spec.ts | 2 +- .../src/test/latestValueManager.spec.ts | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 91b5f999f7ef..18d141589e6e 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -504,7 +504,7 @@ export interface LatestMapArguments< local?: { [K in Keys]: JsonSerializable & JsonDeserialized; }; - controls?: BroadcastControlSettings | undefined; + settings?: BroadcastControlSettings | undefined; } /** @@ -523,7 +523,7 @@ export function latestMap< InternalTypes.MapValueState, LatestMap > { - const controls = args?.controls; + const settings = args?.settings; const initialValues = args?.local; const timestamp = Date.now(); @@ -552,7 +552,7 @@ export function latestMap< initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; manager: InternalTypes.StateValue>; } => ({ - initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs }, + initialData: { value, allowableUpdateLatencyMs: settings?.allowableUpdateLatencyMs }, manager: brandIVM< LatestMapValueManagerImpl, T, @@ -562,7 +562,7 @@ export function latestMap< key, datastoreFromHandle(datastoreHandle), value, - controls, + settings, ), ), }); diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index de6e7d38650f..96331145297c 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -191,7 +191,7 @@ export interface LatestArguments { * The initial value of the local state. */ local: JsonSerializable & JsonDeserialized & object; - controls?: BroadcastControlSettings | undefined; + settings?: BroadcastControlSettings | undefined; } /** @@ -202,7 +202,7 @@ export interface LatestArguments { export function latest( args: LatestArguments, ): InternalTypes.ManagerFactory, Latest> { - const { controls, local } = args; + const { settings, local } = args; // Latest takes ownership of the initial local value but makes a shallow // copy for basic protection. @@ -221,9 +221,9 @@ export function latest( initialData: { value: typeof value; allowableUpdateLatencyMs: number | undefined }; manager: InternalTypes.StateValue>; } => ({ - initialData: { value, allowableUpdateLatencyMs: controls?.allowableUpdateLatencyMs }, + initialData: { value, allowableUpdateLatencyMs: settings?.allowableUpdateLatencyMs }, manager: brandIVM, T, InternalTypes.ValueRequiredState>( - new LatestValueManagerImpl(key, datastoreFromHandle(datastoreHandle), value, controls), + new LatestValueManagerImpl(key, datastoreFromHandle(datastoreHandle), value, settings), ), }); return Object.assign(factory, { instanceBase: LatestValueManagerImpl }); diff --git a/packages/framework/presence/src/test/batching.spec.ts b/packages/framework/presence/src/test/batching.spec.ts index bf0333614192..a0c995c03ef5 100644 --- a/packages/framework/presence/src/test/batching.spec.ts +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -146,7 +146,7 @@ describe("Presence", () => { const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ local: { num: 0 }, - controls: { allowableUpdateLatencyMs: 0 }, + settings: { allowableUpdateLatencyMs: 0 }, }), }); @@ -378,7 +378,7 @@ describe("Presence", () => { const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ local: { num: 0 }, - controls: { allowableUpdateLatencyMs: 100 }, + settings: { allowableUpdateLatencyMs: 100 }, }), }); @@ -500,11 +500,11 @@ describe("Presence", () => { const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ local: { num: 0 }, - controls: { allowableUpdateLatencyMs: 100 }, + settings: { allowableUpdateLatencyMs: 100 }, }), immediateUpdate: StateFactory.latest({ local: { num: 0 }, - controls: { allowableUpdateLatencyMs: 0 }, + settings: { allowableUpdateLatencyMs: 0 }, }), }); @@ -593,11 +593,11 @@ describe("Presence", () => { const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ local: { num: 0 }, - controls: { allowableUpdateLatencyMs: 100 }, + settings: { allowableUpdateLatencyMs: 100 }, }), note: StateFactory.latest({ local: { message: "" }, - controls: { allowableUpdateLatencyMs: 50 }, + settings: { allowableUpdateLatencyMs: 50 }, }), }); // will be queued, deadline is set to 1060 @@ -671,14 +671,14 @@ describe("Presence", () => { const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ local: { num: 0 }, - controls: { allowableUpdateLatencyMs: 100 }, + settings: { allowableUpdateLatencyMs: 100 }, }), }); // will be queued, deadline is 1110 const stateWorkspace2 = presence.states.getWorkspace("name:testStateWorkspace2", { note: StateFactory.latest({ local: { message: "" }, - controls: { allowableUpdateLatencyMs: 60 }, + settings: { allowableUpdateLatencyMs: 60 }, }), }); // will be queued, deadline is 1070 @@ -870,7 +870,7 @@ describe("Presence", () => { const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { count: StateFactory.latest({ local: { num: 0 }, - controls: { allowableUpdateLatencyMs: 100 }, + settings: { allowableUpdateLatencyMs: 100 }, }), }); // will be queued, deadline is 1110 diff --git a/packages/framework/presence/src/test/latestMapValueManager.spec.ts b/packages/framework/presence/src/test/latestMapValueManager.spec.ts index 5a7a7f8798af..0996db777c96 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -28,7 +28,7 @@ function createLatestMapManager( const states = presence.states.getWorkspace(testWorkspaceName, { fixedMap: StateFactory.latestMap({ local: { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, - controls: valueControlSettings, + settings: valueControlSettings, }), }); return states.props.fixedMap; diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index ce34a39f1775..b2dbe3541e78 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -27,7 +27,7 @@ function createLatestManager( const states = presence.states.getWorkspace(testWorkspaceName, { camera: StateFactory.latest({ local: { x: 0, y: 0, z: 0 }, - controls: valueControlSettings, + settings: valueControlSettings, }), }); return states.props.camera; From 701a58ec3e77320d98c642ea27343675ccc8a5f3 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 17:06:26 -0700 Subject: [PATCH 12/18] example fixes --- examples/apps/ai-collab/src/app/presence.ts | 2 +- .../azure-client/external-controller/src/presence.ts | 2 +- .../presence/api-report/presence.alpha.api.md | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/apps/ai-collab/src/app/presence.ts b/examples/apps/ai-collab/src/app/presence.ts index c63591351998..1984c9d51621 100644 --- a/examples/apps/ai-collab/src/app/presence.ts +++ b/examples/apps/ai-collab/src/app/presence.ts @@ -18,7 +18,7 @@ export interface User { } const statesSchema = { - onlineUsers: StateFactory.latest({ initialValue: { photo: "" } satisfies User }), + onlineUsers: StateFactory.latest({ local: { photo: "" } satisfies User }), } satisfies StatesWorkspaceSchema; export type UserPresence = StatesWorkspace; diff --git a/examples/service-clients/azure-client/external-controller/src/presence.ts b/examples/service-clients/azure-client/external-controller/src/presence.ts index 3ab5c5702ba0..0236d0e70f1f 100644 --- a/examples/service-clients/azure-client/external-controller/src/presence.ts +++ b/examples/service-clients/azure-client/external-controller/src/presence.ts @@ -36,7 +36,7 @@ export interface DiceValues { */ const statesSchema = { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - lastRoll: StateFactory.latest({ initialValue: {} as DiceValues }), + lastRoll: StateFactory.latest({ local: {} as DiceValues }), lastDiceRolls: StateFactory.latestMap<{ value: DieValue }, `die${number}`>(), } satisfies StatesWorkspaceSchema; diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 85054c70cbea..7864b7a72edf 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -148,7 +148,7 @@ export interface Latest { } // @alpha -export function latest(props: LatestProps): InternalTypes.ManagerFactory, Latest>; +export function latest(args: LatestProps): InternalTypes.ManagerFactory, Latest>; // @alpha @sealed export interface LatestClientData extends LatestData { @@ -186,7 +186,7 @@ export interface LatestMap { } // @alpha -export function latestMap(props?: LatestMapProps): InternalTypes.ManagerFactory, LatestMap>; +export function latestMap(args?: LatestMapProps): InternalTypes.ManagerFactory, LatestMap>; // @alpha @sealed export interface LatestMapClientData { @@ -232,11 +232,11 @@ export interface LatestMapItemUpdatedClientData ex // @alpha export interface LatestMapProps { - // (undocumented) - controls?: BroadcastControlSettings | undefined; local?: { [K in Keys]: JsonSerializable & JsonDeserialized; }; + // (undocumented) + settings?: BroadcastControlSettings | undefined; } // @alpha @sealed @@ -247,9 +247,9 @@ export interface LatestMetadata { // @alpha export interface LatestProps { - // (undocumented) - controls?: BroadcastControlSettings | undefined; local: JsonSerializable & JsonDeserialized & object; + // (undocumented) + settings?: BroadcastControlSettings | undefined; } // @alpha @sealed From c4c6c593b28da689ed3b3279ebbf85d87e73ba06 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 17:09:43 -0700 Subject: [PATCH 13/18] oops --- .../presence/api-report/presence.alpha.api.md | 36 +++++++++---------- packages/framework/presence/src/index.ts | 4 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 7864b7a72edf..79cd1a566bf8 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -148,7 +148,14 @@ export interface Latest { } // @alpha -export function latest(args: LatestProps): InternalTypes.ManagerFactory, Latest>; +export function latest(args: LatestArguments): InternalTypes.ManagerFactory, Latest>; + +// @alpha +export interface LatestArguments { + local: JsonSerializable & JsonDeserialized & object; + // (undocumented) + settings?: BroadcastControlSettings | undefined; +} // @alpha @sealed export interface LatestClientData extends LatestData { @@ -186,7 +193,16 @@ export interface LatestMap { } // @alpha -export function latestMap(args?: LatestMapProps): InternalTypes.ManagerFactory, LatestMap>; +export function latestMap(args?: LatestMapArguments): InternalTypes.ManagerFactory, LatestMap>; + +// @alpha +export interface LatestMapArguments { + local?: { + [K in Keys]: JsonSerializable & JsonDeserialized; + }; + // (undocumented) + settings?: BroadcastControlSettings | undefined; +} // @alpha @sealed export interface LatestMapClientData { @@ -230,28 +246,12 @@ export interface LatestMapItemUpdatedClientData ex key: K; } -// @alpha -export interface LatestMapProps { - local?: { - [K in Keys]: JsonSerializable & JsonDeserialized; - }; - // (undocumented) - settings?: BroadcastControlSettings | undefined; -} - // @alpha @sealed export interface LatestMetadata { revision: number; timestamp: number; } -// @alpha -export interface LatestProps { - local: JsonSerializable & JsonDeserialized & object; - // (undocumented) - settings?: BroadcastControlSettings | undefined; -} - // @alpha @sealed export interface NotificationEmitter> { broadcast>(notificationName: K, ...args: Parameters): void; diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 3c08cc36ba57..4ecf09b80b45 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -52,13 +52,13 @@ export type { LatestMapEvents, LatestMapItemRemovedClientData, LatestMapItemUpdatedClientData, - LatestMapArguments as LatestMapProps, + LatestMapArguments, StateMap, } from "./latestMapValueManager.js"; export type { latest, Latest, - LatestArguments as LatestProps, + LatestArguments, LatestEvents, } from "./latestValueManager.js"; export type { From 83ca417161c8c287474f2c09dd589b32a0522e91 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 17:36:24 -0700 Subject: [PATCH 14/18] sort --- packages/framework/presence/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 4ecf09b80b45..19f1522b62f1 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -48,11 +48,11 @@ export { export type { latestMap, LatestMap, + LatestMapArguments, LatestMapClientData, LatestMapEvents, LatestMapItemRemovedClientData, LatestMapItemUpdatedClientData, - LatestMapArguments, StateMap, } from "./latestMapValueManager.js"; export type { From a97d189deacec3c46db0d99597157737a136687d Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 17:50:24 -0700 Subject: [PATCH 15/18] docs --- packages/framework/presence/src/latestMapValueManager.ts | 2 +- packages/framework/presence/src/latestValueManager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 18d141589e6e..d8ab30fd27e1 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -490,7 +490,7 @@ class LatestMapValueManagerImpl< } /** - * Arguments that are passed to the {@link latestMap} function. + * Arguments that are passed to the {@link StateFactory.latestMap} function. * * @alpha */ diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 96331145297c..8204382cba98 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -182,7 +182,7 @@ class LatestValueManagerImpl } /** - * Arguments that are passed to the {@link Latest} function. + * Arguments that are passed to the {@link StateFactory.latest} function. * * @alpha */ From 7a381262aaac96264d1b8a142d86616fcd23ef03 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 17:57:06 -0700 Subject: [PATCH 16/18] changeset --- .changeset/eleven-groups-open.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .changeset/eleven-groups-open.md diff --git a/.changeset/eleven-groups-open.md b/.changeset/eleven-groups-open.md new file mode 100644 index 000000000000..2c2a677e2063 --- /dev/null +++ b/.changeset/eleven-groups-open.md @@ -0,0 +1,29 @@ +--- +"@fluidframework/presence": minor +"__section": other +--- +StateFactory.latest/latestMap now takes an object as its only argument + +The `StateFactory.latest` and `StateFactory.latestMap` functions now take a single object argument. To convert existing +code, pass any initial data in the `local` argument, and broadcast settings in the `settings` argument. For example: + +Before: + +```ts + const statesWorkspace = presence.states.getWorkspace("name:workspace", { + cursor: StateFactory.latest( + { x: 0, y: 0 }, + { allowableUpdateLatencyMs: 100 }), + }); +``` + +After: + +```ts + const statesWorkspace = presence.states.getWorkspace("name:workspace", { + cursor: StateFactory.latest({ + local: { x: 0, y: 0 }, + settings: { allowableUpdateLatencyMs: 100 }, + }), + }); +``` From 241222e5bddf21beb4ee5be10bbf47265ddf3066 Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 17:58:20 -0700 Subject: [PATCH 17/18] cleanup --- .changeset/eleven-groups-open.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.changeset/eleven-groups-open.md b/.changeset/eleven-groups-open.md index 2c2a677e2063..28d727506bd4 100644 --- a/.changeset/eleven-groups-open.md +++ b/.changeset/eleven-groups-open.md @@ -10,20 +10,20 @@ code, pass any initial data in the `local` argument, and broadcast settings in t Before: ```ts - const statesWorkspace = presence.states.getWorkspace("name:workspace", { - cursor: StateFactory.latest( - { x: 0, y: 0 }, - { allowableUpdateLatencyMs: 100 }), - }); +const statesWorkspace = presence.states.getWorkspace("name:workspace", { + cursor: StateFactory.latest( + { x: 0, y: 0 }, + { allowableUpdateLatencyMs: 100 }), +}); ``` After: ```ts - const statesWorkspace = presence.states.getWorkspace("name:workspace", { - cursor: StateFactory.latest({ - local: { x: 0, y: 0 }, - settings: { allowableUpdateLatencyMs: 100 }, - }), - }); +const statesWorkspace = presence.states.getWorkspace("name:workspace", { + cursor: StateFactory.latest({ + local: { x: 0, y: 0 }, + settings: { allowableUpdateLatencyMs: 100 }, + }), +}); ``` From 36ee876580a8d25ef8eda743fd161732c284334d Mon Sep 17 00:00:00 2001 From: Tyler Butler Date: Mon, 21 Apr 2025 18:39:06 -0700 Subject: [PATCH 18/18] feedback --- packages/framework/presence/api-report/presence.alpha.api.md | 2 -- packages/framework/presence/src/latestMapValueManager.ts | 4 ++++ packages/framework/presence/src/latestValueManager.ts | 4 ++++ 3 files changed, 8 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 79cd1a566bf8..6c2e3072fb1c 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -153,7 +153,6 @@ export function latest(args: Late // @alpha export interface LatestArguments { local: JsonSerializable & JsonDeserialized & object; - // (undocumented) settings?: BroadcastControlSettings | undefined; } @@ -200,7 +199,6 @@ export interface LatestMapArguments & JsonDeserialized; }; - // (undocumented) settings?: BroadcastControlSettings | undefined; } diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index d8ab30fd27e1..b545808fa9c7 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -504,6 +504,10 @@ export interface LatestMapArguments< local?: { [K in Keys]: JsonSerializable & JsonDeserialized; }; + + /** + * See {@link BroadcastControlSettings}. + */ settings?: BroadcastControlSettings | undefined; } diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 8204382cba98..a2ac5476051f 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -191,6 +191,10 @@ export interface LatestArguments { * The initial value of the local state. */ local: JsonSerializable & JsonDeserialized & object; + + /** + * See {@link BroadcastControlSettings}. + */ settings?: BroadcastControlSettings | undefined; }