Skip to content

Commit

Permalink
fix(client-presence): clientValues impl+ (microsoft#22518)
Browse files Browse the repository at this point in the history
- add implementation for Latest*.clientValues
   - relocate brandedObjectEntries helper
- API changes:
- correct LatestMapValueManager.clientValue to just return the value map
(caller already has the client)
- push manager factory `Key` generics to last and give default of
`string`

- Additionally, comment out speculative, unused StateDatastoreSchema
  • Loading branch information
jason-ha authored Sep 16, 2024
1 parent 3cd6148 commit 92d35f9
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 46 deletions.
8 changes: 4 additions & 4 deletions packages/framework/presence/api-report/presence.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ export interface ISessionClient<SpecificSessionClientId extends ClientSessionId
}

// @alpha
export function Latest<T extends object, Key extends string>(initialValue: JsonSerializable<T> & JsonDeserialized<T> & object, controls?: LatestValueControls): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, LatestValueManager<T>>;
export function Latest<T extends object, Key extends string = string>(initialValue: JsonSerializable<T> & JsonDeserialized<T> & object, controls?: LatestValueControls): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<T>, LatestValueManager<T>>;

// @alpha
export function LatestMap<T extends object, RegistrationKey extends string, Keys extends string | number = string | number>(initialValues?: {
export function LatestMap<T extends object, Keys extends string | number = string | number, RegistrationKey extends string = string>(initialValues?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
}, controls?: LatestValueControls): InternalTypes.ManagerFactory<RegistrationKey, InternalTypes.MapValueState<T>, LatestMapValueManager<T, Keys>>;

Expand Down Expand Up @@ -76,7 +76,7 @@ export interface LatestMapValueClientData<T, Keys extends string | number, Speci
// @alpha @sealed
export interface LatestMapValueManager<T, Keys extends string | number = string | number> {
clients(): ISessionClient[];
clientValue<SpecificSessionClientId extends ClientSessionId>(client: ISessionClient<SpecificSessionClientId>): LatestMapValueClientData<T, Keys, SpecificSessionClientId>;
clientValue(client: ISessionClient): ReadonlyMap<Keys, LatestValueData<T>>;
clientValues(): IterableIterator<LatestMapValueClientData<T, Keys>>;
readonly controls: LatestValueControls;
readonly events: ISubscribable<LatestMapValueManagerEvents<T, Keys>>;
Expand Down Expand Up @@ -143,7 +143,7 @@ export interface NotificationEmitter<E extends InternalUtilityTypes.Notification
}

// @alpha
export function Notifications<T extends InternalUtilityTypes.NotificationEvents<T>, Key extends string>(initialSubscriptions: NotificationSubscriptions<T>): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<InternalTypes.NotificationType>, NotificationsManager<T>>;
export function Notifications<T extends InternalUtilityTypes.NotificationEvents<T>, Key extends string = string>(initialSubscriptions: NotificationSubscriptions<T>): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<InternalTypes.NotificationType>, NotificationsManager<T>>;

// @alpha @sealed
export interface NotificationsManager<T extends InternalUtilityTypes.NotificationEvents<T>> {
Expand Down
9 changes: 9 additions & 0 deletions packages/framework/presence/src/internalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ export interface ClientRecord<
[ClientSessionId: ClientSessionId]: Exclude<TValue, undefined>;
}

/**
* Object.entries retyped to support branded string-based keys.
*
* @internal
*/
export const brandedObjectEntries = Object.entries as <K extends string, T>(
o: Record<K, T>,
) => [K, T][];

/**
* @internal
*/
Expand Down
25 changes: 14 additions & 11 deletions packages/framework/presence/src/latestMapValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,7 @@ export interface LatestMapValueManager<T, Keys extends string | number = string
/**
* Access to a specific client's map of values.
*/
clientValue<SpecificSessionClientId extends ClientSessionId>(
client: ISessionClient<SpecificSessionClientId>,
): LatestMapValueClientData<T, Keys, SpecificSessionClientId>;
clientValue(client: ISessionClient): ReadonlyMap<Keys, LatestValueData<T>>;
}

