Skip to content

refactor(presence): Use branded JsonDeserialized type internally #24641

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

Closed
wants to merge 81 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
164b448
typebrand the JsonDeserialized type
tylerbutler May 15, 2025
0b1bd66
updates
tylerbutler May 15, 2025
b6eca1f
compiles
tylerbutler May 15, 2025
2ba11eb
Remove T &
tylerbutler May 15, 2025
d6b85e7
feedback
tylerbutler May 16, 2025
e716480
rename types to behopefully clearer
tylerbutler May 16, 2025
d727ad0
feedback
tylerbutler May 16, 2025
653320a
cleanup
tylerbutler May 16, 2025
1c54887
Merge branch 'main' into presence-type-branding
tylerbutler May 28, 2025
90063a9
cleanup
tylerbutler May 28, 2025
6d571db
fix
tylerbutler May 28, 2025
af6b5a8
Merge branch 'main' into presence-type-branding
tylerbutler May 28, 2025
6cd5054
rm invalid docs changes
tylerbutler May 28, 2025
019df60
formatting
tylerbutler May 28, 2025
212c77f
tsignore some errors
tylerbutler May 28, 2025
0e6c144
wip
tylerbutler May 28, 2025
4f12474
wip
tylerbutler May 28, 2025
c9cdf28
Merge branch 'main' into presence-type-branding
tylerbutler May 28, 2025
3d1435f
wip
tylerbutler May 28, 2025
40483c0
formatting
tylerbutler May 29, 2025
a00e294
feat(client-core-interfaces): opaque JSON placeholder types
jason-ha May 27, 2025
e82eb79
wip
tylerbutler May 29, 2025
c1d1f3f
Merge branch 'core/opaque-json-types' into presence-type-branding
tylerbutler May 29, 2025
fb60bcf
revert container-runtime-defs changes
tylerbutler May 29, 2025
34011f4
8 errors in 3 files
tylerbutler May 30, 2025
4cc3372
rename functions
tylerbutler May 30, 2025
0a52598
8 errors, 3 files (still)
tylerbutler May 30, 2025
9043b6c
rename and reorg
tylerbutler May 30, 2025
f2aa271
Merge branch 'main' into presence-type-branding
tylerbutler Jun 2, 2025
31a3478
improvement(client-core-interfaces): list OpaqueJsonSerializable as d…
jason-ha Jun 2, 2025
abe20ee
workarounds
tylerbutler Jun 2, 2025
dbf0696
improvement(client-presence): use opaque json internally
jason-ha May 30, 2025
502e103
Merge commit 'dbf0696243' into presence-type-branding
tylerbutler Jun 2, 2025
bc2ba69
source compiles but tests don't
tylerbutler Jun 2, 2025
1cf182c
source compiles
tylerbutler Jun 2, 2025
a5a0947
improvement(client-presence): reduce data in requirement to JsonSeria…
jason-ha Jun 2, 2025
c04d35a
revert
tylerbutler Jun 2, 2025
823262c
Merge branch 'core/promote-OpaqueJsonSerializable-for-degenerate-case…
tylerbutler Jun 2, 2025
37eb489
test(client-presence): updates for Opaque Json use
jason-ha Jun 2, 2025
90bf2a2
docs(client-presence): update API reports
jason-ha Jun 3, 2025
3d3c0c8
test(client-presence): fix for JsonSerializable change
jason-ha Jun 3, 2025
674cd09
Merge branch 'presence/use-opaque-json' into presence-type-branding
tylerbutler Jun 3, 2025
d38d500
use ts-expect-error instead of as any
tylerbutler Jun 3, 2025
99aa26a
Merge branch 'core/promote-OpaqueJsonSerializable-for-degenerate-case…
tylerbutler Jun 3, 2025
6f966b4
remove ts-expect-error that is no longer triggered
tylerbutler Jun 3, 2025
08ff94d
merge: 'main' into presence/use-opaque-json
jason-ha Jun 3, 2025
24537dd
test(client-presence): correction after merge
jason-ha Jun 3, 2025
fa24bfd
Merge branch 'main' into presence-type-branding
tylerbutler Jun 3, 2025
2aab49e
fix(client-presence): workaround "system:presence" incompatilities
jason-ha Jun 3, 2025
742981e
Merge branch 'presence/use-opaque-json' into presence-type-branding
tylerbutler Jun 3, 2025
d65c189
test-case ts-expect-error
tylerbutler Jun 3, 2025
28ff0cb
core-interfaces beta/system
tylerbutler Jun 3, 2025
5c68d8b
presence beta/system
tylerbutler Jun 3, 2025
6d8059f
Revert "fix(client-presence): workaround "system:presence" incompatil…
jason-ha Jun 3, 2025
db2039f
rename and cleanup
tylerbutler Jun 3, 2025
f34296d
use new to/from opaque function names
tylerbutler Jun 3, 2025
8d51f71
remove beta flags in types where possible
tylerbutler Jun 3, 2025
9fa9a7c
add back beta
tylerbutler Jun 4, 2025
0650353
imports
tylerbutler Jun 4, 2025
667aebc
fix(client-presence): remove opacity from system datastore
jason-ha Jun 4, 2025
3a86482
fix(client-core-interfaces): support Opaque unknown thru JsonSerializ…
jason-ha Jun 4, 2025
eee8245
fix(client-presence): separate system:presence index from others
jason-ha Jun 4, 2025
6ba5d8c
fix(client-core-interfaces): support Opaque unknown thru JsonSerializ…
jason-ha Jun 4, 2025
e101659
improvement(client-presence): separate system:presence index from others
jason-ha Jun 4, 2025
dd026c0
Merge branch 'core/fix-Opaque-unknown-serializability' into presence-…
tylerbutler Jun 4, 2025
cdd1e04
Merge branch 'presence/improvement-separate-system_presence-from-gene…
tylerbutler Jun 4, 2025
a0d5fd8
wip
tylerbutler Jun 4, 2025
c320220
Merge branch 'main' into presence-type-branding
tylerbutler Jun 4, 2025
90cafd3
fix
tylerbutler Jun 4, 2025
a2dfa90
Merge branch 'presence/use-opaque-json' into presence-type-branding
tylerbutler Jun 4, 2025
6ffdcb4
merge: 'main' into presence/use-opaque-json
jason-ha Jun 4, 2025
c140b04
wip
tylerbutler Jun 4, 2025
8f22012
Merge branch 'presence/use-opaque-json' into presence-type-branding
tylerbutler Jun 4, 2025
77ce46f
cleanup
tylerbutler Jun 4, 2025
a1aa8c8
possible fix
tylerbutler Jun 4, 2025
a7a59a4
updates
tylerbutler Jun 4, 2025
c619299
docs(client-presence): comment additions
jason-ha Jun 4, 2025
cad419d
refactor(client-presence): rename helpers
jason-ha Jun 4, 2025
ab2d5a0
Merge branch 'presence/use-opaque-json' into presence-type-branding
tylerbutler Jun 5, 2025
b6da44a
revert(client-presence): unneeded code changes
jason-ha Jun 6, 2025
9764eaa
Merge branch 'presence/use-opaque-json' into presence-type-branding
tylerbutler Jun 7, 2025
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
14 changes: 12 additions & 2 deletions packages/framework/presence/api-report/presence.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ export namespace InternalTypes {
// @system (undocumented)
export interface ValueOptionalState<TValue> extends ValueStateMetadata {
// (undocumented)
value?: JsonDeserialized<TValue>;
value?: InternalUtilityTypes.JsonDeserializedHandle<TValue>;
}
// @system (undocumented)
export interface ValueRequiredState<TValue> extends ValueStateMetadata {
// (undocumented)
value: JsonDeserialized<TValue>;
value: InternalUtilityTypes.JsonDeserializedHandle<TValue>;
}
// @system (undocumented)
export interface ValueStateMetadata {
Expand All @@ -129,8 +129,18 @@ export namespace InternalTypes {
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;
// @system (undocumented)
export class JsonDeserializedBrand<T> {
}
// @system (undocumented)
export type JsonDeserializedHandle<T> = T & JsonDeserializedBrand<T>;
// @system
export type JsonDeserializedParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? JsonDeserialized<P> : never;
// @system (undocumented)
export class JsonSerializableBrand<T> {
}
// @system (undocumented)
export type JsonSerializableHandle<T> = T & JsonSerializableBrand<T>;
// @system
export type JsonSerializableParameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? JsonSerializable<P> : never;
// @system
Expand Down
6 changes: 4 additions & 2 deletions packages/framework/presence/src/exposedInternalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
JsonSerializable,
} from "@fluidframework/core-interfaces/internal/exposedUtilityTypes";

import type { InternalUtilityTypes } from "./exposedUtilityTypes.js";

/**
* Collection of value types that are not intended to be used/imported
* directly outside of this package.
Expand All @@ -29,14 +31,14 @@ export namespace InternalTypes {
* @system
*/
export interface ValueOptionalState<TValue> extends ValueStateMetadata {
value?: JsonDeserialized<TValue>;
value?: InternalUtilityTypes.JsonDeserializedHandle<TValue>;
}

/**
* @system
*/
export interface ValueRequiredState<TValue> extends ValueStateMetadata {
value: JsonDeserialized<TValue>;
value: InternalUtilityTypes.JsonDeserializedHandle<TValue>;
}

/**
Expand Down
48 changes: 48 additions & 0 deletions packages/framework/presence/src/exposedUtilityTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,52 @@ export namespace InternalUtilityTypes {
) => any
? JsonSerializable<P>
: never;

/**
* @system
*/
export declare class JsonDeserializedBrand<T> {
private readonly JsonDeserialized: JsonDeserialized<T>;
}

/**
* @system
*/
// export type JsonDeserializedHandle<T> = Tagged<JsonDeserialized<T>, "JsonDeserialized">;
export type JsonDeserializedHandle<T> = T & JsonDeserializedBrand<T>;

/**
* @system
*/
export declare class JsonSerializableBrand<T> {
private readonly JsonSerializable: JsonSerializable<T>;
}

/**
* @system
*/
// export type JsonDeserializedHandle<T> = Tagged<JsonDeserialized<T>, "JsonDeserialized">;
export type JsonSerializableHandle<T> = T & JsonSerializableBrand<T>;
}

/**
* Cast a JsonDeserialized value to its branded version.
*
* @system
*/
export function toJsonDeserializedHandle<T>(
value: JsonDeserialized<T>,
): InternalUtilityTypes.JsonDeserializedHandle<T> {
return value as InternalUtilityTypes.JsonDeserializedHandle<T>;
}

/**
* Cast a branded JsonDeserialized value back to its unbranded version.
*
* @system
*/
export function fromJsonDeserializedHandle<T>(
value: InternalUtilityTypes.JsonDeserializedHandle<T>,
): JsonDeserialized<T> {
return value as JsonDeserialized<T>;
}
23 changes: 15 additions & 8 deletions packages/framework/presence/src/latestMapValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
fromJsonDeserializedHandle,
toJsonDeserializedHandle,
// 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";
Expand Down Expand Up @@ -266,22 +271,24 @@ class ValueMapImpl<T, K extends string | number> implements StateMap<K, T> {
): void {
for (const [key, item] of objectEntries(this.value.items)) {
if (item.value !== undefined) {
callbackfn(asDeeplyReadonly(item.value), key, this);
const unbrandedValue = fromJsonDeserializedHandle(item.value);
callbackfn(asDeeplyReadonly(unbrandedValue), key, this);
}
}
}
public get(key: K): DeepReadonly<JsonDeserialized<T>> | undefined {
return asDeeplyReadonly(this.value.items[key]?.value);
const data = this.value.items[key]?.value;
return data === undefined ? undefined : asDeeplyReadonly(fromJsonDeserializedHandle(data));
}
public has(key: K): boolean {
return this.value.items[key]?.value !== undefined;
}
public set(key: K, value: JsonSerializable<T> & JsonDeserialized<T>): 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: toJsonDeserializedHandle(value) };
}
this.updateItem(key, value);
this.updateItem(key, toJsonDeserializedHandle(value));
this.emitter.emit("localItemUpdated", { key, value: asDeeplyReadonly(value) });
return this;
}
Expand Down Expand Up @@ -413,7 +420,7 @@ class LatestMapRawValueManagerImpl<
const value = item.value;
if (value !== undefined) {
items.set(key, {
value: asDeeplyReadonly(value),
value: asDeeplyReadonly(fromJsonDeserializedHandle(value)),
metadata: { revision: item.rev, timestamp: item.timestamp },
});
}
Expand Down Expand Up @@ -464,13 +471,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 = asDeeplyReadonly(fromJsonDeserializedHandle(item.value));
const updatedItem = {
attendee,
key,
value: itemValue,
metadata,
};
} satisfies LatestMapItemUpdatedClientData<T, Keys>;
postUpdateActions.push(() => this.events.emit("remoteItemUpdated", updatedItem));
allUpdates.items.set(key, { value: itemValue, metadata });
} else if (hadPriorValue !== undefined) {
Expand Down Expand Up @@ -539,7 +546,7 @@ export function latestMap<
value.items[key] = {
rev: 0,
timestamp,
value: initialValues[key],
value: toJsonDeserializedHandle(initialValues[key]),
};
}
}
Expand Down
24 changes: 18 additions & 6 deletions packages/framework/presence/src/latestValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ 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 type { InternalUtilityTypes } from "./exposedUtilityTypes.js";
import {
fromJsonDeserializedHandle,
toJsonDeserializedHandle,
} from "./exposedUtilityTypes.js";
import type { PostUpdateAction, ValueManager } from "./internalTypes.js";
import { asDeeplyReadonly, objectEntries } from "./internalUtils.js";
import type { LatestClientData, LatestData } from "./latestValueTypes.js";
Expand Down Expand Up @@ -112,13 +117,13 @@ class LatestValueManagerImpl<T, Key extends string>
}

