diff --git a/packages/framework/presence/api-report/presence.alpha.api.md b/packages/framework/presence/api-report/presence.alpha.api.md index 116ba0484450..0b74359f11c6 100644 --- a/packages/framework/presence/api-report/presence.alpha.api.md +++ b/packages/framework/presence/api-report/presence.alpha.api.md @@ -112,12 +112,12 @@ export namespace InternalTypes { // @system (undocumented) export interface ValueOptionalState extends ValueStateMetadata { // (undocumented) - value?: JsonDeserialized; + value?: OpaqueJsonDeserialized; } // @system (undocumented) export interface ValueRequiredState extends ValueStateMetadata { // (undocumented) - value: JsonDeserialized; + value: OpaqueJsonDeserialized; } // @system (undocumented) export interface ValueStateMetadata { @@ -128,18 +128,20 @@ export namespace InternalTypes { } } -// @alpha @system +// @beta @system export namespace InternalUtilityTypes { // @system - export type IsNotificationListener = Event extends (...args: infer P) => void ? InternalUtilityTypes_2.IfSameType & JsonDeserialized

, true, false> : false; + export type IsNotificationListener = Event extends (...args: infer P) => void ? InternalUtilityTypes_2.IfSameType & JsonDeserialized_2

, true, false> : false; // @system - export type JsonDeserializedParameters any> = T extends (...args: infer P) => any ? JsonDeserialized

: never; + export type JsonDeserializedParameters any> = T extends (...args: infer P) => any ? JsonDeserialized_2

: never; // @system - export type JsonSerializableParameters any> = T extends (...args: infer P) => any ? JsonSerializable

: never; + export type JsonSerializableParameters any> = T extends (...args: infer P) => any ? JsonSerializable_2

: never; // @system export type NotificationListeners = { [P in string & keyof E as IsNotificationListener extends true ? P : never]: E[P]; }; + // @system (undocumented) + export type OpaqueJsonSerializable = JsonSerializableBrand; } // @beta @@ -147,7 +149,7 @@ export function latest(arg // @beta export interface LatestArguments { - local: JsonSerializable & JsonDeserialized & (object | null); + local: JsonSerializable_2 & JsonDeserialized_2 & (object | null); settings?: BroadcastControlSettings | undefined; } @@ -162,7 +164,7 @@ export interface LatestData { // (undocumented) metadata: LatestMetadata; // (undocumented) - value: DeepReadonly>; + value: DeepReadonly>; } // @beta @@ -171,7 +173,7 @@ export function latestMap { local?: { - [K in Keys]: JsonSerializable & JsonDeserialized; + [K in Keys]: JsonSerializable_2 & JsonDeserialized_2; }; settings?: BroadcastControlSettings | undefined; } @@ -218,7 +220,7 @@ export interface LatestMapRawEvents { }) => void; // @eventProperty localItemUpdated: (updatedItem: { - value: DeepReadonly & JsonDeserialized>; + value: DeepReadonly & JsonDeserialized_2>; key: K; }) => void; // @eventProperty @@ -242,8 +244,8 @@ export interface LatestRaw { getRemote(attendee: Attendee): LatestData; getRemotes(): IterableIterator>; getStateAttendees(): Attendee[]; - get local(): DeepReadonly>; - set local(value: JsonSerializable & JsonDeserialized); + get local(): DeepReadonly>; + set local(value: JsonSerializable_2 & JsonDeserialized_2); readonly presence: Presence; } @@ -251,7 +253,7 @@ export interface LatestRaw { export interface LatestRawEvents { // @eventProperty localUpdated: (update: { - value: DeepReadonly & JsonDeserialized>; + value: DeepReadonly & JsonDeserialized_2>; }) => void; // @eventProperty remoteUpdated: (update: LatestClientData) => void; @@ -344,12 +346,12 @@ export interface StateMap { clear(): void; // (undocumented) delete(key: K): boolean; - forEach(callbackfn: (value: DeepReadonly>, key: K, map: StateMap) => void, thisArg?: unknown): void; - get(key: K): DeepReadonly> | undefined; + forEach(callbackfn: (value: DeepReadonly>, key: K, map: StateMap) => void, thisArg?: unknown): void; + get(key: K): DeepReadonly> | undefined; // (undocumented) has(key: K): boolean; keys(): IterableIterator; - set(key: K, value: JsonSerializable & JsonDeserialized): this; + set(key: K, value: JsonSerializable_2 & JsonDeserialized_2): this; // (undocumented) readonly size: number; } diff --git a/packages/framework/presence/api-report/presence.beta.api.md b/packages/framework/presence/api-report/presence.beta.api.md index c68db527364d..6b42795f3970 100644 --- a/packages/framework/presence/api-report/presence.beta.api.md +++ b/packages/framework/presence/api-report/presence.beta.api.md @@ -99,12 +99,12 @@ export namespace InternalTypes { // @system (undocumented) export interface ValueOptionalState extends ValueStateMetadata { // (undocumented) - value?: JsonDeserialized; + value?: OpaqueJsonDeserialized; } // @system (undocumented) export interface ValueRequiredState extends ValueStateMetadata { // (undocumented) - value: JsonDeserialized; + value: OpaqueJsonDeserialized; } // @system (undocumented) export interface ValueStateMetadata { @@ -115,12 +115,28 @@ export namespace InternalTypes { } } +// @beta @system +export namespace InternalUtilityTypes { + // @system + export type IsNotificationListener = Event extends (...args: infer P) => void ? InternalUtilityTypes_2.IfSameType & JsonDeserialized_2

, true, false> : false; + // @system + export type JsonDeserializedParameters any> = T extends (...args: infer P) => any ? JsonDeserialized_2

: never; + // @system + export type JsonSerializableParameters any> = T extends (...args: infer P) => any ? JsonSerializable_2

: never; + // @system + export type NotificationListeners = { + [P in string & keyof E as IsNotificationListener extends true ? P : never]: E[P]; + }; + // @system (undocumented) + export type OpaqueJsonSerializable = JsonSerializableBrand; +} + // @beta export function latest(args: LatestArguments): InternalTypes.ManagerFactory, LatestRaw>; // @beta export interface LatestArguments { - local: JsonSerializable & JsonDeserialized & (object | null); + local: JsonSerializable_2 & JsonDeserialized_2 & (object | null); settings?: BroadcastControlSettings | undefined; } @@ -135,7 +151,7 @@ export interface LatestData { // (undocumented) metadata: LatestMetadata; // (undocumented) - value: DeepReadonly>; + value: DeepReadonly>; } // @beta @@ -144,7 +160,7 @@ export function latestMap { local?: { - [K in Keys]: JsonSerializable & JsonDeserialized; + [K in Keys]: JsonSerializable_2 & JsonDeserialized_2; }; settings?: BroadcastControlSettings | undefined; } @@ -191,7 +207,7 @@ export interface LatestMapRawEvents { }) => void; // @eventProperty localItemUpdated: (updatedItem: { - value: DeepReadonly & JsonDeserialized>; + value: DeepReadonly & JsonDeserialized_2>; key: K; }) => void; // @eventProperty @@ -215,8 +231,8 @@ export interface LatestRaw { getRemote(attendee: Attendee): LatestData; getRemotes(): IterableIterator>; getStateAttendees(): Attendee[]; - get local(): DeepReadonly>; - set local(value: JsonSerializable & JsonDeserialized); + get local(): DeepReadonly>; + set local(value: JsonSerializable_2 & JsonDeserialized_2); readonly presence: Presence; } @@ -224,7 +240,7 @@ export interface LatestRaw { export interface LatestRawEvents { // @eventProperty localUpdated: (update: { - value: DeepReadonly & JsonDeserialized>; + value: DeepReadonly & JsonDeserialized_2>; }) => void; // @eventProperty remoteUpdated: (update: LatestClientData) => void; @@ -262,12 +278,12 @@ export interface StateMap { clear(): void; // (undocumented) delete(key: K): boolean; - forEach(callbackfn: (value: DeepReadonly>, key: K, map: StateMap) => void, thisArg?: unknown): void; - get(key: K): DeepReadonly> | undefined; + forEach(callbackfn: (value: DeepReadonly>, key: K, map: StateMap) => void, thisArg?: unknown): void; + get(key: K): DeepReadonly> | undefined; // (undocumented) has(key: K): boolean; keys(): IterableIterator; - set(key: K, value: JsonSerializable & JsonDeserialized): this; + set(key: K, value: JsonSerializable_2 & JsonDeserialized_2): this; // (undocumented) readonly size: number; } diff --git a/packages/framework/presence/src/exposedInternalTypes.ts b/packages/framework/presence/src/exposedInternalTypes.ts index 46e457098129..a08ced3c0065 100644 --- a/packages/framework/presence/src/exposedInternalTypes.ts +++ b/packages/framework/presence/src/exposedInternalTypes.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. */ +import type { OpaqueJsonDeserialized } from "@fluidframework/container-runtime-definitions/internal"; import type { JsonDeserialized, JsonSerializable, -} from "@fluidframework/core-interfaces/internal/exposedUtilityTypes"; +} from "@fluidframework/core-interfaces/internal"; /** * Collection of value types that are not intended to be used/imported @@ -29,14 +30,14 @@ export namespace InternalTypes { * @system */ export interface ValueOptionalState extends ValueStateMetadata { - value?: JsonDeserialized; + value?: OpaqueJsonDeserialized; } /** * @system */ export interface ValueRequiredState extends ValueStateMetadata { - value: JsonDeserialized; + value: OpaqueJsonDeserialized; } /** diff --git a/packages/framework/presence/src/exposedUtilityTypes.ts b/packages/framework/presence/src/exposedUtilityTypes.ts index a6df51d9f084..5d6595ad699b 100644 --- a/packages/framework/presence/src/exposedUtilityTypes.ts +++ b/packages/framework/presence/src/exposedUtilityTypes.ts @@ -3,17 +3,22 @@ * Licensed under the MIT License. */ +import { BrandedType } from "@fluidframework/core-interfaces/internal"; import type { + DeepReadonly, InternalUtilityTypes as CoreInternalUtilityTypes, JsonDeserialized, JsonSerializable, } from "@fluidframework/core-interfaces/internal/exposedUtilityTypes"; +import { asDeeplyReadonly } from "./internalUtils.js"; +import type { OpaqueJsonDeserialized } from "@fluidframework/container-runtime-definitions/internal"; + /** * Collection of utility types that are not intended to be used/imported * directly outside of this package. * - * @alpha + * @beta * @system */ // eslint-disable-next-line @typescript-eslint/no-namespace @@ -76,4 +81,63 @@ export namespace InternalUtilityTypes { ) => any ? JsonSerializable

: never; + + /** + * @internal + * @system + */ + declare class JsonDeserializedBrand extends BrandedType { + private readonly EncodedValue: T; + private constructor(); + } + + /** + * @system + */ + // export type JsonDeserializedHandle = Tagged, "JsonDeserialized">; + // export type OpaqueJsonDeserialized = UnverifiedBrand; + // export type OpaqueJsonDeserialized = JsonDeserializedBrand; + // export type OpaqueJsonDeserialized, U> = JsonDeserializedBrand; + + /** + * @internal + * @system + */ + export declare class JsonSerializableBrand extends BrandedType { + private readonly JsonSerializable: JsonSerializable; + private constructor(); + } + + /** + * @system + */ + // export type JsonDeserializedHandle = Tagged, "JsonDeserialized">; + export type OpaqueJsonSerializable = JsonSerializableBrand; +} + +/** + * Cast a JsonDeserialized value to its branded version. + * + * @system + */ +export function brandJson(value: JsonDeserialized): OpaqueJsonDeserialized { + return value as OpaqueJsonDeserialized; +} + +/** + * Cast a branded JsonDeserialized value back to its unbranded version. + * + * @system + */ +export function unbrandJson(value: OpaqueJsonDeserialized): JsonDeserialized { + return value as JsonDeserialized; +} + +/** + * Converts a JsonDeserializedHandle to a deeply readonly JsonDeserialized value. + */ +export function asDeeplyReadonlyFromJsonHandle( + value: OpaqueJsonDeserialized, +): DeepReadonly> { + return asDeeplyReadonly(unbrandJson(value)); } diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index 17e17bf6b8f6..6a2f066c440f 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -15,6 +15,11 @@ import type { import type { BroadcastControls, BroadcastControlSettings } from "./broadcastControls.js"; import { OptionalBroadcastControl } from "./broadcastControls.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; +import { + asDeeplyReadonlyFromJsonHandle, + brandJson, + // type InternalUtilityTypes, +} from "./exposedUtilityTypes.js"; import type { PostUpdateAction, ValueManager } from "./internalTypes.js"; import { asDeeplyReadonly, objectEntries, objectKeys } from "./internalUtils.js"; import type { LatestClientData, LatestData, LatestMetadata } from "./latestValueTypes.js"; @@ -266,12 +271,15 @@ class ValueMapImpl implements StateMap { ): void { for (const [key, item] of objectEntries(this.value.items)) { if (item.value !== undefined) { - callbackfn(asDeeplyReadonly(item.value), key, this); + callbackfn(asDeeplyReadonlyFromJsonHandle(item.value), key, this); } } } public get(key: K): DeepReadonly> | undefined { - return asDeeplyReadonly(this.value.items[key]?.value); + return this.value.items[key]?.value === undefined + ? undefined + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ternary ensures this is non-null + asDeeplyReadonlyFromJsonHandle(this.value.items[key]!.value!); } public has(key: K): boolean { return this.value.items[key]?.value !== undefined; @@ -279,9 +287,9 @@ class ValueMapImpl implements StateMap { public set(key: K, value: JsonSerializable & JsonDeserialized): this { if (!(key in this.value.items)) { this.countDefined += 1; - this.value.items[key] = { rev: 0, timestamp: 0, value }; + this.value.items[key] = { rev: 0, timestamp: 0, value: brandJson(value) }; } - this.updateItem(key, value); + this.updateItem(key, brandJson(value)); this.emitter.emit("localItemUpdated", { key, value: asDeeplyReadonly(value) }); return this; } @@ -413,7 +421,7 @@ class LatestMapRawValueManagerImpl< const value = item.value; if (value !== undefined) { items.set(key, { - value: asDeeplyReadonly(value), + value: asDeeplyReadonlyFromJsonHandle(value), metadata: { revision: item.rev, timestamp: item.timestamp }, }); } @@ -464,13 +472,13 @@ class LatestMapRawValueManagerImpl< currentState.items[key] = item; const metadata = { revision: item.rev, timestamp: item.timestamp }; if (item.value !== undefined) { - const itemValue = asDeeplyReadonly(item.value); + const itemValue = asDeeplyReadonlyFromJsonHandle(item.value); const updatedItem = { attendee, key, value: itemValue, metadata, - }; + } satisfies LatestMapItemUpdatedClientData; postUpdateActions.push(() => this.events.emit("remoteItemUpdated", updatedItem)); allUpdates.items.set(key, { value: itemValue, metadata }); } else if (hadPriorValue !== undefined) { @@ -539,7 +547,7 @@ export function latestMap< value.items[key] = { rev: 0, timestamp, - value: initialValues[key], + value: brandJson(initialValues[key]), }; } } diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 090c6f8f8d4b..80d3f5148e2c 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -15,12 +15,18 @@ import { shallowCloneObject } from "@fluidframework/core-utils/internal"; import type { BroadcastControls, BroadcastControlSettings } from "./broadcastControls.js"; import { OptionalBroadcastControl } from "./broadcastControls.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; +import { + unbrandJson, + brandJson, + asDeeplyReadonlyFromJsonHandle, +} from "./exposedUtilityTypes.js"; import type { PostUpdateAction, ValueManager } from "./internalTypes.js"; import { asDeeplyReadonly, objectEntries } from "./internalUtils.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"; +import type { OpaqueJsonDeserialized } from "@fluidframework/container-runtime-definitions/internal"; /** * @sealed @@ -112,13 +118,13 @@ class LatestValueManagerImpl } public get local(): DeepReadonly> { - return asDeeplyReadonly(this.value.value); + return asDeeplyReadonlyFromJsonHandle(this.value.value); } public set local(value: JsonSerializable & JsonDeserialized) { this.value.rev += 1; this.value.timestamp = Date.now(); - this.value.value = value; + this.value.value = brandJson(value); this.datastore.localUpdate(this.key, this.value, { allowableUpdateLatencyMs: this.controls.allowableUpdateLatencyMs, }); @@ -132,7 +138,7 @@ class LatestValueManagerImpl if (attendeeId !== allKnownStates.self) { yield { attendee: this.datastore.lookupClient(attendeeId), - value: asDeeplyReadonly(value.value), + value: asDeeplyReadonly(unbrandJson(value.value)), metadata: { revision: value.rev, timestamp: value.timestamp }, }; } @@ -153,7 +159,7 @@ class LatestValueManagerImpl throw new Error("No entry for clientId"); } return { - value: asDeeplyReadonly(clientState.value), + value: asDeeplyReadonly(unbrandJson(clientState.value)), metadata: { revision: clientState.rev, timestamp: Date.now() }, }; } @@ -174,7 +180,7 @@ class LatestValueManagerImpl () => this.events.emit("remoteUpdated", { attendee, - value: asDeeplyReadonly(value.value), + value: asDeeplyReadonly(unbrandJson(value.value)), metadata: { revision: value.rev, timestamp: value.timestamp }, }), ]; @@ -211,10 +217,15 @@ export function latest( // Latest takes ownership of the initial local value but makes a shallow // copy for basic protection. + const internalValue = + local === null + ? (local as unknown as OpaqueJsonDeserialized) + : // FIXME: Why isn't this directly castable? + (shallowCloneObject(local) as unknown as OpaqueJsonDeserialized); const value: InternalTypes.ValueRequiredState = { rev: 0, timestamp: Date.now(), - value: local === null ? local : shallowCloneObject(local), + value: internalValue, }; const factory = ( key: Key, diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index 12857b260599..5611402b04b9 100644 --- a/packages/framework/presence/src/notificationsManager.ts +++ b/packages/framework/presence/src/notificationsManager.ts @@ -8,11 +8,12 @@ import type { Listeners, Listenable, Off } from "@fluidframework/core-interfaces import type { JsonTypeWith } from "@fluidframework/core-interfaces/internal"; import type { InternalTypes } from "./exposedInternalTypes.js"; -import type { InternalUtilityTypes } from "./exposedUtilityTypes.js"; +import { unbrandJson, type InternalUtilityTypes } from "./exposedUtilityTypes.js"; import type { PostUpdateAction, ValueManager } from "./internalTypes.js"; import type { Attendee, PresenceWithNotifications as Presence } from "./presence.js"; import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js"; import { brandIVM } from "./valueManager.js"; +import type { OpaqueJsonDeserialized } from "@fluidframework/container-runtime-definitions/internal"; /** * @sealed @@ -177,7 +178,11 @@ class NotificationsManagerImpl< { rev: 0, timestamp: 0, - value: { name, args: [...(args as JsonTypeWith[])] }, + value: { + name, + args: [...(args as JsonTypeWith[])], + // FIXME: Why doesn't as cast work? + } as unknown as OpaqueJsonDeserialized, ignoreUnmonitored: true, }, // This is a notification, so we want to send it immediately. @@ -190,7 +195,11 @@ class NotificationsManagerImpl< { rev: 0, timestamp: 0, - value: { name, args: [...(args as JsonTypeWith[])] }, + value: { + name, + args: [...(args as JsonTypeWith[])], + // FIXME: Why doesn't as cast work? + } as unknown as OpaqueJsonDeserialized, ignoreUnmonitored: true, }, // This is a notification, so we want to send it immediately. @@ -237,12 +246,13 @@ class NotificationsManagerImpl< _received: number, value: InternalTypes.ValueRequiredState, ): PostUpdateAction[] { + const unbrandedValue = unbrandJson(value.value); const postUpdateActions: PostUpdateAction[] = []; - const eventName = value.value.name as keyof Listeners>; + const eventName = unbrandedValue.name as keyof Listeners>; if (this.notificationsInternal.hasListeners(eventName)) { // Without schema validation, we don't know that the args are the correct type. // For now we assume the user is sending the correct types and there is no corruption along the way. - const args = [attendee, ...value.value.args] as Parameters< + const args = [attendee, ...unbrandedValue.args] as Parameters< NotificationSubscriptions[typeof eventName] >; postUpdateActions.push(() => this.notificationsInternal.emit(eventName, ...args)); @@ -250,9 +260,9 @@ class NotificationsManagerImpl< postUpdateActions.push(() => this.events.emit( "unattendedNotification", - value.value.name, + unbrandedValue.name, attendee, - ...value.value.args, + ...unbrandedValue.args, ), ); } diff --git a/packages/framework/presence/src/presenceDatastoreManager.ts b/packages/framework/presence/src/presenceDatastoreManager.ts index bf566a404b01..78ecdbd309e9 100644 --- a/packages/framework/presence/src/presenceDatastoreManager.ts +++ b/packages/framework/presence/src/presenceDatastoreManager.ts @@ -167,6 +167,9 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { this.runtime.submitSignal({ type: joinMessageType, content: { + // FIXME: error TS2353: Object literal may only specify known properties, and 'sendTimestamp' does not exist in + // type 'SerializationErrorPerNonPublicProperties'. + //// @ts-ignore sendTimestamp: Date.now(), avgLatency: this.averageLatency, data: this.datastore, @@ -303,6 +306,9 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { this.datastore["system:presence"].clientToSessionId[clientConnectionId]!; const newMessage = { + // FIXME: error TS2353: Object literal may only specify known properties, and 'sendTimestamp' does not exist in + // type 'SerializationErrorPerNonPublicProperties'. + //// @ts-ignore sendTimestamp: Date.now(), avgLatency: this.averageLatency, // isComplete: false, @@ -320,13 +326,24 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { }, } satisfies OutboundDatastoreUpdateMessage["content"]; this.queuedData = undefined; - this.runtime.submitSignal({ type: datastoreUpdateMessageType, content: newMessage }); + this.runtime.submitSignal({ + type: datastoreUpdateMessageType, + // FIXME: error TS2741: Property '"object serialization error"' is missing in type '{ sendTimestamp: number; + // avgLatency: number; data: { "system:presence": { clientToSessionId: { [x: string]: { value: + // InternalUtilityTypes.OpaqueJsonDeserialized; rev: number; timestamp: number; }; }; }; }; }' but + // required in type 'SerializationErrorPerNonPublicProperties'. + //// @ts-ignore + content: newMessage, + }); } private broadcastAllKnownState(): void { this.runtime.submitSignal({ type: datastoreUpdateMessageType, content: { + // FIXME: error TS2353: Object literal may only specify known properties, and 'sendTimestamp' does not exist in + // type 'SerializationErrorPerNonPublicProperties'. + //// @ts-ignore sendTimestamp: Date.now(), avgLatency: this.averageLatency, isComplete: true, @@ -417,6 +434,11 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { ...workspace.internal.processUpdate( received, timeModifier, + // FIXME: error TS2345: Argument of type '{ [x: string]: { [x: string & { readonly StableId: + // "53172b0d-a3d5-41ea-bd75-b43839c97f5a"; } & { readonly SessionId: "4498f850-e14e-4be9-8db0-89ec00997e58"; + // } & { readonly AttendeeId: "AttendeeId"; }]: { ...; } | { ...; }; }; } | { ...; }' is not assignable to + // parameter of type 'ValueUpdateRecord'. + //// @ts-ignore remoteDatastore, message.clientId, ), @@ -428,6 +450,14 @@ export class PresenceDatastoreManagerImpl implements PresenceDatastoreManager { // Ensure there is a datastore at this address and get it. const workspaceDatastore = (this.datastore[workspaceAddress] ??= {}); for (const [key, remoteAllKnownState] of Object.entries(remoteDatastore)) { + // error TS2345: Argument of type '{ [x: string & { readonly StableId: "53172b0d-a3d5-41ea-bd75-b43839c97f5a"; + // } & { readonly SessionId: "4498f850-e14e-4be9-8db0-89ec00997e58"; } & { readonly AttendeeId: "AttendeeId"; + // }]: { ...; } | { ...; }; }' is not assignable to parameter of type 'ClientUpdateRecord'. + // + // 'string & {readonly StableId: "53172b0d-a3d5-41ea-bd75-b43839c97f5a"; } & { readonly SessionId: + // "4498f850-e14e-4be9-8db0-89ec00997e58"; } & { readonly AttendeeId: "AttendeeId"; }' and 'AttendeeId' index + // signatures are incompatible. + //// @ts-ignore mergeUntrackedDatastore(key, remoteAllKnownState, workspaceDatastore, timeModifier); } } diff --git a/packages/framework/presence/src/systemWorkspace.ts b/packages/framework/presence/src/systemWorkspace.ts index b4675e4221fe..7582e48f12a6 100644 --- a/packages/framework/presence/src/systemWorkspace.ts +++ b/packages/framework/presence/src/systemWorkspace.ts @@ -9,6 +9,7 @@ import { assert } from "@fluidframework/core-utils/internal"; import type { ClientConnectionId } from "./baseTypes.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; +import { unbrandJson, brandJson } from "./exposedUtilityTypes.js"; import type { PostUpdateAction } from "./internalTypes.js"; import type { Attendee, AttendeesEvents, AttendeeId, Presence } from "./presence.js"; import { AttendeeStatus } from "./presence.js"; @@ -133,7 +134,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { for (const [clientConnectionId, value] of Object.entries( remoteDatastore.clientToSessionId, )) { - const attendeeId = value.value; + const attendeeId = unbrandJson(value.value); const { attendee, isJoining } = this.ensureAttendee( attendeeId, clientConnectionId, @@ -169,7 +170,7 @@ class SystemWorkspaceImpl implements PresenceStatesInternal, SystemWorkspace { this.datastore.clientToSessionId[clientConnectionId] = { rev: this.selfAttendee.order++, timestamp: Date.now(), - value: this.selfAttendee.attendeeId, + value: brandJson(this.selfAttendee.attendeeId), }; // Mark 'Connected' remote attendees connections as stale @@ -306,7 +307,8 @@ export function createSystemWorkspace( return { workspace, statesEntry: { - internal: workspace, + // FIXME: Cast shouldn't be needed + internal: workspace as PresenceStatesInternal, public: undefined as unknown as AnyWorkspace, }, }; diff --git a/packages/runtime/container-runtime-definitions/src/containerExtension.ts b/packages/runtime/container-runtime-definitions/src/containerExtension.ts index 28f40e031937..261f6ee8ae1b 100644 --- a/packages/runtime/container-runtime-definitions/src/containerExtension.ts +++ b/packages/runtime/container-runtime-definitions/src/containerExtension.ts @@ -57,7 +57,13 @@ export type ExtensionMessage< * @internal */ export type OutboundExtensionMessage = - ExtensionMessage<{ type: TMessage["type"]; content: JsonSerializable }>; + ExtensionMessage<{ + type: TMessage["type"]; + content: JsonSerializable< + TMessage["content"], + { AllowExtensionOf: OpaqueJsonDeserialized } + >; + }>; /** * Brand for value that has not been verified. @@ -299,3 +305,16 @@ export interface ContainerExtensionStore { ...context: TUseContext ): T; } + +/** + * @internal + */ +export declare class JsonDeserializedBrand extends BrandedType { + private readonly EncodedValue: T; + private constructor(); +} + +/** + * @internal + */ +export type OpaqueJsonDeserialized = JsonDeserializedBrand; diff --git a/packages/runtime/container-runtime-definitions/src/index.ts b/packages/runtime/container-runtime-definitions/src/index.ts index bf0dc6d1eaae..feb373ab0170 100644 --- a/packages/runtime/container-runtime-definitions/src/index.ts +++ b/packages/runtime/container-runtime-definitions/src/index.ts @@ -14,7 +14,9 @@ export type { ExtensionMessage, ExtensionRuntimeProperties, InboundExtensionMessage, + JsonDeserializedBrand, OutboundExtensionMessage, + OpaqueJsonDeserialized, RawInboundExtensionMessage, UnverifiedBrand, VerifiedInboundExtensionMessage,