1import { Subscription } from 'expo-modules-core';
2
3import ExpoLocation from './ExpoLocation';
4import { LocationCallback, LocationHeadingCallback } from './Location.types';
5import { LocationEventEmitter } from './LocationEventEmitter';
6
7type EventObject = {
8  watchId: number;
9  [key: string]: any;
10};
11
12let nextWatchId = 0;
13
14class Subscriber<CallbackType extends LocationCallback | LocationHeadingCallback> {
15  private eventName: string;
16  private eventDataField: string;
17  private callbacks: { [id: string]: CallbackType } = {};
18  private eventSubscription: Subscription | null = null;
19
20  constructor(eventName: string, eventDataField: string) {
21    this.eventName = eventName;
22    this.eventDataField = eventDataField;
23  }
24
25  maybeInitializeSubscription() {
26    if (this.eventSubscription) {
27      return;
28    }
29    this.eventSubscription = LocationEventEmitter.addListener(
30      this.eventName,
31      (event: EventObject) => this.trigger(event)
32    );
33  }
34
35  /**
36   * Registers given callback under new id which is then returned.
37   */
38  registerCallback(callback: CallbackType): number {
39    this.maybeInitializeSubscription();
40    const id = ++nextWatchId;
41    this.callbacks[id] = callback;
42    return id;
43  }
44
45  /**
46   * Unregisters a callback with given id and revokes the subscription if possible.
47   */
48  unregisterCallback(id: number): void {
49    // Do nothing if we have already unregistered the callback.
50    if (!this.callbacks[id]) {
51      return;
52    }
53
54    delete this.callbacks[id];
55    ExpoLocation.removeWatchAsync(id);
56
57    if (Object.keys(this.callbacks).length === 0 && this.eventSubscription) {
58      LocationEventEmitter.removeSubscription(this.eventSubscription);
59      this.eventSubscription = null;
60    }
61  }
62
63  trigger(event: EventObject): void {
64    const watchId = event.watchId;
65    const callback = this.callbacks[watchId];
66
67    if (callback) {
68      callback(event[this.eventDataField]);
69    } else {
70      ExpoLocation.removeWatchAsync(watchId);
71    }
72  }
73}
74
75export const LocationSubscriber = new Subscriber<LocationCallback>(
76  'Expo.locationChanged',
77  'location'
78);
79export const HeadingSubscriber = new Subscriber<LocationHeadingCallback>(
80  'Expo.headingChanged',
81  'heading'
82);
83
84/**
85 * @private Necessary for some unit tests.
86 */
87export function _getCurrentWatchId(): number {
88  return nextWatchId;
89}
90