public get local(): DeepReadonly<JsonDeserialized<T>> {
return asDeeplyReadonly(this.value.value);
return asDeeplyReadonly(fromJsonDeserializedHandle(this.value.value));
}

public set local(value: JsonSerializable<T> & JsonDeserialized<T>) {
this.value.rev += 1;
this.value.timestamp = Date.now();
this.value.value = value;
this.value.value = toJsonDeserializedHandle(value);
this.datastore.localUpdate(this.key, this.value, {
allowableUpdateLatencyMs: this.controls.allowableUpdateLatencyMs,
});
Expand All @@ -132,7 +137,7 @@ class LatestValueManagerImpl<T, Key extends string>
if (attendeeId !== allKnownStates.self) {
yield {
attendee: this.datastore.lookupClient(attendeeId),
value: asDeeplyReadonly(value.value),
value: asDeeplyReadonly(fromJsonDeserializedHandle(value.value)),
metadata: { revision: value.rev, timestamp: value.timestamp },
};
}
Expand All @@ -153,7 +158,7 @@ class LatestValueManagerImpl<T, Key extends string>
throw new Error("No entry for clientId");
}
return {
value: asDeeplyReadonly(clientState.value),
value: asDeeplyReadonly(fromJsonDeserializedHandle(clientState.value)),
metadata: { revision: clientState.rev, timestamp: Date.now() },
};
}
Expand All @@ -174,7 +179,7 @@ class LatestValueManagerImpl<T, Key extends string>
() =>
this.events.emit("remoteUpdated", {
attendee,
value: asDeeplyReadonly(value.value),
value: asDeeplyReadonly(fromJsonDeserializedHandle(value.value)),
metadata: { revision: value.rev, timestamp: value.timestamp },
}),
];
Expand Down Expand Up @@ -211,10 +216,17 @@ export function latest<T extends object | null, Key extends string = string>(

// Latest takes ownership of the initial local value but makes a shallow
// copy for basic protection.
const internalValue =
local === null
? (local as InternalUtilityTypes.JsonDeserializedHandle<T>)
: // FIXME: Why isn't this directly castable?
(shallowCloneObject(
local,
) as unknown as InternalUtilityTypes.JsonDeserializedHandle<T>);
const value: InternalTypes.ValueRequiredState<T> = {
rev: 0,
timestamp: Date.now(),
value: local === null ? local : shallowCloneObject(local),
value: internalValue,
};
const factory = (
key: Key,
Expand Down
12 changes: 10 additions & 2 deletions packages/framework/presence/src/notificationsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,11 @@ class NotificationsManagerImpl<
{
rev: 0,
timestamp: 0,
value: { name, args: [...(args as JsonTypeWith<never>[])] },
value: {
name,
args: [...(args as JsonTypeWith<never>[])],
// FIXME: Why doesn't as cast work?
} as unknown as InternalUtilityTypes.JsonDeserializedHandle<InternalTypes.NotificationType>,
ignoreUnmonitored: true,
},
// This is a notification, so we want to send it immediately.
Expand All @@ -190,7 +194,11 @@ class NotificationsManagerImpl<
{
rev: 0,
timestamp: 0,
value: { name, args: [...(args as JsonTypeWith<never>[])] },
value: {
name,
args: [...(args as JsonTypeWith<never>[])],
// FIXME: Why doesn't as cast work?
} as unknown as InternalUtilityTypes.JsonDeserializedHandle<InternalTypes.NotificationType>,
ignoreUnmonitored: true,
},
// This is a notification, so we want to send it immediately.
Expand Down
3 changes: 2 additions & 1 deletion packages/framework/presence/src/systemWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { assert } from "@fluidframework/core-utils/internal";

import type { ClientConnectionId } from "./baseTypes.js";
import type { InternalTypes } from "./exposedInternalTypes.js";
import { toJsonDeserializedHandle } from "./exposedUtilityTypes.js";
import type { PostUpdateAction } from "./internalTypes.js";
import type { Attendee, AttendeesEvents, AttendeeId, Presence } from "./presence.js";
import { AttendeeStatus } from "./presence.js";
Expand Down Expand Up @@ -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: toJsonDeserializedHandle(this.selfAttendee.attendeeId),
};

// Mark 'Connected' remote attendees connections as stale
Expand Down
Loading