Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,27 @@ export declare interface RealtimePresence {
*/
unsubscribe(): void;

/**
* Returns all listeners currently registered on this presence object.
*
* @returns An array of listener functions for all events. Returns an empty array if no listeners are found.
*/
listeners(): Function[];
/**
* Returns the listeners for a specified presence action on this presence object.
*
* @param event - The presence action name to retrieve the listeners for.
* @returns An array of listener functions for the specified event. Returns an empty array if no listeners are found.
*/
listeners(event: string): Function[];
/**
* Returns the listeners for multiple specified presence actions on this presence object.
*
* @param events - An array of presence action names to retrieve the listeners for.
* @returns An array of listener functions for all the specified events combined. Returns an empty array if no listeners are found.
*/
listeners(events: string[]): Function[];

/**
* Retrieves the current members present on the channel and the metadata for each member, such as their {@link PresenceAction} and ID. Returns an array of {@link PresenceMessage} objects.
*
Expand Down Expand Up @@ -2934,6 +2955,27 @@ export declare interface RealtimeChannel extends EventEmitter<channelEventCallba
*/
unsubscribe(): void;

/**
* Returns all listeners currently registered on this channel.
*
* @returns An array of listener functions for all events. Returns an empty array if no listeners are found.
*/
listeners(): Function[];
/**
* Returns the listeners for a specified event name on this channel.
*
* @param event - The event name to retrieve the listeners for.
* @returns An array of listener functions for the specified event. Returns an empty array if no listeners are found.
*/
listeners(event: string): Function[];
/**
* Returns the listeners for multiple specified event names on this channel.
*
* @param events - An array of event names to retrieve the listeners for.
* @returns An array of listener functions for all the specified events combined. Returns an empty array if no listeners are found.
*/
listeners(events: string[]): Function[];
Comment on lines +2958 to +2977
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Type/behavior conflict with inherited EventEmitter.listeners.

RealtimeChannel extends EventEmitter<…> which already declares listeners(eventName?: EventType): CallbackType[] | null. Adding channel.listeners overloads returning Function[] conflicts:

  • Overload ambiguity for listeners() and listeners(string), with incompatible return types (Function[] vs CallbackType[] | null).
  • Runtime implementation returns message subscription listeners, not channel state listeners expected by EventEmitter.

Options:

  • Rename to messageListeners(…) (preferred), or
  • Keep name but add stateListeners(…) for channel state, and relax these overloads to Function[] | null to avoid type conflict, and
  • Update EventEmitter.listeners in this d.ts to accept arrays: listeners(eventName?: EventType | EventType[]): CallbackType[] | null.

As a minimal typing fix if you keep the name:

-  listeners(): Function[];
+  listeners(): Function[] | null;
-  listeners(event: string): Function[];
+  listeners(event: string): Function[] | null;
-  listeners(events: string[]): Function[];
+  listeners(events: string[]): Function[] | null;

Additionally, please add:

-  listeners(eventName?: EventType): CallbackType[] | null;
+  listeners(eventName?: EventType | EventType[]): CallbackType[] | null;

to the EventEmitter interface to reflect the new array support. Clarify in docs that RealtimeChannel.listeners() returns message subscription listeners, not channel state listeners, to prevent confusion.

To surface potential TS breakage from overload conflicts, run:


🏁 Script executed:

#!/bin/bash
# Show where both EventEmitter and RealtimeChannel listeners are referenced in d.ts
rg -n -C2 -P 'interface\s+RealtimeChannel|interface\s+EventEmitter<|listeners\(' ably.d.ts

Length of output: 2731


Fix type conflict in RealtimeChannel.listeners overloads
RealtimeChannel.listeners overloads clash with inherited EventEmitter.listeners(eventName?: EventType): CallbackType[] | null due to incompatible return types.

As a minimal typing fix if you keep the name:

-  listeners(): Function[];
+  listeners(): Function[] | null;
-  listeners(event: string): Function[];
+  listeners(event: string): Function[] | null;
-  listeners(events: string[]): Function[];
+  listeners(events: string[]): Function[] | null;

Additionally, please update EventEmitter to accept an array of event names:

-  listeners(eventName?: EventType): CallbackType[] | null;
+  listeners(eventName?: EventType | EventType[]): CallbackType[] | null;
  • Rename to messageListeners(…) (preferred) to avoid overload conflicts.
  • Clarify docs that .listeners() returns subscription listeners, not channel-state listeners.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Returns all listeners currently registered on this channel.
*
* @returns An array of listener functions for all events. Returns an empty array if no listeners are found.
*/
listeners(): Function[];
/**
* Returns the listeners for a specified event name on this channel.
*
* @param event - The event name to retrieve the listeners for.
* @returns An array of listener functions for the specified event. Returns an empty array if no listeners are found.
*/
listeners(event: string): Function[];
/**
* Returns the listeners for multiple specified event names on this channel.
*
* @param events - An array of event names to retrieve the listeners for.
* @returns An array of listener functions for all the specified events combined. Returns an empty array if no listeners are found.
*/
listeners(events: string[]): Function[];
/**
* Returns all listeners currently registered on this channel.
*
* @returns An array of listener functions for all events. Returns an empty array if no listeners are found.
*/
listeners(): Function[] | null;
/**
* Returns the listeners for a specified event name on this channel.
*
* @param event - The event name to retrieve the listeners for.
* @returns An array of listener functions for the specified event. Returns an empty array if no listeners are found.
*/
listeners(event: string): Function[] | null;
/**
* Returns the listeners for multiple specified event names on this channel.
*
* @param events - An array of event names to retrieve the listeners for.
* @returns An array of listener functions for all the specified events combined. Returns an empty array if no listeners are found.
*/
listeners(events: string[]): Function[] | null;
🤖 Prompt for AI Agents
In ably.d.ts around lines 2958-2977, the RealtimeChannel.listeners overloads
conflict with the inherited EventEmitter.listeners signature; rename the method
to messageListeners(...) throughout the file and update its JSDoc to state it
returns subscription (message) listeners not channel-state listeners; update all
call sites and exports accordingly; if you must keep the name instead, change
the overloads to a single signature compatible with EventEmitter (e.g.
listeners(event?: string | string[]): Function[] | null) and adjust the
EventEmitter type to accept an array of event names (event?: string | string[])
so return types align.


/**
* A {@link RealtimePresence} object.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/common/lib/client/realtimechannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,11 @@ class RealtimeChannel extends EventEmitter {
this.subscriptions.off(event, listener);
}

listeners(event?: string | string[]): Function[] {
const result = this.subscriptions.listeners(event);
return result || [];
}

sync(): void {
/* check preconditions */
switch (this.state) {
Expand Down
5 changes: 5 additions & 0 deletions src/common/lib/client/realtimepresence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,11 @@ class RealtimePresence extends EventEmitter {
const listener = args[1];
this.subscriptions.off(event, listener);
}

listeners(event?: string | string[]): Function[] {
const result = this.subscriptions.listeners(event);
return result || [];
}
}

export default RealtimePresence;
15 changes: 13 additions & 2 deletions src/common/lib/util/eventemitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,21 @@ class EventEmitter {

/**
* Get the array of listeners for a given event; excludes once events
* @param event (optional) the name of the event, or none for 'any'
* @param event (optional) the name of the event, array of event names, or none for 'any'
* @return array of events, or null if none
*/
listeners(event: string) {
listeners(event?: string | string[]) {
if (Array.isArray(event)) {
const allListeners: Function[] = [];
event.forEach((eventName) => {
const listeners = this.listeners(eventName);
if (listeners) {
allListeners.push(...listeners);
}
});
return allListeners.length ? allListeners : null;
}

Comment on lines +181 to +195
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: listeners() mutates internal state and breaks once semantics.

In the single-event branch (below), you push eventsOnce[event] into the persistent array returned by this.events[event], which permanently converts once listeners into persistent listeners whenever listeners() is called. This PR introduces more public usages of listeners(), increasing the likelihood of this bug surfacing.

Fix by returning copies and never mutating internal arrays. Also consider de‑duping when aggregating arrays.

Apply:

-  listeners(event?: string | string[]) {
-    if (Array.isArray(event)) {
-      const allListeners: Function[] = [];
-      event.forEach((eventName) => {
-        const listeners = this.listeners(eventName);
-        if (listeners) {
-          allListeners.push(...listeners);
-        }
-      });
-      return allListeners.length ? allListeners : null;
-    }
-
-    if (event) {
-      const listeners = this.events[event] || [];
-      if (this.eventsOnce[event]) Array.prototype.push.apply(listeners, this.eventsOnce[event]);
-      return listeners.length ? listeners : null;
-    }
-    return this.any.length ? this.any : null;
-  }
+  listeners(event?: string | string[]) {
+    if (Array.isArray(event)) {
+      const set = new Set<Function>();
+      event.forEach((eventName) => {
+        const ls = this.listeners(eventName);
+        if (ls) ls.forEach((l) => set.add(l));
+      });
+      return set.size ? Array.from(set) : null;
+    }
+
+    if (event) {
+      const persistent = this.events[event] || [];
+      const once = this.eventsOnce[event] || [];
+      const combinedLen = persistent.length + once.length;
+      return combinedLen ? persistent.slice().concat(once) : null;
+    }
+    return this.any.length ? this.any.slice() : null;
+  }

Note: The doc comment above still states “excludes once events”, which is inaccurate; update accordingly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param event (optional) the name of the event, array of event names, or none for 'any'
* @return array of events, or null if none
*/
listeners(event: string) {
listeners(event?: string | string[]) {
if (Array.isArray(event)) {
const allListeners: Function[] = [];
event.forEach((eventName) => {
const listeners = this.listeners(eventName);
if (listeners) {
allListeners.push(...listeners);
}
});
return allListeners.length ? allListeners : null;
}
listeners(event?: string | string[]) {
if (Array.isArray(event)) {
const set = new Set<Function>();
event.forEach((eventName) => {
const ls = this.listeners(eventName);
if (ls) ls.forEach((l) => set.add(l));
});
return set.size ? Array.from(set) : null;
}
if (event) {
const persistent = this.events[event] || [];
const once = this.eventsOnce[event] || [];
const combinedLen = persistent.length + once.length;
return combinedLen ? persistent.slice().concat(once) : null;
}
return this.any.length ? this.any.slice() : null;
}
🤖 Prompt for AI Agents
In src/common/lib/util/eventemitter.ts around lines 181-195, listeners()
currently mutates internal arrays by pushing once-listeners into the persistent
this.events[event] array and thus converts once listeners into permanent ones;
change it to never mutate internal state: when returning listeners for a single
event, return a shallow copy (e.g., concatenate copies of persistent and once
arrays) instead of pushing into stored arrays; when handling an array of events,
aggregate copies from each event and de-duplicate the combined list (e.g., via a
Set) before returning; ensure the function still returns null when the resulting
list is empty and update the doc comment above to accurately state whether once
listeners are included.

if (event) {
const listeners = this.events[event] || [];
if (this.eventsOnce[event]) Array.prototype.push.apply(listeners, this.eventsOnce[event]);
Expand Down
Loading