Skip to content

Commit

Permalink
feat(presence): Raise events for presence notifications (microsoft#22950
Browse files Browse the repository at this point in the history
)

Events for presence notifications have been implemented. Some
type-related workarounds were needed, but all are documented inline.

Note that the emit.unicast function, used to send notifications to
specific clients, is not tested.

[AB#19706](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/19706)

---------

Co-authored-by: Jason Hartman <[email protected]>
  • Loading branch information
tylerbutler and jason-ha authored Nov 14, 2024
1 parent 9485e13 commit 761f539
Show file tree
Hide file tree
Showing 6 changed files with 543 additions and 88 deletions.
4 changes: 2 additions & 2 deletions packages/framework/presence/api-report/presence.alpha.api.md
Original file line number Diff line number Diff line change
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 = 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: Partial<NotificationSubscriptions<T>>): InternalTypes.ManagerFactory<Key, InternalTypes.ValueRequiredState<InternalTypes.NotificationType>, NotificationsManager<T>>;

// @alpha @sealed
export interface NotificationsManager<T extends InternalUtilityTypes.NotificationEvents<T>> {
Expand All @@ -165,7 +165,7 @@ export interface NotificationSubscribable<E extends InternalUtilityTypes.Notific

// @alpha @sealed
export type NotificationSubscriptions<E extends InternalUtilityTypes.NotificationEvents<E>> = {
[K in string & keyof InternalUtilityTypes.NotificationEvents<E>]: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonSerializableParameters<E[K]>) => void;
[K in string & keyof InternalUtilityTypes.NotificationEvents<E>]: (sender: ISessionClient, ...args: InternalUtilityTypes.JsonDeserializedParameters<E[K]>) => void;
};

// @alpha @sealed (undocumented)
Expand Down
1 change: 0 additions & 1 deletion packages/framework/presence/src/latestMapValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ export interface LatestMapValueManager<T, Keys extends string | number = string
readonly local: ValueMap<Keys, T>;
/**
* Iterable access to remote clients' map of values.
* @remarks This is not yet implemented.
*/
clientValues(): IterableIterator<LatestMapValueClientData<T, Keys>>;
/**
Expand Down
1 change: 0 additions & 1 deletion packages/framework/presence/src/latestValueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export interface LatestValueManager<T> {

/**
* Iterable access to remote clients' values.
* @remarks This is not yet implemented.
*/
clientValues(): IterableIterator<LatestValueClientData<T>>;
/**
Expand Down
54 changes: 46 additions & 8 deletions packages/framework/presence/src/notificationsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ISessionClient } from "./presence.js";
import { datastoreFromHandle, type StateDatastore } from "./stateDatastore.js";
import { brandIVM } from "./valueManager.js";

import type { ISubscribable } from "@fluid-experimental/presence/internal/events";
import type { Events, ISubscribable } from "@fluid-experimental/presence/internal/events";
import { createEmitter } from "@fluid-experimental/presence/internal/events";
import type { InternalTypes } from "@fluid-experimental/presence/internal/exposedInternalTypes";
import type { InternalUtilityTypes } from "@fluid-experimental/presence/internal/exposedUtilityTypes";
Expand Down Expand Up @@ -65,7 +65,7 @@ export interface NotificationSubscribable<
export type NotificationSubscriptions<E extends InternalUtilityTypes.NotificationEvents<E>> = {
[K in string & keyof InternalUtilityTypes.NotificationEvents<E>]: (
sender: ISessionClient,
...args: InternalUtilityTypes.JsonSerializableParameters<E[K]>
...args: InternalUtilityTypes.JsonDeserializedParameters<E[K]>
) => void;
};

Expand Down Expand Up @@ -125,6 +125,12 @@ export interface NotificationsManager<T extends InternalUtilityTypes.Notificatio
readonly notifications: NotificationSubscribable<T>;
}

/**
* Object.keys retyped to support specific records keys and
* branded string-based keys.
*/
const recordKeys = Object.keys as <K extends string>(o: Partial<Record<K, unknown>>) => K[];

class NotificationsManagerImpl<
T extends InternalUtilityTypes.NotificationEvents<T>,
Key extends string,
Expand Down Expand Up @@ -156,26 +162,58 @@ class NotificationsManagerImpl<
},
};

// @ts-expect-error TODO
public readonly notifications: NotificationSubscribable<T> =
// Workaround for types
private readonly notificationsInternal =
// @ts-expect-error TODO
createEmitter<NotificationSubscriptions<T>>();

// @ts-expect-error TODO
public readonly notifications: NotificationSubscribable<T> = this.notificationsInternal;

public constructor(
private readonly key: Key,
private readonly datastore: StateDatastore<
Key,
InternalTypes.ValueRequiredState<InternalTypes.NotificationType>
>,
_initialSubscriptions: Partial<NotificationSubscriptions<T>>,
) {}
initialSubscriptions: Partial<NotificationSubscriptions<T>>,
) {
// Add event listeners provided at instantiation
for (const subscriptionName of recordKeys(initialSubscriptions)) {
// Lingering Event typing issues with Notifications specialization requires
// this cast. The only thing that really matters is that name is a string.
const name = subscriptionName as keyof Events<NotificationSubscriptions<T>>;
const value = initialSubscriptions[subscriptionName];
// This check should not be needed while using exactOptionalPropertyTypes, but
// typescript appears to ignore that with Partial<>. Good to be defensive
// against callers sending `undefined` anyway.
if (value !== undefined) {
this.notificationsInternal.on(name, value);
}
}
}

public update(
client: ISessionClient,
_received: number,
value: InternalTypes.ValueRequiredState<InternalTypes.NotificationType>,
): void {
this.events.emit("unattendedNotification", value.value.name, client, ...value.value.args);
const eventName = value.value.name as keyof Events<NotificationSubscriptions<T>>;
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 = [client, ...value.value.args] as Parameters<
NotificationSubscriptions<T>[typeof eventName]
>;
this.notificationsInternal.emit(eventName, ...args);
} else {
this.events.emit(
"unattendedNotification",
value.value.name,
client,
...value.value.args,
);
}
}
}

Expand All @@ -192,7 +230,7 @@ export function Notifications<
T extends InternalUtilityTypes.NotificationEvents<T>,
Key extends string = string,
>(
initialSubscriptions: NotificationSubscriptions<T>,
initialSubscriptions: Partial<NotificationSubscriptions<T>>,
): InternalTypes.ManagerFactory<
Key,
InternalTypes.ValueRequiredState<InternalTypes.NotificationType>,
Expand Down
Loading

0 comments on commit 761f539

Please sign in to comment.