addons/shared/TypedEventEmitter.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318class EventWithPayload<T> extends Event {
b69ab319 constructor(
b69ab3110 type: string,
b69ab3111 public data: T | Error,
b69ab3112 ) {
b69ab3113 super(type);
b69ab3114 }
b69ab3115}
b69ab3116
b69ab3117type TypedListenerParam<T> = ((data: T) => void) | ((err: Error) => void);
b69ab3118
b69ab3119/**
b69ab3120 * Like {@link EventEmitter} / {@link EventTarget}, but with type checking for one particular subscription type,
b69ab3121 * plus errors. uses EventTarget so it works in browser and node.
b69ab3122 * ```
b69ab3123 * const myEmitter = new TypedEventEmitter<'data', number>();
b69ab3124 * myEmitter.on('data', (data: number) => ...); // typechecks 'data' param and callback
b69ab3125 * myEmitter.on('error', (error: Error) => ...); // errors are always allowed too
b69ab3126 * // Fields other than 'data' and 'error' are type errors.
b69ab3127 * ```
b69ab3128 */
b69ab3129export class TypedEventEmitter<EventName extends string, EventType> {
b69ab3130 private listeners: {[key: string]: Map<TypedListenerParam<EventType>, EventListener>} = {};
b69ab3131
b69ab3132 private target = new EventTarget();
b69ab3133
b69ab3134 on(event: EventName, listener: (data: EventType) => void): this;
b69ab3135 on(event: 'error', listener: (err: Error) => void): this;
b69ab3136 on(event: EventName | 'error', listener: TypedListenerParam<EventType>): this {
b69ab3137 const map = this.getOrCreateMap(event);
b69ab3138 let found = map.get(listener);
b69ab3139 if (found == null) {
b69ab3140 found = (event: Event | EventWithPayload<EventType>) => {
b69ab3141 if ('data' in event) {
b69ab3142 (listener as (data: EventType | Error) => void)(event.data);
b69ab3143 }
b69ab3144 };
b69ab3145 map.set(listener, found);
b69ab3146 }
b69ab3147 this.target.addEventListener(event as EventName | 'error', found);
b69ab3148 return this;
b69ab3149 }
b69ab3150
b69ab3151 off(event: EventName, listener: (data: EventType) => void): this;
b69ab3152 off(event: 'error', listener: (err: Error) => void): this;
b69ab3153 off(event: EventName | 'error', listener: TypedListenerParam<EventType>): this {
b69ab3154 const map = this.getOrCreateMap(event);
b69ab3155 const found = map.get(listener);
b69ab3156 if (found == null) {
b69ab3157 return this;
b69ab3158 }
b69ab3159 map.delete(listener);
b69ab3160 this.target.removeEventListener(event as EventName | 'error', found);
b69ab3161 return this;
b69ab3162 }
b69ab3163
b69ab3164 emit(event: EventName): EventType extends undefined ? boolean : never;
b69ab3165 emit(event: EventName, data: EventType): boolean;
b69ab3166 emit(event: 'error', data: Error): boolean;
b69ab3167
b69ab3168 emit(name: EventName | 'error', data?: EventType | Error): boolean {
b69ab3169 const event = new EventWithPayload(name, data);
b69ab3170 if (!this.target.dispatchEvent(event)) {
b69ab3171 return false;
b69ab3172 }
b69ab3173 return true;
b69ab3174 }
b69ab3175
b69ab3176 private getOrCreateMap(event: EventName | 'error') {
b69ab3177 return (this.listeners[event] ??= new Map());
b69ab3178 }
b69ab3179
b69ab3180 removeAllListeners() {
b69ab3181 for (const [key, map] of Object.entries(this.listeners)) {
b69ab3182 for (const listener of map.values()) {
b69ab3183 this.target.removeEventListener(key, listener);
b69ab3184 }
b69ab3185 map.clear();
b69ab3186 }
b69ab3187 }
b69ab3188
b69ab3189 /** Get an EventTarget-compatible object while still being able to use the typed APIs */
b69ab3190 asEventTarget(): EventTarget {
b69ab3191 const listeners = new Map<(event: Event) => void, (data: EventType) => void>();
b69ab3192 return {
b69ab3193 addEventListener: (type: EventName, handler: (event: Event) => void) => {
b69ab3194 const wrapped = (data: EventType) => {
b69ab3195 handler(new EventWithPayload<EventType>(type, data));
b69ab3196 };
b69ab3197 listeners.set(handler, wrapped);
b69ab3198 this.on(type as EventName, wrapped);
b69ab3199 },
b69ab31100 removeEventListener: (type: EventName, handler: (event: Event) => void) => {
b69ab31101 const existing = listeners.get(handler);
b69ab31102 if (existing) {
b69ab31103 this.off(type as EventName, existing);
b69ab31104 listeners.delete(handler);
b69ab31105 }
b69ab31106 },
b69ab31107 dispatchEvent: (event: EventWithPayload<EventType>) =>
b69ab31108 this.emit(event.type as EventName, event.data as EventType),
b69ab31109 } as EventTarget;
b69ab31110 }
b69ab31111}