1import invariant from 'invariant';
2import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
3
4const nativeEmitterSubscriptionKey = '@@nativeEmitterSubscription@@';
5
6type NativeModule = {
7  __expo_module_name__?: string;
8  startObserving?: () => void;
9  stopObserving?: () => void;
10  addListener: (eventName: string) => void;
11  removeListeners: (count: number) => void;
12};
13
14// @needsAudit
15export type Subscription = {
16  /**
17   * A method to unsubscribe the listener.
18   */
19  remove: () => void;
20};
21
22export class EventEmitter {
23  _listenerCount = 0;
24  _nativeModule: NativeModule;
25  _eventEmitter: NativeEventEmitter;
26
27  constructor(nativeModule: NativeModule) {
28    // Expo modules installed through the JSI don't have `addListener` and `removeListeners` set,
29    // so if someone wants to use them with `EventEmitter`, make sure to provide these functions
30    // as they are required by `NativeEventEmitter`. This is only temporary — in the future
31    // JSI modules will have event emitter built in.
32    if (nativeModule.__expo_module_name__ && NativeModules.EXReactNativeEventEmitter) {
33      nativeModule.addListener = (...args) =>
34        NativeModules.EXReactNativeEventEmitter.addProxiedListener(
35          nativeModule.__expo_module_name__,
36          ...args
37        );
38      nativeModule.removeListeners = (...args) =>
39        NativeModules.EXReactNativeEventEmitter.removeProxiedListeners(
40          nativeModule.__expo_module_name__,
41          ...args
42        );
43    }
44    this._nativeModule = nativeModule;
45    this._eventEmitter = new NativeEventEmitter(nativeModule as any);
46  }
47
48  addListener<T>(eventName: string, listener: (event: T) => void): Subscription {
49    if (!this._listenerCount && Platform.OS !== 'ios' && this._nativeModule.startObserving) {
50      this._nativeModule.startObserving();
51    }
52
53    this._listenerCount++;
54    const nativeEmitterSubscription = this._eventEmitter.addListener(eventName, listener);
55    const subscription = {
56      [nativeEmitterSubscriptionKey]: nativeEmitterSubscription,
57      remove: () => {
58        this.removeSubscription(subscription);
59      },
60    };
61    return subscription;
62  }
63
64  removeAllListeners(eventName: string): void {
65    // @ts-ignore: the EventEmitter interface has been changed in [email protected]
66    const removedListenerCount = this._eventEmitter.listenerCount
67      ? // @ts-ignore: this is available since 0.64
68        this._eventEmitter.listenerCount(eventName)
69      : // @ts-ignore: this is available in older versions
70        this._eventEmitter.listeners(eventName).length;
71    this._eventEmitter.removeAllListeners(eventName);
72    this._listenerCount -= removedListenerCount;
73    invariant(
74      this._listenerCount >= 0,
75      `EventEmitter must have a non-negative number of listeners`
76    );
77
78    if (!this._listenerCount && Platform.OS !== 'ios' && this._nativeModule.stopObserving) {
79      this._nativeModule.stopObserving();
80    }
81  }
82
83  removeSubscription(subscription: Subscription): void {
84    const nativeEmitterSubscription = subscription[nativeEmitterSubscriptionKey];
85    if (!nativeEmitterSubscription) {
86      return;
87    }
88
89    if ('remove' in nativeEmitterSubscription) {
90      // `[email protected]` doesn't support `removeSubscription`
91      nativeEmitterSubscription.remove();
92    } else if ('removeSubscription' in this._eventEmitter) {
93      this._eventEmitter.removeSubscription(nativeEmitterSubscription!);
94    }
95    this._listenerCount--;
96
97    // Ensure that the emitter's internal state remains correct even if `removeSubscription` is
98    // called again with the same subscription
99    delete subscription[nativeEmitterSubscriptionKey];
100
101    // Release closed-over references to the emitter
102    subscription.remove = () => {};
103
104    if (!this._listenerCount && Platform.OS !== 'ios' && this._nativeModule.stopObserving) {
105      this._nativeModule.stopObserving();
106    }
107  }
108
109  emit(eventName: string, ...params: any[]): void {
110    this._eventEmitter.emit(eventName, ...params);
111  }
112}
113