socket.io icon indicating copy to clipboard operation
socket.io copied to clipboard

socket listen with AbortSignal

Open ZachHaber opened this issue 1 year ago • 2 comments

Is your feature request related to a problem? Please describe. When setting up an listener, you end up having to define your callback in a separate variable in order to be able to clear the listener. This makes it so that you lose the convenient typing that the socket.on<Ev> generic provides.

Describe the solution you'd like I'd like an AbortSignal to be able to be passed into the socket.on function via a third options argument. The options can mirror the DOM api's AddEventListenerOptions:

{
  once?: boolean;
  signal?: AbortSignal;
}

When the signal fires, the listener would be removed. This will make it much more convenient to cleanup the listeners, and could potentially allow for cleaning up multiple listeners with a single controller call. This also aligns with the trend towards adopting AbortControllers in various APIs (axios ,fetch ,DOM eventListeners, node's timer promises, and likely more).

Describe alternatives you've considered It isn't complicated to set up a wrapper function to accomplish this from a user side. So maybe adding something like this to the documentation could suffice?

JavaScript Variant
/**
 * @typedef SocketOnOptions
 * @property {AbortSignal} [signal] If an AbortSignal is passed for signal, then the event listener will be removed when signal is aborted.
 * @property {boolean} [once] When set to true, once indicates that the callback will only be invoked once after which the event listener will be removed.
 */

/**
 * Adds the listener function as an event listener for ev.
 *
 * This allows for convenience of passing options including a 
 * {@link SocketOnOptions.signal `signal`} to remove the listener when desired
 *
 * @template {Parameters<typeof socket.on>[0]} Ev Name of the event
 * @param {Ev} ev Name of the event
 * @param {Parameters<typeof socket.on<Ev>>[1]} listener Callback function
 * @param {SocketOnOptions} [param2]
 */
export function socketOn(
  ev,
  listener,
  { signal, once } = {}
) {
  if (once) {
    socket.once(ev, listener);
  } else {
    socket.on(ev, listener);
  }
  signal?.addEventListener(
    "abort",
    () => {
      socket.off(ev, listener);
    },
    { once: true }
  );
}
TypeScript Variant
export interface SocketOnOptions {
  /**
   * If an AbortSignal is passed for signal, then the event listener will be removed when signal is aborted.
   */
  signal?: AbortSignal;
  /**
   * When set to true, once indicates that the callback will only be invoked once after which the event listener will be removed.
   */
  once?: boolean;
}
/**
 * Adds the listener function as an event listener for ev.
 *
 * This allows for convenience of passing options including a 
 * {@link SocketOnOptions.signal `signal`} to remove the listener when desired
 *
 * @param ev  Name of the event
 * @param listener Callback function
 * @param options
 */
export function socketOn<Ev extends Parameters<typeof socket.on>[0]>(
  ev: Ev,
  listener: Parameters<typeof socket.on<Ev>>[1],
  { signal, once }: SocketOnOptions = {}
) {
  if (once) {
    socket.once(ev, listener);
  } else {
    socket.on(ev, listener);
  }
  signal?.addEventListener(
    "abort",
    () => {
      socket.off(ev, listener);
    },
    { once: true }
  );
}

ZachHaber avatar Oct 13 '23 02:10 ZachHaber