Skip to content

improvement(client-presence): Json in-out improvements #24772

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions packages/framework/presence/api-report/presence.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export namespace InternalTypes {
// @system (undocumented)
export interface NotificationType {
// (undocumented)
args: (JsonSerializable<unknown> & JsonDeserialized<unknown>)[];
args: unknown[];
// (undocumented)
name: string;
}
Expand All @@ -109,15 +109,15 @@ export namespace InternalTypes {
}
// @system (undocumented)
export type ValueDirectoryOrState<T> = ValueRequiredState<T> | ValueDirectory<T>;
// @system (undocumented)
// @system
export interface ValueOptionalState<TValue> extends ValueStateMetadata {
// (undocumented)
value?: JsonDeserialized<TValue>;
value?: OpaqueJsonDeserialized<TValue>;
}
// @system (undocumented)
// @system
export interface ValueRequiredState<TValue> extends ValueStateMetadata {
// (undocumented)
value: JsonDeserialized<TValue>;
value: OpaqueJsonDeserialized<TValue>;
}
// @system (undocumented)
export interface ValueStateMetadata {
Expand All @@ -131,23 +131,23 @@ export namespace InternalTypes {
// @alpha @system
export namespace InternalUtilityTypes {
// @system
export type IsNotificationListener<Event> = Event extends (...args: infer P) => void ? InternalUtilityTypes_2.IfSameType<P, JsonSerializable<P> & JsonDeserialized<P>, true, false> : false;
export type IfNotificationListener<Event, IfListener, Else> = Event extends (...args: infer P) => void ? InternalUtilityTypes_2.IfSameType<P, JsonSerializable<P>, IfListener, Else> : Else;
// @system
export type JsonDeserializedParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? JsonDeserialized<P> : never;
export type JsonDeserializedParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? JsonDeserialized<P> : never;
// @system
export type JsonSerializableParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? JsonSerializable<P> : never;
export type JsonSerializableParameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? JsonSerializable<P> : never;
// @system
export type NotificationListeners<E> = {
[P in string & keyof E as IsNotificationListener<E[P]> extends true ? P : never]: E[P];
[P in keyof E as IfNotificationListener<E[P], P, never>]: E[P];
};
}

// @beta
export function latest<T extends object | null, Key extends string = string>(args: LatestArguments<T>): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, LatestRaw<T>>;

// @beta
// @beta @input
export interface LatestArguments<T extends object | null> {
local: JsonSerializable<T> & JsonDeserialized<T> & (object | null);
local: JsonSerializable<T>;
settings?: BroadcastControlSettings | undefined;
}

Expand All @@ -165,10 +165,10 @@ export interface LatestData<T> {
// @beta
export function latestMap<T, Keys extends string | number = string | number, RegistrationKey extends string = string>(args?: LatestMapArguments<T, Keys>): InternalTypes.ManagerFactory<RegistrationKey, InternalTypes.MapValueState<T, Keys>, LatestMapRaw<T, Keys>>;

// @beta
// @beta @input
export interface LatestMapArguments<T, Keys extends string | number = string | number> {
local?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
[K in Keys]: JsonSerializable<T>;
};
settings?: BroadcastControlSettings | undefined;
}
Expand Down Expand Up @@ -210,7 +210,7 @@ export interface LatestMapRawEvents<T, K extends string | number> {
}) => void;
// @eventProperty
localItemUpdated: (updatedItem: {
value: DeepReadonly<JsonSerializable<T> & JsonDeserialized<T>>;
value: DeepReadonly<JsonSerializable<T>>;
key: K;
}) => void;
// @eventProperty
Expand All @@ -235,24 +235,24 @@ export interface LatestRaw<T> {
getRemotes(): IterableIterator<LatestClientData<T>>;
getStateAttendees(): Attendee[];
get local(): DeepReadonly<JsonDeserialized<T>>;
set local(value: JsonSerializable<T> & JsonDeserialized<T>);
set local(value: JsonSerializable<T>);
readonly presence: Presence;
}

// @beta @sealed
export interface LatestRawEvents<T> {
// @eventProperty
localUpdated: (update: {
value: DeepReadonly<JsonSerializable<T> & JsonDeserialized<T>>;
value: DeepReadonly<JsonSerializable<T>>;
}) => void;
// @eventProperty
remoteUpdated: (update: LatestClientData<T>) => void;
}

// @alpha @sealed
export interface NotificationEmitter<E extends InternalUtilityTypes.NotificationListeners<E>> {
broadcast<K extends string & keyof InternalUtilityTypes.NotificationListeners<E>>(notificationName: K, ...args: Parameters<E[K]>): void;
unicast<K extends string & keyof InternalUtilityTypes.NotificationListeners<E>>(notificationName: K, targetAttendee: Attendee, ...args: Parameters<E[K]>): void;
broadcast<K extends keyof InternalUtilityTypes.NotificationListeners<E>>(notificationName: K, ...args: Parameters<E[K]>): void;
unicast<K extends keyof InternalUtilityTypes.NotificationListeners<E>>(notificationName: K, targetAttendee: Attendee, ...args: Parameters<E[K]>): void;
}

// @alpha @sealed
Expand Down Expand Up @@ -339,7 +339,7 @@ export interface StateMap<K extends string | number, V> {
get(key: K): DeepReadonly<JsonDeserialized<V>> | undefined;
has(key: K): boolean;
keys(): IterableIterator<K>;
set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
set(key: K, value: JsonSerializable<V>): this;
readonly size: number;
}

Expand Down
26 changes: 13 additions & 13 deletions packages/framework/presence/api-report/presence.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export namespace InternalTypes {
// @system (undocumented)
export interface NotificationType {
// (undocumented)
args: (JsonSerializable<unknown> & JsonDeserialized<unknown>)[];
args: unknown[];
// (undocumented)
name: string;
}
Expand All @@ -96,15 +96,15 @@ export namespace InternalTypes {
}
// @system (undocumented)
export type ValueDirectoryOrState<T> = ValueRequiredState<T> | ValueDirectory<T>;
// @system (undocumented)
// @system
export interface ValueOptionalState<TValue> extends ValueStateMetadata {
// (undocumented)
value?: JsonDeserialized<TValue>;
value?: OpaqueJsonDeserialized<TValue>;
}
// @system (undocumented)
// @system
export interface ValueRequiredState<TValue> extends ValueStateMetadata {
// (undocumented)
value: JsonDeserialized<TValue>;
value: OpaqueJsonDeserialized<TValue>;
}
// @system (undocumented)
export interface ValueStateMetadata {
Expand All @@ -118,9 +118,9 @@ export namespace InternalTypes {
// @beta
export function latest<T extends object | null, Key extends string = string>(args: LatestArguments<T>): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, LatestRaw<T>>;

// @beta
// @beta @input
export interface LatestArguments<T extends object | null> {
local: JsonSerializable<T> & JsonDeserialized<T> & (object | null);
local: JsonSerializable<T>;
settings?: BroadcastControlSettings | undefined;
}

Expand All @@ -138,10 +138,10 @@ export interface LatestData<T> {
// @beta
export function latestMap<T, Keys extends string | number = string | number, RegistrationKey extends string = string>(args?: LatestMapArguments<T, Keys>): InternalTypes.ManagerFactory<RegistrationKey, InternalTypes.MapValueState<T, Keys>, LatestMapRaw<T, Keys>>;

// @beta
// @beta @input
export interface LatestMapArguments<T, Keys extends string | number = string | number> {
local?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
[K in Keys]: JsonSerializable<T>;
};
settings?: BroadcastControlSettings | undefined;
}
Expand Down Expand Up @@ -183,7 +183,7 @@ export interface LatestMapRawEvents<T, K extends string | number> {
}) => void;
// @eventProperty
localItemUpdated: (updatedItem: {
value: DeepReadonly<JsonSerializable<T> & JsonDeserialized<T>>;
value: DeepReadonly<JsonSerializable<T>>;
key: K;
}) => void;
// @eventProperty
Expand All @@ -208,15 +208,15 @@ export interface LatestRaw<T> {
getRemotes(): IterableIterator<LatestClientData<T>>;
getStateAttendees(): Attendee[];
get local(): DeepReadonly<JsonDeserialized<T>>;
set local(value: JsonSerializable<T> & JsonDeserialized<T>);
set local(value: JsonSerializable<T>);
readonly presence: Presence;
}

// @beta @sealed
export interface LatestRawEvents<T> {
// @eventProperty
localUpdated: (update: {
value: DeepReadonly<JsonSerializable<T> & JsonDeserialized<T>>;
value: DeepReadonly<JsonSerializable<T>>;
}) => void;
// @eventProperty
remoteUpdated: (update: LatestClientData<T>) => void;
Expand Down Expand Up @@ -257,7 +257,7 @@ export interface StateMap<K extends string | number, V> {
get(key: K): DeepReadonly<JsonDeserialized<V>> | undefined;
has(key: K): boolean;
keys(): IterableIterator<K>;
set(key: K, value: JsonSerializable<V> & JsonDeserialized<V>): this;
set(key: K, value: JsonSerializable<V>): this;
readonly size: number;
}

Expand Down
29 changes: 22 additions & 7 deletions packages/framework/presence/src/exposedInternalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
* Licensed under the MIT License.
*/

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

/**
* Collection of value types that are not intended to be used/imported
Expand All @@ -26,17 +23,35 @@ export namespace InternalTypes {
}

/**
* Represents a state that may have a value.
* And it includes standard metadata.
*
* @remarks
* See {@link InternalTypes.ValueRequiredState}.
*
* @system
*/
export interface ValueOptionalState<TValue> extends ValueStateMetadata {
value?: JsonDeserialized<TValue>;
value?: OpaqueJsonDeserialized<TValue>;
}

/**
* Represents a state that must have a value.
* And it includes standard metadata.
*
* @remarks
* The value is wrapped in `OpaqueJsonDeserialized` as uses are expected
* to involve generic or unknown types that will be filtered. It is here
* mostly as a convenience to the many such uses that would otherwise
* need to specify some wrapper themselves.
*
* For known cases, construct a custom interface that extends
* {@link InternalTypes.ValueStateMetadata}.
*
* @system
*/
export interface ValueRequiredState<TValue> extends ValueStateMetadata {
value: JsonDeserialized<TValue>;
value: OpaqueJsonDeserialized<TValue>;
}

/**
Expand Down Expand Up @@ -121,6 +136,6 @@ export namespace InternalTypes {
*/
export interface NotificationType {
name: string;
args: (JsonSerializable<unknown> & JsonDeserialized<unknown>)[];
args: unknown[];
}
}
22 changes: 10 additions & 12 deletions packages/framework/presence/src/exposedUtilityTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,16 @@ import type {
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace InternalUtilityTypes {
/**
* `true` iff the given type is an acceptable shape for a notification.
* Yields `IfListener` when the given type is an acceptable shape for a notification.
* `Else` otherwise.
*
* @system
*/
export type IsNotificationListener<Event> = Event extends (...args: infer P) => void
? CoreInternalUtilityTypes.IfSameType<
P,
JsonSerializable<P> & JsonDeserialized<P>,
true,
false
>
: false;
export type IfNotificationListener<Event, IfListener, Else> = Event extends (
...args: infer P
) => void
? CoreInternalUtilityTypes.IfSameType<P, JsonSerializable<P>, IfListener, Else>
: Else;

/**
* Used to specify the kinds of notifications emitted by a {@link NotificationListenable}.
Expand All @@ -52,15 +50,15 @@ export namespace InternalUtilityTypes {
* @system
*/
export type NotificationListeners<E> = {
[P in string & keyof E as IsNotificationListener<E[P]> extends true ? P : never]: E[P];
[P in keyof E as IfNotificationListener<E[P], P, never>]: E[P];
};

/**
* {@link @fluidframework/core-interfaces#JsonDeserialized} version of the parameters of a function.
*
* @system
*/
export type JsonDeserializedParameters<T extends (...args: any) => any> = T extends (
export type JsonDeserializedParameters<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any
? JsonDeserialized<P>
Expand All @@ -71,7 +69,7 @@ export namespace InternalUtilityTypes {
*
* @system
*/
export type JsonSerializableParameters<T extends (...args: any) => any> = T extends (
export type JsonSerializableParameters<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any
? JsonSerializable<P>
Expand Down
57 changes: 55 additions & 2 deletions packages/framework/presence/src/internalUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
* Licensed under the MIT License.
*/

import type { DeepReadonly } from "@fluidframework/core-interfaces/internal";
import type {
DeepReadonly,
JsonDeserialized,
JsonSerializable,
OpaqueJsonDeserialized,
OpaqueJsonSerializable,
} from "@fluidframework/core-interfaces/internal";

/**
* Returns union of types of values in a record.
Expand Down Expand Up @@ -75,8 +81,55 @@ export function getOrCreateRecord<const K extends string | number | symbol, cons
}

/**
* Do nothing helper to apply deep immutability to a value's type.
* No-runtime-effect helper to apply deep immutability to a value's type.
*/
export function asDeeplyReadonly<T>(value: T): DeepReadonly<T> {
return value as DeepReadonly<T>;
}

export function asDeeplyReadonlyDeserializedJson<T>(
value: OpaqueJsonDeserialized<T>,
): DeepReadonly<JsonDeserialized<T>>;
export function asDeeplyReadonlyDeserializedJson<T>(
value: OpaqueJsonDeserialized<T> | undefined,
): DeepReadonly<JsonDeserialized<T>> | undefined;
/**
* No-runtime-effect helper to apply deep immutability to a value's opaque JSON
* type, revealing the JSON type.
*/
export function asDeeplyReadonlyDeserializedJson<T>(
value: OpaqueJsonDeserialized<T> | undefined,
): DeepReadonly<JsonDeserialized<T>> | undefined {
return value as DeepReadonly<JsonDeserialized<T>> | undefined;
}

type RevealOpaqueJsonDeserialized<T> = T extends OpaqueJsonDeserialized<infer U>
? JsonDeserialized<U>
: { [Key in keyof T]: RevealOpaqueJsonDeserialized<T[Key]> };

/**
* No-runtime-effect helper to reveal the JSON type from a value's opaque JSON
* types throughout a structure.
*
* @remarks
* {@link OpaqueJsonDeserialized} instances will be replaced shallowly such
* that nested instances are retained.
*/
export function revealOpaqueJson<T>(value: T): RevealOpaqueJsonDeserialized<T> {
return value as RevealOpaqueJsonDeserialized<T>;
}

/**
* No-runtime-effect helper to automatically cast JSON type to Opaque JSON type
* at outermost scope.
*
* @remarks
* Types that satisfy {@link JsonSerializable} may also be deserialized. Thus,
* the return type is both {@link OpaqueJsonSerializable} and
* {@link OpaqueJsonDeserialized}.
*/
export function toOpaqueJson<const T>(
value: JsonSerializable<T>,
): OpaqueJsonSerializable<T> & OpaqueJsonDeserialized<T> {
return value as OpaqueJsonSerializable<T> & OpaqueJsonDeserialized<T>;
}
Loading
Loading