diff --git a/docs/class-definitions.md b/docs/class-definitions.md index c117ce93..186f065b 100644 --- a/docs/class-definitions.md +++ b/docs/class-definitions.md @@ -22,7 +22,7 @@ Refer to the [Ably docs for the JS SDK](https://ably.com/docs/getting-started/se #### client -Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) client that was passed to the [constructor](#constructor). +Instance of the [ably-js](https://github.com/ably/ably-js) client that was passed to the [constructor](#constructor). ```ts type client = Ably.RealtimePromise; @@ -30,7 +30,7 @@ type client = Ably.RealtimePromise; #### connection -Instance of the [Ably-JS](https://github.com/ably/ably-js#introduction) connection, belonging to the client that was passed to the [constructor](#constructor). +Instance of the [ably-js](https://github.com/ably/ably-js) connection, belonging to the client that was passed to the [constructor](#constructor). ```ts type connection = Ably.ConnectionPromise; @@ -296,8 +296,11 @@ type SpaceMember = { connectionId: string; isConnected: boolean; profileData: Record; - location: Location; - lastEvent: PresenceEvent; + location: unknown; + lastEvent: { + name: Types.PresenceAction; + timestamp: number; + }; }; ``` @@ -325,15 +328,6 @@ The current location of the user within the space. The most recent event emitted by [presence](https://ably.com/docs/presence-occupancy/presence?lang=javascript) and its timestamp. Events will be either `enter`, `leave`, `update` or `present`. -#### PresenceEvent - -```ts -type PresenceEvent = { - name: 'enter' | 'leave' | 'update' | 'present'; - timestamp: number; -}; -``` - ## Member Locations Handles the tracking of member locations within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). @@ -348,18 +342,18 @@ Available events: - ##### **update** - Fires when a member updates their location. The argument supplied to the event listener is a [LocationUpdate](#locationupdate-1). + Fires when a member updates their location. The argument supplied to the event listener is an UpdateEvent. ```ts - space.locations.subscribe('update', (locationUpdate: LocationUpdate) => {}); + space.locations.subscribe('update', (locationUpdate: LocationsEvents.UpdateEvent) => {}); ``` #### set() -Set your current location. [Location](#location-1) can be any JSON-serializable object. Emits a [locationUpdate](#locationupdate) event to all connected clients in this space. +Set your current location. The `location` argument can be any JSON-serializable object. Emits a `locationUpdate` event to all connected clients in this space. ```ts -type set = (update: Location) => Promise; +type set = (location: unknown) => Promise; ``` #### unsubscribe() @@ -375,7 +369,7 @@ space.locations.unsubscribe('update'); Get location for self. ```ts -type getSelf = () => Promise; +type getSelf = () => Promise; ``` Example: @@ -389,7 +383,7 @@ const myLocation = await space.locations.getSelf(); Get location for all members. ```ts -type getAll = () => Promise>; +type getAll = () => Promise>; ``` Example: @@ -403,7 +397,7 @@ const allLocations = await space.locations.getAll(); Get location for other members ```ts -type getOthers = () => Promise>; +type getOthers = () => Promise>; ``` Example: @@ -414,26 +408,6 @@ const otherLocations = await space.locations.getOthers() ### Related types -#### Location - -Represents a location in an application. - -```ts -type Location = string | Record | null; -``` - -#### LocationUpdate - -Represents a change between locations for a given [`SpaceMember`](#spacemember). - -```ts -type LocationUpdate = { - member: SpaceMember; - currentLocation: Location; - previousLocation: Location; -}; -``` - ## Live Cursors Handles tracking of member cursors within a space. Inherits from [EventEmitter](/docs/usage.md#event-emitters). diff --git a/src/Cursors.ts b/src/Cursors.ts index e3318bc2..82c28213 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -3,12 +3,7 @@ import { Types } from 'ably'; import Space from './Space.js'; import CursorBatching from './CursorBatching.js'; import CursorDispensing from './CursorDispensing.js'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import CursorHistory from './CursorHistory.js'; import { CURSOR_UPDATE } from './CursorConstants.js'; @@ -16,9 +11,9 @@ import type { CursorsOptions, CursorUpdate } from './types.js'; import type { RealtimeMessage } from './utilities/types.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; -type CursorsEventMap = { +export interface CursorsEventMap { update: CursorUpdate; -}; +} const CURSORS_CHANNEL_TAG = '::$cursors'; @@ -27,10 +22,11 @@ export default class Cursors extends EventEmitter { private readonly cursorDispensing: CursorDispensing; private readonly cursorHistory: CursorHistory; readonly options: CursorsOptions; - readonly channelName: string; + private readonly channelName: string; public channel?: Types.RealtimeChannelPromise; + /** @internal */ constructor(private space: Space) { super(); @@ -90,7 +86,7 @@ export default class Cursors extends EventEmitter { return !this.emitterHasListeners(subscriptions); } - private emitterHasListeners = (emitter: EventEmitter<{}>) => { + private emitterHasListeners = (emitter: EventEmitter) => { const flattenEvents = (obj: Record) => Object.entries(obj) .map((_, v) => v) @@ -104,9 +100,14 @@ export default class Cursors extends EventEmitter { ); }; - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + subscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + subscribe(listener?: EventListener): void; + subscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -129,9 +130,14 @@ export default class Cursors extends EventEmitter { } } - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + unsubscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + unsubscribe(listener?: EventListener): void; + unsubscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Locations.ts b/src/Locations.ts index f2b57106..ddf78885 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -1,9 +1,4 @@ -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; @@ -11,17 +6,27 @@ import type Space from './Space.js'; import { ERR_NOT_ENTERED_SPACE } from './Errors.js'; import SpaceUpdate from './SpaceUpdate.js'; -type LocationsEventMap = { - update: { member: SpaceMember; currentLocation: unknown; previousLocation: unknown }; -}; +export namespace LocationsEvents { + export interface UpdateEvent { + member: SpaceMember; + currentLocation: unknown; + previousLocation: unknown; + } +} + +export interface LocationsEventMap { + update: LocationsEvents.UpdateEvent; +} export default class Locations extends EventEmitter { private lastLocationUpdate: Record = {}; + /** @internal */ constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { super(); } + /** @internal */ async processPresenceMessage(message: PresenceMember) { // Only an update action is currently a valid location update. if (message.action !== 'update') return; @@ -61,9 +66,14 @@ export default class Locations extends EventEmitter { await this.presenceUpdate(update.updateLocation(location)); } - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + subscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + subscribe(listener?: EventListener): void; + subscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -78,9 +88,14 @@ export default class Locations extends EventEmitter { } } - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + unsubscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + unsubscribe(listener?: EventListener): void; + unsubscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/Locks.ts b/src/Locks.ts index c8f9e20e..23d7ce96 100644 --- a/src/Locks.ts +++ b/src/Locks.ts @@ -4,26 +4,21 @@ import Space from './Space.js'; import type { Lock, SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import { ERR_LOCK_IS_LOCKED, ERR_LOCK_INVALIDATED, ERR_LOCK_REQUEST_EXISTS, ERR_NOT_ENTERED_SPACE } from './Errors.js'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import SpaceUpdate from './SpaceUpdate.js'; export type LockAttributes = Record; -interface LockOptions { +export interface LockOptions { attributes: LockAttributes; } -type LockEventMap = { +export interface LocksEventMap { update: Lock; -}; +} -export default class Locks extends EventEmitter { +export default class Locks extends EventEmitter { // locks tracks the local state of locks, which is used to determine whether // a lock's status has changed when processing presence updates. // @@ -32,6 +27,7 @@ export default class Locks extends EventEmitter { // have requested. private locks: Map>; + /** @internal */ constructor(private space: Space, private presenceUpdate: Space['presenceUpdate']) { super(); this.locks = new Map(); @@ -131,9 +127,11 @@ export default class Locks extends EventEmitter { this.deleteLock(id, self.connectionId); } - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + subscribe(eventOrEvents: K | K[], listener?: EventListener): void; + subscribe(listener?: EventListener): void; + subscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -148,9 +146,11 @@ export default class Locks extends EventEmitter { } } - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; + unsubscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); @@ -165,6 +165,7 @@ export default class Locks extends EventEmitter { } } + /** @internal */ async processPresenceMessage(message: Types.PresenceMessage) { const member = await this.space.members.getByConnectionId(message.connectionId); if (!member) return; @@ -219,7 +220,7 @@ export default class Locks extends EventEmitter { // // TODO: remove this once the Ably system processes PENDING requests // internally using this same logic. - processPending(member: SpaceMember, pendingLock: Lock) { + private processPending(member: SpaceMember, pendingLock: Lock) { // if the requested lock ID is not currently locked, then mark the PENDING // lock request as LOCKED const lock = this.get(pendingLock.id); @@ -263,18 +264,19 @@ export default class Locks extends EventEmitter { pendingLock.reason = ERR_LOCK_IS_LOCKED(); } - updatePresence(self: SpaceMember) { + private updatePresence(self: SpaceMember) { const update = new SpaceUpdate({ self, extras: this.getLockExtras(self.connectionId) }); return this.presenceUpdate(update.noop()); } + /** @internal */ getLock(id: string, connectionId: string): Lock | undefined { const locks = this.locks.get(id); if (!locks) return; return locks.get(connectionId); } - setLock(lock: Lock) { + private setLock(lock: Lock) { let locks = this.locks.get(lock.id); if (!locks) { locks = new Map(); @@ -283,7 +285,7 @@ export default class Locks extends EventEmitter { locks.set(lock.member.connectionId, lock); } - deleteLock(id: string, connectionId: string) { + private deleteLock(id: string, connectionId: string) { const locks = this.locks.get(id); if (!locks) return; return locks.delete(connectionId); @@ -303,6 +305,7 @@ export default class Locks extends EventEmitter { return requests; } + /** @internal */ getLockExtras(connectionId: string): PresenceMember['extras'] { const locks = this.getLocksForConnectionId(connectionId); if (locks.length === 0) return; diff --git a/src/Members.ts b/src/Members.ts index 2edae1f3..b058fc4b 100644 --- a/src/Members.ts +++ b/src/Members.ts @@ -1,32 +1,29 @@ -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import Leavers from './Leavers.js'; import type { SpaceMember } from './types.js'; import type { PresenceMember } from './utilities/types.js'; import type Space from './Space.js'; -type MemberEventsMap = { +export interface MembersEventMap { leave: SpaceMember; enter: SpaceMember; update: SpaceMember; updateProfile: SpaceMember; remove: SpaceMember; -}; +} -class Members extends EventEmitter { +class Members extends EventEmitter { private lastMemberUpdate: Record = {}; private leavers: Leavers; + /** @internal */ constructor(private space: Space) { super(); this.leavers = new Leavers(this.space.options.offlineTimeout); } + /** @internal */ async processPresenceMessage(message: PresenceMember) { const { action, connectionId } = message; const isLeaver = !!this.leavers.getByConnectionId(connectionId); @@ -69,9 +66,14 @@ class Members extends EventEmitter { return members.filter((m) => m.connectionId !== this.space.connectionId); } - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + subscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + subscribe(listener?: EventListener): void; + subscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -86,9 +88,14 @@ class Members extends EventEmitter { } } - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + unsubscribe( + eventOrEvents: K | K[], + listener?: EventListener, + ): void; + unsubscribe(listener?: EventListener): void; + unsubscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); @@ -103,12 +110,13 @@ class Members extends EventEmitter { } } + /** @internal */ async getByConnectionId(connectionId: string): Promise { const members = await this.getAll(); return members.find((m) => m.connectionId === connectionId) ?? null; } - createMember(message: PresenceMember): SpaceMember { + private createMember(message: PresenceMember): SpaceMember { return { clientId: message.clientId, connectionId: message.connectionId, @@ -122,7 +130,7 @@ class Members extends EventEmitter { }; } - async onMemberOffline(member: SpaceMember) { + private async onMemberOffline(member: SpaceMember) { this.leavers.removeLeaver(member.connectionId); this.emit('remove', member); diff --git a/src/Space.ts b/src/Space.ts index 50dec7d2..81685ce1 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -1,11 +1,6 @@ import Ably, { Types } from 'ably'; -import EventEmitter, { - InvalidArgumentError, - inspect, - type EventKey, - type EventListener, -} from './utilities/EventEmitter.js'; +import EventEmitter, { InvalidArgumentError, inspect, type EventListener } from './utilities/EventEmitter.js'; import Locations from './Locations.js'; import Cursors from './Cursors.js'; import Members from './Members.js'; @@ -29,12 +24,31 @@ const SPACE_OPTIONS_DEFAULTS = { }, }; -type SpaceEventsMap = { - update: { members: SpaceMember[] }; -}; +export namespace SpaceEvents { + export interface UpdateEvent { + members: SpaceMember[]; + } +} + +export interface SpaceEventMap { + update: SpaceEvents.UpdateEvent; +} + +export interface SpaceState { + members: SpaceMember[]; +} -class Space extends EventEmitter { - readonly channelName: string; +export type UpdateProfileDataFunction = (profileData: ProfileData) => ProfileData; + +class Space extends EventEmitter { + /** + * @internal + */ + readonly client: Types.RealtimePromise; + private readonly channelName: string; + /** + * @internal + */ readonly connectionId: string | undefined; readonly options: SpaceOptions; readonly locations: Locations; @@ -44,9 +58,11 @@ class Space extends EventEmitter { readonly locks: Locks; readonly name: string; - constructor(name: string, readonly client: Types.RealtimePromise, options?: Subset) { + /** @internal */ + constructor(name: string, client: Types.RealtimePromise, options?: Subset) { super(); + this.client = client; this.options = this.setOptions(options); this.connectionId = this.client.connection.id; this.name = name; @@ -127,7 +143,9 @@ class Space extends EventEmitter { }); } - async updateProfileData(profileDataOrUpdateFn: ProfileData | ((update: ProfileData) => ProfileData)): Promise { + async updateProfileData(profileData: ProfileData): Promise; + async updateProfileData(updateFn: UpdateProfileDataFunction): Promise; + async updateProfileData(profileDataOrUpdateFn: ProfileData | UpdateProfileDataFunction): Promise { const self = await this.members.getSelf(); if (!isObject(profileDataOrUpdateFn) && !isFunction(profileDataOrUpdateFn)) { @@ -172,14 +190,16 @@ class Space extends EventEmitter { await this.presenceLeave(data); } - async getState(): Promise<{ members: SpaceMember[] }> { + async getState(): Promise { const members = await this.members.getAll(); return { members }; } - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + subscribe(eventOrEvents: K | K[], listener?: EventListener): void; + subscribe(listener?: EventListener): void; + subscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.on(listenerOrEvents, listener); @@ -194,9 +214,11 @@ class Space extends EventEmitter { } } - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, + unsubscribe(eventOrEvents: K | K[], listener?: EventListener): void; + unsubscribe(listener?: EventListener): void; + unsubscribe( + listenerOrEvents?: K | K[] | EventListener, + listener?: EventListener, ) { try { super.off(listenerOrEvents, listener); diff --git a/src/index.ts b/src/index.ts index 806f1e55..a58d2e69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,11 @@ import Spaces from './Spaces.js'; -export type Space = Awaited>; +export type { default as Space, SpaceEventMap, SpaceEvents, SpaceState, UpdateProfileDataFunction } from './Space.js'; + +export type { default as Cursors, CursorsEventMap } from './Cursors.js'; +export type { default as Locations, LocationsEventMap, LocationsEvents } from './Locations.js'; +export type { default as Locks, LocksEventMap, LockOptions } from './Locks.js'; +export type { default as Members, MembersEventMap } from './Members.js'; // Can be changed to * when we update to TS5 @@ -16,6 +21,11 @@ export type { SpaceMember, Lock, LockStatus, + LockStatuses, } from './types.js'; export type { LockAttributes } from './Locks.js'; + +export type { default as EventEmitter, EventListener, EventListenerThis } from './utilities/EventEmitter.js'; + +export type { Subset } from './utilities/types.js'; diff --git a/src/types.ts b/src/types.ts index 9641b53c..96a16ba7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,7 +39,13 @@ export interface SpaceMember { }; } -export type LockStatus = 'pending' | 'locked' | 'unlocked'; +export namespace LockStatuses { + export type Pending = 'pending'; + export type Locked = 'locked'; + export type Unlocked = 'unlocked'; +} + +export type LockStatus = LockStatuses.Pending | LockStatuses.Locked | LockStatuses.Unlocked; export type Lock = { id: string; diff --git a/src/utilities/EventEmitter.test.ts b/src/utilities/EventEmitter.test.ts index 330102ea..8e2a62e6 100644 --- a/src/utilities/EventEmitter.test.ts +++ b/src/utilities/EventEmitter.test.ts @@ -174,7 +174,7 @@ describe('EventEmitter', () => { }); it('removes a specific listener from multiple events', () => { - const eventEmitter = new EventEmitter(); + const eventEmitter = new EventEmitter<{ myEvent: unknown; myOtherEvent: unknown; myThirdEvent: unknown }>(); const specificListener = vi.fn(); eventEmitter['on'](['myEvent', 'myOtherEvent', 'myThirdEvent'], specificListener); eventEmitter['off'](['myEvent', 'myOtherEvent'], specificListener); diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 6c2d1101..fa7da99b 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -1,8 +1,8 @@ import { isArray, isFunction, isObject, isString } from './is.js'; -function callListener(eventThis: { event: string }, listener: Function, args: unknown[]) { +function callListener(eventThis: { event: K }, listener: EventListener, arg: T[K]) { try { - listener.apply(eventThis, args); + listener.apply(eventThis, [arg]); } catch (e) { console.error( 'EventEmitter.emit()', @@ -17,14 +17,14 @@ function callListener(eventThis: { event: string }, listener: Function, args: un * @param listener the listener callback to remove * @param eventFilter (optional) event name instructing the function to only remove listeners for the specified event */ -export function removeListener( - targetListeners: (Function[] | Record)[], +export function removeListener( + targetListeners: (Function[] | Record)[], listener: Function, - eventFilter?: string, + eventFilter?: keyof T, ) { - let listeners: Function[] | Record; + let listeners: Function[] | Record; let index: number; - let eventName: string; + let eventName: keyof T; for (let targetListenersIndex = 0; targetListenersIndex < targetListeners.length; targetListenersIndex++) { listeners = targetListeners[targetListenersIndex]; @@ -64,17 +64,25 @@ export class InvalidArgumentError extends Error { } } -export type EventMap = Record; -// extract all the keys of an event map and use them as a type -export type EventKey = string & keyof T; -export type EventListener = (params: T) => void; +export interface EventListenerThis { + event: K; +} + +export type EventListener = (this: EventListenerThis, param: T[K]) => void; -export default class EventEmitter { +export default class EventEmitter { + /** @internal */ any: Array; - events: Record; + /** @internal */ + events: Record; + /** @internal */ anyOnce: Array; - eventsOnce: Record; + /** @internal */ + eventsOnce: Record; + /** + * @internal + */ constructor() { this.any = []; this.events = Object.create(null); @@ -83,11 +91,23 @@ export default class EventEmitter { } /** + * {@label WITH_EVENTS} * Add an event listener - * @param listenerOrEvents (optional) the name of the event to listen to or the listener to be called. + * @param eventOrEvents the name of the event to listen to or the listener to be called. + * @param listener (optional) the listener to be called. + */ + on(eventOrEvents?: K | K[], listener?: EventListener): void; + /** + * Behaves the same as { @link on:WITH_EVENTS | the overload which accepts one or more event names }, but listens to _all_ events. * @param listener (optional) the listener to be called. */ - on>(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { + on(listener?: EventListener): void; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void; + on(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .on(() => {}) if (isFunction(listenerOrEvents)) { this.any.push(listenerOrEvents); @@ -113,12 +133,23 @@ export default class EventEmitter { } /** + * {@label WITH_EVENTS} * Remove one or more event listeners - * @param listenerOrEvents (optional) the name of the event whose listener is to be removed. If not supplied, - * the listener is treated as an 'any' listener. + * @param eventOrEvents the name of the event whose listener is to be removed. + * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. + */ + off(eventOrEvents?: K | K[], listener?: EventListener): void; + /** + * Behaves the same as { @link off:WITH_EVENTS | the overload which accepts one or more event names }, but removes the listener from _all_ events. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ - off>(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { + off(listener?: EventListener): void; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void; + off(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .off() // don't use arguments.length === 0 here as don't won't handle // cases like .off(undefined) which is a valid call @@ -173,7 +204,7 @@ export default class EventEmitter { * @param event (optional) the name of the event, or none for 'any' * @return array of events, or null if none */ - listeners>(event: K): Function[] | null { + listeners(event: K): Function[] | null { if (event) { const listeners = [...(this.events[event] ?? [])]; @@ -188,13 +219,15 @@ export default class EventEmitter { } /** + * @internal + * * Emit an event * @param event the event name * @param arg the arguments to pass to the listener */ - emit>(event: K, arg: T[K]) { + emit(event: K, arg: T[K]) { const eventThis = { event }; - const listeners: Function[] = []; + const listeners: EventListener[] = []; if (this.anyOnce.length > 0) { Array.prototype.push.apply(listeners, this.anyOnce); @@ -217,18 +250,33 @@ export default class EventEmitter { } listeners.forEach(function (listener) { - callListener(eventThis, listener, [arg]); + callListener(eventThis, listener, arg); }); } /** + * {@label WITH_EVENTS} * Listen for a single occurrence of an event - * @param listenerOrEvent (optional) the name of the event to listen to + * @param event the name of the event to listen to * @param listener (optional) the listener to be called */ - once>( - listenerOrEvent: K | EventListener, - listener?: EventListener, + once(event: K, listener?: EventListener): void | Promise; + /** + * Behaves the same as { @link once:WITH_EVENTS | the overload which accepts one or more event names }, but listens for _all_ events. + * @param listener (optional) the listener to be called + */ + once(listener?: EventListener): void | Promise; + /** + * @internal + * We add the implementation signature as an overload signature (but mark it as internal so that it does not appear in documentation) so that it can be called by subclasses. + */ + once( + listenerOrEvent: K | EventListener, + listener?: EventListener, + ): void | Promise; + once( + listenerOrEvent: K | EventListener, + listener?: EventListener, ): void | Promise { // .once("eventName", () => {}) if (isString(listenerOrEvent) && isFunction(listener)) { @@ -247,15 +295,20 @@ export default class EventEmitter { } /** - * Private API + * @internal * * Listen for a single occurrence of a state event and fire immediately if currentState matches targetState * @param targetState the name of the state event to listen to * @param currentState the name of the current state of this object * @param listener the listener to be called - * @param listenerArgs + * @param listenerArg the argument to pass to the listener */ - whenState(targetState: string, currentState: string, listener: EventListener, ...listenerArgs: unknown[]) { + whenState( + targetState: K, + currentState: keyof T, + listener: EventListener, + listenerArg: T[K], + ) { const eventThis = { event: targetState }; if (typeof targetState !== 'string' || typeof currentState !== 'string') { @@ -263,14 +316,11 @@ export default class EventEmitter { } if (typeof listener !== 'function' && Promise) { return new Promise((resolve) => { - EventEmitter.prototype.whenState.apply( - this, - [targetState, currentState, resolve].concat(listenerArgs as any[]) as any, - ); + EventEmitter.prototype.whenState.apply(this, [targetState, currentState, resolve, listenerArg]); }); } if (targetState === currentState) { - callListener(eventThis, listener, listenerArgs); + callListener(eventThis, listener, listenerArg); } else { this.once(targetState, listener); } diff --git a/src/utilities/is.ts b/src/utilities/is.ts index 8a6fd0d3..5e3e4435 100644 --- a/src/utilities/is.ts +++ b/src/utilities/is.ts @@ -11,7 +11,7 @@ function isFunction(arg: unknown): arg is Function { return ['Function', 'AsyncFunction', 'GeneratorFunction', 'Proxy'].includes(typeOf(arg)); } -function isString(arg: unknown): arg is String { +function isString(arg: unknown): arg is string { return typeOf(arg) === 'String'; } diff --git a/src/utilities/types.ts b/src/utilities/types.ts index cd3e2f5a..7eadcd05 100644 --- a/src/utilities/types.ts +++ b/src/utilities/types.ts @@ -1,6 +1,5 @@ import type { Types } from 'ably'; -import type { EventKey, EventListener, EventMap } from './EventEmitter.js'; import type { ProfileData, Lock } from '../types.js'; export type PresenceMember = { @@ -20,22 +19,10 @@ export type PresenceMember = { }; } & Omit; -export type Subset = { - [attr in keyof K]?: K[attr] extends object ? Subset : K[attr]; +export type Subset = { + [attr in keyof T]?: T[attr] extends object ? Subset : T[attr]; }; -export interface Provider { - subscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, - ): void; - - unsubscribe>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, - ): void; -} - export type RealtimeMessage = Omit & { connectionId: string; };