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