class LatestMapValueManagerImpl<
Expand Down Expand Up @@ -343,8 +341,15 @@ class LatestMapValueManagerImpl<

public readonly local: ValueMap<Keys, T>;

public clientValues(): IterableIterator<LatestMapValueClientData<T, Keys>> {
throw new Error("Method not implemented.");
public *clientValues(): IterableIterator<LatestMapValueClientData<T, Keys>> {
const allKnownStates = this.datastore.knownValues(this.key);
for (const clientSessionId of Object.keys(allKnownStates.states)) {
if (clientSessionId !== allKnownStates.self) {
const client = this.datastore.lookupClient(clientSessionId);
const items = this.clientValue(client);
yield { client, items };
}
}
}

public clients(): ISessionClient[] {
Expand All @@ -354,11 +359,9 @@ class LatestMapValueManagerImpl<
.map((clientSessionId) => this.datastore.lookupClient(clientSessionId));
}

public clientValue<SpecificSessionClientId extends ClientSessionId>(
client: SpecificSessionClient<SpecificSessionClientId>,
): LatestMapValueClientData<T, Keys, SpecificSessionClientId> {
public clientValue(client: ISessionClient): ReadonlyMap<Keys, LatestValueData<T>> {
const allKnownStates = this.datastore.knownValues(this.key);
const clientSessionId: SpecificSessionClientId = client.sessionId;
const clientSessionId = client.sessionId;
if (!(clientSessionId in allKnownStates.states)) {
throw new Error("No entry for client");
}
Expand All @@ -373,7 +376,7 @@ class LatestMapValueManagerImpl<
});
}
}
return { client, items };
return items;
}

