diff --git a/src/CursorBatching.ts b/src/CursorBatching.ts index 5d0ecc79..b7e47830 100644 --- a/src/CursorBatching.ts +++ b/src/CursorBatching.ts @@ -44,14 +44,14 @@ export default class CursorBatching { this.outgoingBuffers.push(value); } - private async publishFromBuffer(channel, eventName: string) { + private async publishFromBuffer(channel: Types.RealtimeChannelPromise, eventName: string) { if (!this.isRunning) { this.isRunning = true; await this.batchToChannel(channel, eventName); } } - private async batchToChannel(channel, eventName: string) { + private async batchToChannel(channel: Types.RealtimeChannelPromise, eventName: string) { if (!this.hasMovement) { this.isRunning = false; return; diff --git a/src/CursorDispensing.ts b/src/CursorDispensing.ts index d73905d2..ae8df5c9 100644 --- a/src/CursorDispensing.ts +++ b/src/CursorDispensing.ts @@ -7,13 +7,8 @@ export default class CursorDispensing { private buffer: Record = {}; private handlerRunning: boolean = false; private timerIds: ReturnType[] = []; - private emitCursorUpdate: (update: CursorUpdate) => void; - private getCurrentBatchTime: () => number; - constructor(emitCursorUpdate, getCurrentBatchTime) { - this.emitCursorUpdate = emitCursorUpdate; - this.getCurrentBatchTime = getCurrentBatchTime; - } + constructor(private emitCursorUpdate: (update: CursorUpdate) => void, private getCurrentBatchTime: () => number) {} emitFromBatch(batchDispenseInterval: number) { if (!this.bufferHaveData()) { @@ -66,7 +61,7 @@ export default class CursorDispensing { } processBatch(message: RealtimeMessage) { - const updates = message.data || []; + const updates: CursorUpdate[] = message.data || []; updates.forEach((update) => { const enhancedMsg = { diff --git a/src/Cursors.ts b/src/Cursors.ts index f95ad8d1..563fbfd0 100644 --- a/src/Cursors.ts +++ b/src/Cursors.ts @@ -20,20 +20,6 @@ type CursorsEventMap = { export const CURSOR_UPDATE = 'cursorUpdate'; -const emitterHasListeners = (emitter) => { - const flattenEvents = (obj) => - Object.entries(obj) - .map((_, v) => v) - .flat(); - - return ( - emitter.any.length > 0 || - emitter.anyOnce.length > 0 || - flattenEvents(emitter.events).length > 0 || - flattenEvents(emitter.eventsOnce).length > 0 - ); -}; - export default class Cursors extends EventEmitter { private readonly cursorBatching: CursorBatching; private readonly cursorDispensing: CursorDispensing; @@ -92,9 +78,29 @@ export default class Cursors extends EventEmitter { private isUnsubscribed() { const channel = this.getChannel(); - return !emitterHasListeners(channel['subscriptions']); + + interface ChannelWithSubscriptions extends Types.RealtimeChannelPromise { + subscriptions: EventEmitter<{}>; + } + + const subscriptions = (channel as ChannelWithSubscriptions).subscriptions; + return !this.emitterHasListeners(subscriptions); } + private emitterHasListeners = (emitter: EventEmitter<{}>) => { + const flattenEvents = (obj: Record) => + Object.entries(obj) + .map((_, v) => v) + .flat(); + + return ( + emitter.any.length > 0 || + emitter.anyOnce.length > 0 || + flattenEvents(emitter.events).length > 0 || + flattenEvents(emitter.eventsOnce).length > 0 + ); + }; + subscribe>( listenerOrEvents?: K | K[] | EventListener, listener?: EventListener, @@ -136,7 +142,7 @@ export default class Cursors extends EventEmitter { } } - const hasListeners = emitterHasListeners(this); + const hasListeners = this.emitterHasListeners(this); if (!hasListeners) { const channel = this.getChannel(); diff --git a/src/Locations.ts b/src/Locations.ts index 2df2101c..edb2f577 100644 --- a/src/Locations.ts +++ b/src/Locations.ts @@ -117,14 +117,14 @@ export default class Locations extends EventEmitter { return this.space.members .getAll() .filter((member) => member.connectionId !== self?.connectionId) - .reduce((acc, member) => { + .reduce((acc: Record, member: SpaceMember) => { acc[member.connectionId] = member.location; return acc; }, {}); } getAll(): Record { - return this.space.members.getAll().reduce((acc, member) => { + return this.space.members.getAll().reduce((acc: Record, member: SpaceMember) => { acc[member.connectionId] = member.location; return acc; }, {}); diff --git a/src/Space.ts b/src/Space.ts index 1233f12a..c1deaeda 100644 --- a/src/Space.ts +++ b/src/Space.ts @@ -92,7 +92,11 @@ class Space extends EventEmitter { return new Promise((resolve) => { const presence = this.channel.presence; - presence['subscriptions'].once('enter', async () => { + interface PresenceWithSubscriptions extends Types.RealtimePresencePromise { + subscriptions: EventEmitter<{ enter: [unknown] }>; + } + + (presence as PresenceWithSubscriptions).subscriptions.once('enter', async () => { const presenceMessages = await presence.get(); const members = this.members.mapPresenceMembersToSpaceMembers(presenceMessages); diff --git a/src/Spaces.test.ts b/src/Spaces.test.ts index 3e859ca2..f204abab 100644 --- a/src/Spaces.test.ts +++ b/src/Spaces.test.ts @@ -1,17 +1,17 @@ import { it, describe, expect, expectTypeOf, vi, beforeEach } from 'vitest'; import { Realtime, Types } from 'ably/promises'; -import Spaces from './Spaces.js'; +import Spaces, { type ClientWithOptions } from './Spaces.js'; interface SpacesTestContext { - client: Types.RealtimePromise; + client: ClientWithOptions; } vi.mock('ably/promises'); describe('Spaces', () => { beforeEach((context) => { - context.client = new Realtime({ key: 'asd' }); + context.client = new Realtime({ key: 'asd' }) as ClientWithOptions; }); it('expects the injected client to be of the type RealtimePromise', ({ client }) => { @@ -32,14 +32,18 @@ describe('Spaces', () => { it('applies the agent header to an existing SDK instance', ({ client }) => { const spaces = new Spaces(client); - expect(client['options'].agents).toEqual({ 'ably-spaces': spaces.version, 'space-custom-client': true }); + expect(client.options.agents).toEqual({ + 'ably-spaces': spaces.version, + 'space-custom-client': true, + }); }); it('extend the agents array when it already exists', ({ client }) => { - client['options']['agents'] = { 'some-client': '1.2.3' }; + (client as ClientWithOptions).options.agents = { 'some-client': '1.2.3' }; const spaces = new Spaces(client); + const ablyClient = spaces.ably as ClientWithOptions; - expect(spaces.ably['options'].agents).toEqual({ + expect(ablyClient.options.agents).toEqual({ 'some-client': '1.2.3', 'ably-spaces': spaces.version, 'space-custom-client': true, diff --git a/src/Spaces.ts b/src/Spaces.ts index 1a8ef033..56acfea5 100644 --- a/src/Spaces.ts +++ b/src/Spaces.ts @@ -5,6 +5,12 @@ import Space from './Space.js'; import type { SpaceOptions } from './types.js'; import type { Subset } from './utilities/types.js'; +export interface ClientWithOptions extends Types.RealtimePromise { + options: { + agents?: Record; + }; +} + class Spaces { private spaces: Record = {}; ably: Types.RealtimePromise; @@ -13,7 +19,7 @@ class Spaces { constructor(client: Types.RealtimePromise) { this.ably = client; - this.addAgent(this.ably['options']); + this.addAgent((this.ably as ClientWithOptions)['options']); this.ably.time(); } diff --git a/src/utilities/EventEmitter.test.ts b/src/utilities/EventEmitter.test.ts index e96650ac..aa4c87f6 100644 --- a/src/utilities/EventEmitter.test.ts +++ b/src/utilities/EventEmitter.test.ts @@ -98,14 +98,14 @@ describe('EventEmitter', () => { it('adds a listener to the "any" set of event listeners', (context) => { context.eventEmitter['on'](context.spy); expect(context.eventEmitter['any']).toStrictEqual([context.spy]); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledOnce(); }); it('adds a listener to a provided field of an event listener', (context) => { context.eventEmitter['on']('myEvent', context.spy); expect(context.eventEmitter['events']['myEvent']).toStrictEqual([context.spy]); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledOnce(); }); @@ -114,11 +114,11 @@ describe('EventEmitter', () => { expect(context.eventEmitter['events']['myEvent']).toStrictEqual([context.spy]); expect(context.eventEmitter['events']['myOtherEvent']).toStrictEqual([context.spy]); expect(context.eventEmitter['events']['myThirdEvent']).toStrictEqual([context.spy]); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledTimes(1); - context.eventEmitter['emit']('myOtherEvent'); + context.eventEmitter['emit']('myOtherEvent', ''); expect(context.spy).toHaveBeenCalledTimes(2); - context.eventEmitter['emit']('myThirdEvent'); + context.eventEmitter['emit']('myThirdEvent', ''); expect(context.spy).toHaveBeenCalledTimes(3); }); }); @@ -148,7 +148,7 @@ describe('EventEmitter', () => { expect(context.eventEmitter['anyOnce']).toStrictEqual([]); expect(context.eventEmitter['events']).toStrictEqual(Object.create(null)); expect(context.eventEmitter['eventsOnce']).toStrictEqual(Object.create(null)); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).not.toHaveBeenCalled(); }); @@ -158,7 +158,7 @@ describe('EventEmitter', () => { expect(context.eventEmitter['anyOnce']).toStrictEqual([altListener]); expect(context.eventEmitter['events']['myEvent']).not.toContain(context.spy); expect(context.eventEmitter['eventsOnce']['myEvent']).not.toContain(context.spy); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).not.toHaveBeenCalled(); }); @@ -168,9 +168,9 @@ describe('EventEmitter', () => { expect(context.eventEmitter['anyOnce']).toStrictEqual([context.spy, altListener]); expect(context.eventEmitter['events']['myEvent']).not.toContain(context.spy); expect(context.eventEmitter['events']['myOtherEvent']).toContain(context.spy); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledTimes(2); - context.eventEmitter['emit']('myOtherEvent'); + context.eventEmitter['emit']('myOtherEvent', ''); expect(context.spy).toHaveBeenCalledTimes(3); }); @@ -182,11 +182,11 @@ describe('EventEmitter', () => { expect(eventEmitter['events']['myEvent']).toBe(undefined); expect(eventEmitter['events']['myOtherEvent']).toBe(undefined); expect(eventEmitter['events']['myThirdEvent']).toContain(specificListener); - eventEmitter['emit']('myEvent'); - eventEmitter['emit']('myOtherEvent'); + eventEmitter['emit']('myEvent', ''); + eventEmitter['emit']('myOtherEvent', ''); expect(specificListener).not.toHaveBeenCalled(); expect(eventEmitter['events']['myThirdEvent']).toContain(specificListener); - eventEmitter['emit']('myThirdEvent'); + eventEmitter['emit']('myThirdEvent', ''); expect(specificListener).toHaveBeenCalledOnce(); }); @@ -241,17 +241,17 @@ describe('EventEmitter', () => { it('adds a listener to anyOnce on calling `once` with a listener', (context) => { context.eventEmitter['once'](context.spy); expect(context.eventEmitter['anyOnce']).toHaveLength(1); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledOnce(); - context.eventEmitter['emit']('myOtherEvent'); + context.eventEmitter['emit']('myOtherEvent', ''); }); it('adds a listener to an eventOnce on calling `once` with a listener and event name', (context) => { context.eventEmitter['once']('myEvent', context.spy); expect(context.eventEmitter['eventsOnce']['myEvent']).toHaveLength(1); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledOnce(); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledOnce(); }); @@ -260,7 +260,7 @@ describe('EventEmitter', () => { expect(context.eventEmitter['eventsOnce']['myEvent']).toHaveLength(1); expect(context.eventEmitter['eventsOnce']['myOtherEvent']).toHaveLength(1); expect(context.eventEmitter['eventsOnce']['myThirdEvent']).toHaveLength(1); - expect(context.eventEmitter['emit']('myEvent')); + expect(context.eventEmitter['emit']('myEvent', '')); expect(context.eventEmitter['eventsOnce']['myEvent']).toBe(undefined); expect(context.eventEmitter['eventsOnce']['myOtherEvent']).toBe(undefined); expect(context.eventEmitter['eventsOnce']['myThirdEvent']).toBe(undefined); @@ -279,7 +279,7 @@ describe('EventEmitter', () => { context.eventEmitter['on']('myEvent', context.spy); expect(context.eventEmitter['listeners']('myEvent')).toContain(context.spy); expect(context.spy).not.toHaveBeenCalled(); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.spy).toHaveBeenCalledOnce(); // anyOnce must also be emptied expect(context.eventEmitter['anyOnce']).toStrictEqual([]); @@ -287,7 +287,7 @@ describe('EventEmitter', () => { it('emits any events in anyOnce on emitting specific events', (context) => { context.eventEmitter['on']('myEvent', context.spy); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.eventEmitter['anyOnce']).toStrictEqual([]); expect(context.spy).toHaveBeenCalledOnce(); }); @@ -295,7 +295,7 @@ describe('EventEmitter', () => { it('emits an event and removes it on being called for a once operation', (context) => { context.eventEmitter['once']('myEvent', context.spy); expect(context.eventEmitter['listeners']('myEvent')).toContain(context.spy); - context.eventEmitter['emit']('myEvent'); + context.eventEmitter['emit']('myEvent', ''); expect(context.eventEmitter['listeners']('myEvent')).toBe(null); expect(context.eventEmitter['anyOnce']).toStrictEqual([]); expect(context.spy).toHaveBeenCalledOnce(); diff --git a/src/utilities/EventEmitter.ts b/src/utilities/EventEmitter.ts index 0359f224..358b74c6 100644 --- a/src/utilities/EventEmitter.ts +++ b/src/utilities/EventEmitter.ts @@ -23,13 +23,13 @@ export function removeListener( eventFilter?: string, ) { let listeners: Function[] | Record; - let index; - let eventName; + let index: number; + let eventName: string; for (let targetListenersIndex = 0; targetListenersIndex < targetListeners.length; targetListenersIndex++) { listeners = targetListeners[targetListenersIndex]; - if (eventFilter) { + if (isString(eventFilter) && isObject(listeners)) { listeners = listeners[eventFilter]; } @@ -39,8 +39,9 @@ export function removeListener( } /* If events object has an event name key with no listeners then remove the key to stop the list growing indefinitely */ - if (eventFilter && listeners.length === 0) { - delete targetListeners[targetListenersIndex][eventFilter]; + const parentCollection = targetListeners[targetListenersIndex]; + if (eventFilter && listeners.length === 0 && isObject(parentCollection)) { + delete parentCollection[eventFilter]; } } else if (isObject(listeners)) { for (eventName in listeners) { @@ -58,21 +59,21 @@ export function inspect(args: unknown): string { } export class InvalidArgumentError extends Error { - constructor(...args) { + constructor(...args: [string | undefined]) { super(...args); } } -export type EventMap = Record; +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 default class EventEmitter { - protected any: Array; - protected events: Record; - protected anyOnce: Array; - protected eventsOnce: Record; + any: Array; + events: Record; + anyOnce: Array; + eventsOnce: Record; constructor() { this.any = []; @@ -86,10 +87,7 @@ export default class EventEmitter { * @param listenerOrEvents (optional) the name of the event to listen to or the listener to be called. * @param listener (optional) the listener to be called. */ - protected on>( - listenerOrEvents?: K | K[] | EventListener, - listener?: EventListener, - ): void { + on>(listenerOrEvents?: K | K[] | EventListener, listener?: EventListener): void { // .on(() => {}) if (isFunction(listenerOrEvents)) { this.any.push(listenerOrEvents); @@ -120,10 +118,7 @@ export default class EventEmitter { * the listener is treated as an 'any' listener. * @param listener (optional) the listener to remove. If not supplied, all listeners are removed. */ - protected 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 @@ -178,7 +173,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 */ - protected listeners>(event: K): Function[] | null { + listeners>(event: K): Function[] | null { if (event) { const listeners = [...(this.events[event] ?? [])]; @@ -195,9 +190,9 @@ export default class EventEmitter { /** * Emit an event * @param event the event name - * @param args the arguments to pass to the listener + * @param arg the arguments to pass to the listener */ - emit>(event: K, ...args: unknown[] /* , args... */) { + emit>(event: K, arg: T[K]) { const eventThis = { event }; const listeners: Function[] = []; @@ -222,7 +217,7 @@ export default class EventEmitter { } listeners.forEach(function (listener) { - callListener(eventThis, listener, args); + callListener(eventThis, listener, [arg]); }); } @@ -231,7 +226,7 @@ export default class EventEmitter { * @param listenerOrEvents (optional) the name of the event to listen to * @param listener (optional) the listener to be called */ - protected once>( + once>( listenerOrEvents: K | K[] | EventListener, listener?: EventListener, ): void | Promise { @@ -245,12 +240,14 @@ export default class EventEmitter { // .once(["eventName"], () => {}) if (isArray(listenerOrEvents) && isFunction(listener)) { const self = this; + listenerOrEvents.forEach(function (eventName) { - const listenerWrapper = function (listenerThis: any) { - const innerArgs = Array.prototype.slice.call(arguments); + const listenerWrapper: EventListener = function (this: EventListener, listenerThis) { + const innerArgs = Array.prototype.slice.call(arguments) as [params: T[K]]; listenerOrEvents.forEach((eventName) => { self.off(eventName, this); }); + listener.apply(listenerThis, innerArgs); }; self.once(eventName, listenerWrapper); @@ -277,12 +274,7 @@ export default class EventEmitter { * @param listener the listener to be called * @param listenerArgs */ - protected whenState( - targetState: string, - currentState: string, - listener: EventListener, - ...listenerArgs: unknown[] - ) { + whenState(targetState: string, currentState: string, listener: EventListener, ...listenerArgs: unknown[]) { const eventThis = { event: targetState }; if (typeof targetState !== 'string' || typeof currentState !== 'string') { diff --git a/src/utilities/math.ts b/src/utilities/math.ts index 33bc7d7d..0e6c2a69 100644 --- a/src/utilities/math.ts +++ b/src/utilities/math.ts @@ -1,4 +1,4 @@ -function clamp(num, min, max) { +function clamp(num: number, min: number, max: number) { return Math.min(Math.max(num, min), max); } diff --git a/src/utilities/test/fakes.ts b/src/utilities/test/fakes.ts index 49e5c2a1..696b99d3 100644 --- a/src/utilities/test/fakes.ts +++ b/src/utilities/test/fakes.ts @@ -1,10 +1,14 @@ +import { Types } from 'ably'; + +import Space from '../../Space.js'; + import type { SpaceMember } from '../../types.js'; import type { PresenceMember } from '../../utilities/types.js'; // import { nanoidId } from '../../../__mocks__/nanoid/index.js'; const nanoidId = 'NanoidID'; -const enterPresenceMessage = { +const enterPresenceMessage: Types.PresenceMessage = { clientId: '1', data: { profileUpdate: { @@ -24,17 +28,23 @@ const enterPresenceMessage = { timestamp: 1, }; -const updatePresenceMessage = { +const updatePresenceMessage: Types.PresenceMessage = { ...enterPresenceMessage, action: 'update', }; -const leavePresenceMessage = { +const leavePresenceMessage: Types.PresenceMessage = { ...enterPresenceMessage, action: 'leave', }; -const createPresenceMessage = (type, override?) => { +type MessageMap = { + enter: typeof enterPresenceMessage; + update: typeof updatePresenceMessage; + leave: typeof leavePresenceMessage; +}; + +const createPresenceMessage = (type: T, override?: Partial) => { switch (type) { case 'enter': return { ...enterPresenceMessage, ...override }; @@ -47,7 +57,7 @@ const createPresenceMessage = (type, override?) => { } }; -const createPresenceEvent = (space, type, override?) => { +const createPresenceEvent = (space: Space, type: T, override?: Partial) => { space['onPresenceUpdate'](createPresenceMessage(type, override)); };