diff --git a/.changeset/eleven-groups-open.md b/.changeset/eleven-groups-open.md new file mode 100644 index 000000000000..28d727506bd4 --- /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 }, + }), +}); +``` diff --git a/examples/apps/ai-collab/src/app/presence.ts b/examples/apps/ai-collab/src/app/presence.ts index 0f27f7b4c631..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({ photo: "" } satisfies User), + onlineUsers: StateFactory.latest({ local: { photo: "" } satisfies User }), } satisfies StatesWorkspaceSchema; export type UserPresence = StatesWorkspace; diff --git a/examples/apps/presence-tracker/src/FocusTracker.ts b/examples/apps/presence-tracker/src/FocusTracker.ts index 265bebd510c3..4c2e7bdaa6af 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({ + local: { 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 9cf83b3255d2..2a9744d4585c 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({ local: { x: 0, y: 0 } }), + ); // Save a reference to the cursor state for easy access within the MouseTracker. this.cursor = statesWorkspace.props.cursor; 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 2ca7c87e9025..2b9d8790e28b 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({ 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 1d2775d8ded0..46a3ed21bcbf 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -136,7 +136,13 @@ export namespace InternalUtilityTypes { } // @alpha -export function latest(initialValue: JsonSerializable & JsonDeserialized & (object | null), controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, LatestRaw>; +export function latest(args: LatestArguments): InternalTypes.ManagerFactory, LatestRaw>; + +// @alpha +export interface LatestArguments { + local: JsonSerializable & JsonDeserialized & (object | null); + settings?: BroadcastControlSettings | undefined; +} // @alpha @sealed export interface LatestClientData extends LatestData { @@ -153,9 +159,15 @@ export interface LatestData { } // @alpha -export function latestMap(initialValues?: { - [K in Keys]: JsonSerializable & JsonDeserialized; -}, controls?: BroadcastControlSettings): InternalTypes.ManagerFactory, LatestMapRaw>; +export function latestMap(args?: LatestMapArguments): InternalTypes.ManagerFactory, LatestMapRaw>; + +// @alpha +export interface LatestMapArguments { + local?: { + [K in Keys]: JsonSerializable & JsonDeserialized; + }; + settings?: BroadcastControlSettings | undefined; +} // @alpha @sealed export interface LatestMapClientData { diff --git a/packages/framework/presence/src/index.ts b/packages/framework/presence/src/index.ts index 073faba7eff1..182ad758b1c9 100644 --- a/packages/framework/presence/src/index.ts +++ b/packages/framework/presence/src/index.ts @@ -47,6 +47,7 @@ export { export type { latestMap, + LatestMapArguments, LatestMapRaw, LatestMapClientData, LatestMapRawEvents, @@ -56,6 +57,7 @@ export type { } from "./latestMapValueManager.js"; export type { latest, + LatestArguments, LatestRaw, LatestRawEvents, } from "./latestValueManager.js"; diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 0478798e0b2e..b399ab6295c7 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -489,6 +489,25 @@ class LatestMapRawValueManagerImpl< } } +/** + * Arguments that are passed to the {@link StateFactory.latestMap} function. + * + * @alpha + */ +export interface LatestMapArguments { + /** + * The initial value of the local state. + */ + local?: { + [K in Keys]: JsonSerializable & JsonDeserialized; + }; + + /** + * See {@link BroadcastControlSettings}. + */ + settings?: BroadcastControlSettings | undefined; +} + /** * Factory for creating a {@link LatestMapRaw} State object. * @@ -499,15 +518,15 @@ export function latestMap< Keys extends string | number = string | number, RegistrationKey extends string = string, >( - initialValues?: { - [K in Keys]: JsonSerializable & JsonDeserialized; - }, - controls?: BroadcastControlSettings, + args?: LatestMapArguments, ): InternalTypes.ManagerFactory< RegistrationKey, InternalTypes.MapValueState, LatestMapRaw > { + const settings = args?.settings; + const initialValues = args?.local; + const timestamp = Date.now(); const value: InternalTypes.MapValueState< T, @@ -517,7 +536,11 @@ export function latestMap< // LatestMapRaw 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 = ( @@ -530,7 +553,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< LatestMapRawValueManagerImpl, T, @@ -540,7 +563,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 d2ca05d34bcb..1ead9618e433 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -181,22 +181,40 @@ class LatestValueManagerImpl } } +/** + * Arguments that are passed to the {@link StateFactory.latest} function. + * + * @alpha + */ +export interface LatestArguments { + /** + * The initial value of the local state. + */ + // eslint-disable-next-line @rushstack/no-new-null + local: JsonSerializable & JsonDeserialized & (object | null); + + /** + * See {@link BroadcastControlSettings}. + */ + settings?: BroadcastControlSettings | undefined; +} + /** * Factory for creating a {@link LatestRaw} State object. * * @alpha */ export function latest( - // eslint-disable-next-line @rushstack/no-new-null - initialValue: JsonSerializable & JsonDeserialized & (object | null), - controls?: BroadcastControlSettings, + args: LatestArguments, ): InternalTypes.ManagerFactory, LatestRaw> { - // Latest takes ownership of initialValue but makes a shallow + const { local, settings } = args; + + // 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: initialValue === null ? initialValue : shallowCloneObject(initialValue), + value: local === null ? local : shallowCloneObject(local), }; const factory = ( key: Key, @@ -208,9 +226,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 4e11e6aa16bc..a0c995c03ef5 100644 --- a/packages/framework/presence/src/test/batching.spec.ts +++ b/packages/framework/presence/src/test/batching.spec.ts @@ -144,7 +144,10 @@ describe("Presence", () => { // Configure a state workspace // SIGNAL #1 - intial data is sent immediately const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), + count: StateFactory.latest({ + local: { num: 0 }, + settings: { allowableUpdateLatencyMs: 0 }, + }), }); const { count } = stateWorkspace.props; @@ -193,7 +196,9 @@ describe("Presence", () => { // Configure a state workspace presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), + count: StateFactory.latest({ + local: { num: 0 } /* default allowableUpdateLatencyMs = 60 */, + }), }); // will be queued; deadline is now 1070 // SIGNAL #1 @@ -267,7 +272,9 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 } /* default allowableUpdateLatencyMs = 60 */), + count: StateFactory.latest({ + local: { num: 0 } /* default allowableUpdateLatencyMs = 60 */, + }), }); // will be queued; deadline is now 1070 const { count } = stateWorkspace.props; @@ -369,7 +376,10 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest({ + local: { num: 0 }, + settings: { allowableUpdateLatencyMs: 100 }, + }), }); const { count } = stateWorkspace.props; @@ -488,8 +498,14 @@ describe("Presence", () => { // SIGNAL #1 - this signal is not queued because it contains a State object with a latency of 0, // so the initial data will be sent immediately. const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), - immediateUpdate: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 0 }), + count: StateFactory.latest({ + local: { num: 0 }, + settings: { allowableUpdateLatencyMs: 100 }, + }), + immediateUpdate: StateFactory.latest({ + local: { num: 0 }, + settings: { allowableUpdateLatencyMs: 0 }, + }), }); const { count, immediateUpdate } = stateWorkspace.props; @@ -575,8 +591,14 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), - note: StateFactory.latest({ message: "" }, { allowableUpdateLatencyMs: 50 }), + count: StateFactory.latest({ + local: { num: 0 }, + settings: { allowableUpdateLatencyMs: 100 }, + }), + note: StateFactory.latest({ + local: { message: "" }, + settings: { allowableUpdateLatencyMs: 50 }, + }), }); // will be queued, deadline is set to 1060 const { count, note } = stateWorkspace.props; @@ -647,11 +669,17 @@ describe("Presence", () => { // Configure two state workspaces const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest({ + local: { num: 0 }, + settings: { allowableUpdateLatencyMs: 100 }, + }), }); // will be queued, deadline is 1110 const stateWorkspace2 = presence.states.getWorkspace("name:testStateWorkspace2", { - note: StateFactory.latest({ message: "" }, { allowableUpdateLatencyMs: 60 }), + note: StateFactory.latest({ + local: { message: "" }, + settings: { allowableUpdateLatencyMs: 60 }, + }), }); // will be queued, deadline is 1070 const { count } = stateWorkspace.props; @@ -840,7 +868,10 @@ describe("Presence", () => { // Configure a state workspace const stateWorkspace = presence.states.getWorkspace("name:testStateWorkspace", { - count: StateFactory.latest({ num: 0 }, { allowableUpdateLatencyMs: 100 }), + count: StateFactory.latest({ + local: { num: 0 }, + settings: { 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 d98cfdbf7f2d..5893da6b9071 100644 --- a/packages/framework/presence/src/test/eventing.spec.ts +++ b/packages/framework/presence/src/test/eventing.spec.ts @@ -246,8 +246,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({ local: { x: 0, y: 0, z: 0 } }), + latestMap: StateFactory.latestMap({ + local: { key1: { a: 0, b: 0 }, key2: { c: 0, d: 0 } }, + }), }); latest = states.props.latest; latestMap = states.props.latestMap; @@ -265,10 +267,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({ local: { 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({ + local: { 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 075cc5f24dfa..b1ff3cee479b 100644 --- a/packages/framework/presence/src/test/latestMapValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestMapValueManager.spec.ts @@ -26,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 } }, - valueControlSettings, - ), + fixedMap: StateFactory.latestMap({ + local: { key1: { x: 0, y: 0 }, key2: { ref: "default", someId: 0 } }, + settings: valueControlSettings, + }), }); return states.props.fixedMap; } @@ -52,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({ 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({ key1: { x: 0, y: 0 } }), + fixedMap: StateFactory.latestMap({ local: { key1: { x: 0, y: 0 } } }), }); assert.strictEqual(states.props.fixedMap.presence, presence); @@ -111,8 +111,10 @@ export function checkCompiles(): void { "name:testStatesWorkspaceWithLatestMap", { fixedMap: StateFactory.latestMap({ - key1: { x: 0, y: 0 }, - key2: { ref: "default", someId: 0 }, + local: { + key1: { x: 0, y: 0 }, + key2: { ref: "default", someId: 0 }, + }, }), }, ); @@ -148,7 +150,7 @@ export function checkCompiles(): void { tilt?: number; } - workspace.add("pointers", StateFactory.latestMap({})); + workspace.add("pointers", StateFactory.latestMap({ local: {} })); const pointers = workspace.props.pointers; const localPointers = pointers.local; @@ -194,11 +196,13 @@ export function checkCompiles(): void { workspace.add( "primitiveMap", StateFactory.latestMap({ - // eslint-disable-next-line unicorn/no-null - null: null, - string: "string", - number: 0, - boolean: true, + local: { + // eslint-disable-next-line unicorn/no-null + null: null, + string: "string", + number: 0, + boolean: true, + }, }), ); diff --git a/packages/framework/presence/src/test/latestValueManager.spec.ts b/packages/framework/presence/src/test/latestValueManager.spec.ts index 670cc0d2bfe3..aa1055af9aed 100644 --- a/packages/framework/presence/src/test/latestValueManager.spec.ts +++ b/packages/framework/presence/src/test/latestValueManager.spec.ts @@ -27,7 +27,10 @@ function createLatestManager( valueControlSettings?: BroadcastControlSettings, ) { const states = presence.states.getWorkspace(testWorkspaceName, { - camera: StateFactory.latest({ x: 0, y: 0, z: 0 }, valueControlSettings), + camera: StateFactory.latest({ + local: { x: 0, y: 0, z: 0 }, + settings: valueControlSettings, + }), }); return states.props.camera; } @@ -48,42 +51,42 @@ describe("Presence", () => { it("can set and get empty object as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - obj: StateFactory.latest({}), + 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({ 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([]), + 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([1, 2, 3]), + arr: StateFactory.latest({ local: [1, 2, 3] }), }); assert.deepStrictEqual(states.props.arr.local, [1, 2, 3]); }); it("can set and get null as initial value", () => { const states = presence.states.getWorkspace(testWorkspaceName, { - arr: StateFactory.latest(null), + nullable: StateFactory.latest({ local: null }), }); - assert.deepStrictEqual(states.props.arr.local, null); + assert.deepStrictEqual(states.props.nullable.local, null); }); 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({ local: { x: 0, y: 0, z: 0 } }), }); assert.strictEqual(states.props.camera.presence, presence); @@ -92,7 +95,9 @@ describe("Presence", () => { it("can set and get null as modified local value", () => { // Setup const states = presence.states.getWorkspace(testWorkspaceName, { - nullable: StateFactory.latest<{ x: number; y: number } | null>({ x: 0, y: 0 }), + nullable: StateFactory.latest<{ x: number; y: number } | null>({ + local: { x: 0, y: 0 }, + }), }); // Act and Verify @@ -107,7 +112,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({ local: { x: 0, y: 0, z: 0 } }), }); const camera = states.props.camera; @@ -133,15 +138,15 @@ 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 }), - nullablePoint: StateFactory.latest(null), + cursor: StateFactory.latest({ local: { x: 0, y: 0 } }), + camera: StateFactory.latest({ local: { x: 0, y: 0, z: 0 } }), + nullablePoint: StateFactory.latest({ local: null }), }); // 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({ 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 862fe4ec738b..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({}), + obj: StateFactory.latest({ local: {} }), }); assert.strictEqual(states.presence, presence); });