diff --git a/packages/framework/presence/src/events/events.ts b/packages/framework/presence/src/events/events.ts index b427b50c13c63..e8c9b2c822e2b 100644 --- a/packages/framework/presence/src/events/events.ts +++ b/packages/framework/presence/src/events/events.ts @@ -3,43 +3,8 @@ * Licensed under the MIT License. */ -/** - * This file is a clone of the `events.ts` file from the `@fluidframework/tree` package. - * `@public` APIs have been changed to `@alpha` and some lint defects from more strict rules - * have been fixed or suppressed. - */ - import type { IEvent } from "@fluidframework/core-interfaces"; -import { assert } from "@fluidframework/core-utils/internal"; - -function fail(message: string): never { - throw new Error(message); -} - -/** - * Retrieve a value from a map with the given key, or create a new entry if the key is not in the map. - * @param map - The map to query/update - * @param key - The key to lookup in the map - * @param defaultValue - a function which returns a default value. This is called and used to set an initial value for the given key in the map if none exists - * @returns either the existing value for the given key, or the newly-created value (the result of `defaultValue`) - */ -function getOrCreate(map: Map, key: K, defaultValue: (key: K) => V): V { - let value = map.get(key); - if (value === undefined) { - value = defaultValue(key); - map.set(key, value); - } - return value; -} - -/** - * Convert a union of types to an intersection of those types. Useful for `TransformEvents`. - */ -export type UnionToIntersection = (T extends any ? (k: T) => unknown : never) extends ( - k: infer U, -) => unknown - ? U - : never; +import type { UnionToIntersection } from "@fluidframework/core-utils"; /** * `true` iff the given type is an acceptable shape for an event @@ -122,208 +87,3 @@ export interface ISubscribable> { */ on>(eventName: K, listener: E[K]): () => void; } - -/** - * Interface for an event emitter that can emit typed events to subscribed listeners. - * @internal - */ -export interface IEmitter> { - /** - * Emits an event with the specified name and arguments, notifying all subscribers by calling their registered listener functions. - * @param eventName - the name of the event to fire - * @param args - the arguments passed to the event listener functions - */ - emit>(eventName: K, ...args: Parameters): void; - - /** - * Emits an event with the specified name and arguments, notifying all subscribers by calling their registered listener functions. - * It also collects the return values of all listeners into an array. - * - * Warning: This method should be used with caution. It deviates from the standard event-based integration pattern as creates substantial coupling between the emitter and its listeners. - * For the majority of use-cases it is recommended to use the standard {@link IEmitter.emit} functionality. - * @param eventName - the name of the event to fire - * @param args - the arguments passed to the event listener functions - * @returns An array of the return values of each listener, preserving the order listeners were called. - */ - emitAndCollect>( - eventName: K, - ...args: Parameters - ): ReturnType[]; -} - -/** - * Create an {@link ISubscribable} that can be instructed to emit events via the {@link IEmitter} interface. - * - * A class can delegate handling {@link ISubscribable} to the returned value while using it to emit the events. - * See also `EventEmitter` which be used as a base class to implement {@link ISubscribable} via extension. - * @internal - */ -export function createEmitter>( - noListeners?: NoListenersCallback, -): ISubscribable & IEmitter & HasListeners { - return new ComposableEventEmitter(noListeners); -} - -/** - * Called when the last listener for `eventName` is removed. - * Useful for determining when to clean up resources related to detecting when the event might occurs. - * @internal - */ -export type NoListenersCallback> = (eventName: keyof Events) => void; - -/** - * @internal - */ -export interface HasListeners> { - /** - * When no `eventName` is provided, returns true iff there are any listeners. - * - * When `eventName` is provided, returns true iff there are listeners for that event. - * - * @remarks - * This can be used to know when its safe to cleanup data-structures which only exist to fire events for their listeners. - */ - hasListeners(eventName?: keyof Events): boolean; -} - -/** - * Provides an API for subscribing to and listening to events. - * - * @remarks Classes wishing to emit events may either extend this class or compose over it. - * - * @example Extending this class - * - * ```typescript - * interface MyEvents { - * "loaded": () => void; - * } - * - * class MyClass extends EventEmitter { - * private load() { - * this.emit("loaded"); - * } - * } - * ``` - * - * @example Composing over this class - * - * ```typescript - * class MyClass implements ISubscribable { - * private readonly events = EventEmitter.create(); - * - * private load() { - * this.events.emit("loaded"); - * } - * - * public on(eventName: K, listener: MyEvents[K]): () => void { - * return this.events.on(eventName, listener); - * } - * } - * ``` - */ -export class EventEmitter> implements ISubscribable, HasListeners { - // TODO: because the inner data-structure here is a set, adding the same callback twice does not error, - // but only calls it once, and unsubscribing will stop calling it all together. - // This is surprising since it makes subscribing and unsubscribing not inverses (but instead both idempotent). - // This might be desired, but if so the documentation should indicate it. - private readonly listeners = new Map any>>(); - - // Because this is protected and not public, calling this externally (not from a subclass) makes sending events to the constructed instance impossible. - // Instead, use the static `create` function to get an instance which allows emitting events. - protected constructor(private readonly noListeners?: NoListenersCallback) {} - - protected emit>(eventName: K, ...args: Parameters): void { - const listeners = this.listeners.get(eventName); - if (listeners !== undefined) { - const argArray: unknown[] = args; // TODO: Current TS (4.5.5) cannot spread `args` into `listener()`, but future versions (e.g. 4.8.4) can. - // This explicitly copies listeners so that new listeners added during this call to emit will not receive this event. - for (const listener of listeners) { - // If listener has been unsubscribed while invoking other listeners, skip it. - if (listeners.has(listener)) { - listener(...argArray); - } - } - } - } - - protected emitAndCollect>( - eventName: K, - ...args: Parameters - ): ReturnType[] { - const listeners = this.listeners.get(eventName); - if (listeners !== undefined) { - const argArray: unknown[] = args; - const resultArray: ReturnType[] = []; - for (const listener of listeners.values()) { - // listner return type is any to enable this.listeners to be a Map - // of Sets rather than a Record with tracked (known) return types. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - resultArray.push(listener(...argArray)); - } - return resultArray; - } - return []; - } - - /** - * Register an event listener. - * @param eventName - the name of the event - * @param listener - the handler to run when the event is fired by the emitter - * @returns a function which will deregister the listener when run. - * This function will error if called more than once. - * @privateRemarks - * TODO: - * invoking the returned callback can error even if its only called once if the same listener was provided to two calls to "on". - * This behavior is not documented and its unclear if its a bug or not: see note on listeners. - */ - public on>(eventName: K, listener: E[K]): () => void { - getOrCreate(this.listeners, eventName, () => new Set()).add(listener); - return () => this.off(eventName, listener); - } - - private off>(eventName: K, listener: E[K]): void { - const listeners = - this.listeners.get(eventName) ?? - // TODO: consider making this (and assert below) a usage error since it can be triggered by users of the public API: maybe separate those use cases somehow? - fail("Event has no listeners. Event deregistration functions may only be invoked once."); - assert( - listeners.delete(listener), - 0xa30 /* Listener does not exist. Event deregistration functions may only be invoked once. */, - ); - if (listeners.size === 0) { - this.listeners.delete(eventName); - this.noListeners?.(eventName); - } - } - - public hasListeners(eventName?: keyof Events): boolean { - if (eventName === undefined) { - return this.listeners.size > 0; - } - return this.listeners.has(eventName); - } -} - -// This class exposes the constructor and the `emit` method of `EventEmitter`, elevating them from protected to public -class ComposableEventEmitter> - extends EventEmitter - implements IEmitter -{ - public constructor(noListeners?: NoListenersCallback) { - super(noListeners); - } - - public override emit>( - eventName: K, - ...args: Parameters - ): void { - return super.emit(eventName, ...args); - } - - public override emitAndCollect>( - eventName: K, - ...args: Parameters - ): ReturnType[] { - return super.emitAndCollect(eventName, ...args); - } -} diff --git a/packages/framework/presence/src/latestMapValueManager.ts b/packages/framework/presence/src/latestMapValueManager.ts index f7537c1ab035c..bc6b40333fbae 100644 --- a/packages/framework/presence/src/latestMapValueManager.ts +++ b/packages/framework/presence/src/latestMapValueManager.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. */ +import { createEmitter } from "@fluidframework/core-utils/internal"; + import type { ValueManager } from "./internalTypes.js"; import type { LatestValueControls } from "./latestValueControls.js"; import { LatestValueControl } from "./latestValueControls.js"; @@ -20,7 +22,6 @@ import type { JsonSerializable, } from "@fluid-experimental/presence/internal/core-interfaces"; import type { 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"; diff --git a/packages/framework/presence/src/latestValueManager.ts b/packages/framework/presence/src/latestValueManager.ts index 4f7e0d3d74f65..e9790e3532ff4 100644 --- a/packages/framework/presence/src/latestValueManager.ts +++ b/packages/framework/presence/src/latestValueManager.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. */ +import { createEmitter } from "@fluidframework/core-utils/internal"; + import type { ValueManager } from "./internalTypes.js"; import { brandedObjectEntries } from "./internalTypes.js"; import type { LatestValueControls } from "./latestValueControls.js"; @@ -17,7 +19,6 @@ import type { JsonSerializable, } from "@fluid-experimental/presence/internal/core-interfaces"; import type { 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"; diff --git a/packages/framework/presence/src/notificationsManager.ts b/packages/framework/presence/src/notificationsManager.ts index 9b6ab69fd3700..39197944e4dee 100644 --- a/packages/framework/presence/src/notificationsManager.ts +++ b/packages/framework/presence/src/notificationsManager.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. */ +import { createEmitter } from "@fluidframework/core-utils/internal"; + import type { ValueManager } from "./internalTypes.js"; 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 { 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"; @@ -158,7 +159,6 @@ class NotificationsManagerImpl< // @ts-expect-error TODO public readonly notifications: NotificationSubscribable = - // @ts-expect-error TODO createEmitter>(); public constructor( diff --git a/packages/framework/presence/src/presenceManager.ts b/packages/framework/presence/src/presenceManager.ts index 92bcb3eaa6b63..26b763504e5a0 100644 --- a/packages/framework/presence/src/presenceManager.ts +++ b/packages/framework/presence/src/presenceManager.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import { createEmitter, type IEmitter } from "@fluidframework/core-utils/internal"; import { createSessionId } from "@fluidframework/id-compressor/internal"; import type { ITelemetryLoggerExt, @@ -32,8 +33,6 @@ import type { IContainerExtension, IExtensionMessage, } from "@fluid-experimental/presence/internal/container-definitions/internal"; -import type { IEmitter } from "@fluid-experimental/presence/internal/events"; -import { createEmitter } from "@fluid-experimental/presence/internal/events"; /** * Portion of the container extension requirements ({@link IContainerExtension}) that are delegated to presence manager. diff --git a/packages/framework/presence/src/systemWorkspace.ts b/packages/framework/presence/src/systemWorkspace.ts index 01a693e81cadc..253466578a669 100644 --- a/packages/framework/presence/src/systemWorkspace.ts +++ b/packages/framework/presence/src/systemWorkspace.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { assert } from "@fluidframework/core-utils/internal"; +import { assert, type IEmitter } from "@fluidframework/core-utils/internal"; import type { ClientConnectionId } from "./baseTypes.js"; import type { InternalTypes } from "./exposedInternalTypes.js"; @@ -17,8 +17,6 @@ import { SessionClientStatus } from "./presence.js"; import type { PresenceStatesInternal } from "./presenceStates.js"; import type { PresenceStates, PresenceStatesSchema } from "./types.js"; -import type { IEmitter } from "@fluid-experimental/presence/internal/events"; - /** * The system workspace's datastore structure. *