Skip to content

Commit

Permalink
RN: Rewrite EventEmitter
Browse files Browse the repository at this point in the history
Summary:
Rewrites `EventEmitter` as a simple, type-safe abstraction with a minimal interface.

The public interface of `EventEmitter` is unchanged. This rewrite was made possible only after deprecating and removing public methods that imposed restrictions on implementation details (e.g. deleting `removeListener`).

However, this includes a subtle breaking change that makes it behave the same as `EventEmitter` in Node.js and `EventTarget` in the DOM. The set of listeners being notified by `emit` will no longer be influenced by changes made during the course of notifying the existing listeners.

Changelog:
[General][Changed] - `EventEmitter#emit` now freezes the set of listeners before iterating over them, meaning listeners that are added or removed will not affect that iteration.

Reviewed By: javache

Differential Revision: D22153962

fbshipit-source-id: 81b87113590dee0296eff61374bf732171855453
  • Loading branch information
yungsters authored and facebook-github-bot committed Jun 18, 2022
1 parent 4bb551d commit e5c5dcd
Show file tree
Hide file tree
Showing 8 changed files with 558 additions and 507 deletions.
117 changes: 105 additions & 12 deletions Libraries/vendor/emitter/EventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,139 @@
* @format
*/

'use strict';
export interface EventSubscription {
remove(): void;
}

export interface IEventEmitter<TEventToArgsMap: {...}> {
addListener<TEvent: $Keys<TEventToArgsMap>>(
eventType: TEvent,
listener: (...args: $ElementType<TEventToArgsMap, TEvent>) => mixed,
context?: mixed,
): EventSubscription;

const EventEmitter = require('./_EventEmitter');
emit<TEvent: $Keys<TEventToArgsMap>>(
eventType: TEvent,
...args: $ElementType<TEventToArgsMap, TEvent>
): void;

import type {EventSubscription} from './EventSubscription';
removeAllListeners<TEvent: $Keys<TEventToArgsMap>>(eventType?: ?TEvent): void;

listenerCount<TEvent: $Keys<TEventToArgsMap>>(eventType: TEvent): number;
}

export default EventEmitter;
interface Registration<TArgs> {
+context: mixed;
+listener: (...args: TArgs) => mixed;
+remove: () => void;
}

export type {EventSubscription};
type Registry<TEventToArgsMap: {...}> = $ObjMap<
TEventToArgsMap,
<TArgs>(TArgs) => Set<Registration<TArgs>>,
>;

/**
* Essential interface for an EventEmitter.
* EventEmitter manages listeners and publishes events to them.
*
* EventEmitter accepts a single type parameter that defines the valid events
* and associated listener argument(s).
*
* @example
*
* const emitter = new EventEmitter<{
* success: [number, string],
* error: [Error],
* }>();
*
* emitter.on('success', (statusCode, responseText) => {...});
* emitter.emit('success', 200, '...');
*
* emitter.on('error', error => {...});
* emitter.emit('error', new Error('Resource not found'));
*
*/
export interface IEventEmitter<TEventToArgsMap: {...}> {
export default class EventEmitter<TEventToArgsMap: {...}>
implements IEventEmitter<TEventToArgsMap>
{
_registry: Registry<TEventToArgsMap> = {};

/**
* Registers a listener that is called when the supplied event is emitted.
* Returns a subscription that has a `remove` method to undo registration.
*/
addListener<TEvent: $Keys<TEventToArgsMap>>(
eventType: TEvent,
listener: (...args: $ElementType<TEventToArgsMap, TEvent>) => mixed,
context?: mixed,
): EventSubscription;
context: mixed,
): EventSubscription {
const registrations = allocate(this._registry, eventType);
const registration: Registration<$ElementType<TEventToArgsMap, TEvent>> = {
context,
listener,
remove(): void {
registrations.delete(registration);
},
};
registrations.add(registration);
return registration;
}

/**
* Emits the supplied event. Additional arguments supplied to `emit` will be
* passed through to each of the registered listeners.
*
* If a listener modifies the listeners registered for the same event, those
* changes will not be reflected in the current invocation of `emit`.
*/
emit<TEvent: $Keys<TEventToArgsMap>>(
eventType: TEvent,
...args: $ElementType<TEventToArgsMap, TEvent>
): void;
): void {
const registrations: ?Set<
Registration<$ElementType<TEventToArgsMap, TEvent>>,
> = this._registry[eventType];
if (registrations != null) {
for (const registration of [...registrations]) {
registration.listener.apply(registration.context, args);
}
}
}

/**
* Removes all registered listeners.
*/
removeAllListeners<TEvent: $Keys<TEventToArgsMap>>(eventType?: ?TEvent): void;
removeAllListeners<TEvent: $Keys<TEventToArgsMap>>(
eventType?: ?TEvent,
): void {
if (eventType == null) {
this._registry = {};
} else {
delete this._registry[eventType];
}
}

/**
* Returns the number of registered listeners for the supplied event.
*/
listenerCount<TEvent: $Keys<TEventToArgsMap>>(eventType: TEvent): number;
listenerCount<TEvent: $Keys<TEventToArgsMap>>(eventType: TEvent): number {
const registrations: ?Set<Registration<mixed>> = this._registry[eventType];
return registrations == null ? 0 : registrations.size;
}
}

function allocate<
TEventToArgsMap: {...},
TEvent: $Keys<TEventToArgsMap>,
TEventArgs: $ElementType<TEventToArgsMap, TEvent>,
>(
registry: Registry<TEventToArgsMap>,
eventType: TEvent,
): Set<Registration<TEventArgs>> {
let registrations: ?Set<Registration<TEventArgs>> = registry[eventType];
if (registrations == null) {
registrations = new Set();
registry[eventType] = registrations;
}
return registrations;
}
19 changes: 0 additions & 19 deletions Libraries/vendor/emitter/EventSubscription.js

This file was deleted.

62 changes: 0 additions & 62 deletions Libraries/vendor/emitter/_EmitterSubscription.js

This file was deleted.

144 changes: 0 additions & 144 deletions Libraries/vendor/emitter/_EventEmitter.js

This file was deleted.

Loading

0 comments on commit e5c5dcd

Please sign in to comment.