public update<SpecificSessionClientId extends ClientSessionId>(
Expand Down Expand Up @@ -441,8 +444,8 @@ class LatestMapValueManagerImpl<
*/
export function LatestMap<
T extends object,
RegistrationKey extends string,
Keys extends string | number = string | number,
RegistrationKey extends string = string,
>(
initialValues?: {
[K in Keys]: JsonSerializable<T> & JsonDeserialized<T>;
Expand Down
16 changes: 13 additions & 3 deletions packages/framework/presence/src/latestValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import type { ValueManager } from "./internalTypes.js";
import { brandedObjectEntries } from "./internalTypes.js";
import type { LatestValueControls } from "./latestValueControls.js";
import { LatestValueControl } from "./latestValueControls.js";
import type { LatestValueClientData, LatestValueData } from "./latestValueTypes.js";
Expand Down Expand Up @@ -105,8 +106,17 @@ class LatestValueManagerImpl<T, Key extends string>
this.datastore.localUpdate(this.key, this.value, /* forceUpdate */ false);
}

public clientValues(): IterableIterator<LatestValueClientData<T>> {
throw new Error("Method not implemented.");
public *clientValues(): IterableIterator<LatestValueClientData<T>> {
const allKnownStates = this.datastore.knownValues(this.key);
for (const [clientSessionId, value] of brandedObjectEntries(allKnownStates.states)) {
if (clientSessionId !== allKnownStates.self) {
yield {
client: this.datastore.lookupClient(clientSessionId),
value: value.value,
metadata: { revision: value.rev, timestamp: value.timestamp },
};
}
}
}

public clients(): ISessionClient[] {
Expand Down Expand Up @@ -153,7 +163,7 @@ class LatestValueManagerImpl<T, Key extends string>
*
* @alpha
*/
export function Latest<T extends object, Key extends string>(
export function Latest<T extends object, Key extends string = string>(
initialValue: JsonSerializable<T> & JsonDeserialized<T> & object,
controls?: LatestValueControls,
): InternalTypes.ManagerFactory<
Expand Down
8 changes: 6 additions & 2 deletions packages/framework/presence/src/notificationsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class NotificationsManagerImpl<
Key,
InternalTypes.ValueRequiredState<InternalTypes.NotificationType>
>,
_initialSubscriptions: NotificationSubscriptions<T>,
_initialSubscriptions: Partial<NotificationSubscriptions<T>>,
) {}

public update(
Expand All @@ -182,11 +182,15 @@ class NotificationsManagerImpl<
/**
* Factory for creating a {@link NotificationsManager}.
*
* @remarks
* Typescript inference for `Notifications` is not working correctly yet.
* Explicitly specify generics to make result types usable.
*
* @alpha
*/
export function Notifications<
T extends InternalUtilityTypes.NotificationEvents<T>,
Key extends string,
Key extends string = string,
>(
initialSubscriptions: NotificationSubscriptions<T>,
): InternalTypes.ManagerFactory<
Expand Down
8 changes: 1 addition & 7 deletions packages/framework/presence/src/presenceStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { assert } from "@fluidframework/core-utils/internal";
import type { ClientConnectionId } from "./baseTypes.js";
import type { InternalTypes } from "./exposedInternalTypes.js";
import type { ClientRecord } from "./internalTypes.js";
import { brandedObjectEntries } from "./internalTypes.js";
import type { ClientSessionId, ISessionClient } from "./presence.js";
import { handleFromDatastore, type StateDatastore } from "./stateDatastore.js";
import type { PresenceStates, PresenceStatesMethods, PresenceStatesSchema } from "./types.js";
Expand Down Expand Up @@ -192,13 +193,6 @@ export function mergeUntrackedDatastore(
}
}

/**
* Object.entries retyped to support branded string-based keys.
*/
const brandedObjectEntries = Object.entries as <K extends string, T>(
o: Record<K, T>,
) => [K, T][];

class PresenceStatesImpl<TSchema extends PresenceStatesSchema>
implements
PresenceStatesInternal,
Expand Down
20 changes: 10 additions & 10 deletions packages/framework/presence/src/stateDatastore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ import type { ClientSessionId, ISessionClient } from "./presence.js";
// TValue extends InternalTypes.ValueDirectoryOrState<any> = InternalTypes.ValueDirectoryOrState<unknown>,
// > = TValue extends InternalTypes.ValueDirectoryOrState<infer T> ? InternalTypes.ValueDirectoryOrState<T> : never;

/**
* @internal
*/
export interface StateDatastoreSchema {
// This type is not precise. It may
// need to be replaced with PresenceStates schema pattern
// similar to what is commented out.
[key: string]: InternalTypes.ValueDirectoryOrState<unknown>;
// [key: string]: StateDatastoreSchemaNode;
}
// /**
// * @internal
// */
// export interface StateDatastoreSchema {
// // This type is not precise. It may
// // need to be replaced with PresenceStates schema pattern
// // similar to what is commented out.
// [key: string]: InternalTypes.ValueDirectoryOrState<unknown>;
// // [key: string]: StateDatastoreSchemaNode;
// }

/**
* @internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function checkCompiles(): void {
tilt?: number;
}

map.add("pointers", LatestMap<PointerData, "pointers">({}));
map.add("pointers", LatestMap<PointerData>({}));

const pointers = map.pointers;
const localPointers = pointers.local;
Expand All @@ -75,8 +75,8 @@ export function checkCompiles(): void {
pointerItemUpdatedOff();

for (const client of pointers.clients()) {
const clientData = pointers.clientValue(client);
for (const [key, { value }] of clientData.items.entries()) {
const items = pointers.clientValue(client);
for (const [key, { value }] of items.entries()) {
logClientValue({ client, key, value });
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@ export function checkCompiles(): void {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const presence = {} as IPresence;
const notificationsWorkspace = presence.getNotifications("name:testNotificationWorkspace", {
chat: Notifications<
{
msg: (message: string) => void;
},
string
>({
chat: Notifications<{
msg: (message: string) => void;
}>({
msg: (client: ISessionClient, message: string) => {
console.log(`${client.sessionId} says, "${message}"`);
},
Expand Down

0 comments on commit 92d35f9

Please sign in to comment.