1import { ExpoConfig, getConfig } from '@expo/config';
2
3import { APISettings } from '../../api/settings';
4import {
5  closeDevelopmentSessionAsync,
6  updateDevelopmentSessionAsync,
7} from '../../api/updateDevelopmentSession';
8import { getUserAsync } from '../../api/user/user';
9import * as ProjectDevices from '../project/devices';
10
11const UPDATE_FREQUENCY = 20 * 1000; // 20 seconds
12
13async function isAuthenticatedAsync(): Promise<boolean> {
14  return !!(await getUserAsync().catch(() => null));
15}
16
17export class DevelopmentSession {
18  private timeout: NodeJS.Timeout | null = null;
19
20  constructor(
21    /** Project root directory. */
22    private projectRoot: string,
23    /** Development Server URL. */
24    public url: string | null
25  ) {}
26
27  /**
28   * Notify the Expo servers that a project is running, this enables the Expo Go app
29   * and Dev Clients to offer a "recently in development" section for quick access.
30   *
31   * This method starts an interval that will continue to ping the servers until we stop it.
32   *
33   * @param projectRoot Project root folder, used for retrieving device installation IDs.
34   * @param props.exp Partial Expo config with values that will be used in the Expo Go app.
35   * @param props.runtime which runtime the app should be opened in. `native` for dev clients, `web` for web browsers.
36   * @returns
37   */
38  public async startAsync({
39    exp = getConfig(this.projectRoot).exp,
40    runtime,
41  }: {
42    exp?: Pick<ExpoConfig, 'name' | 'description' | 'slug' | 'primaryColor'>;
43    runtime: 'native' | 'web';
44  }): Promise<void> {
45    if (APISettings.isOffline) {
46      this.stopNotifying();
47      return;
48    }
49
50    const deviceIds = await this.getDeviceInstallationIdsAsync();
51
52    if (!(await isAuthenticatedAsync()) && !deviceIds?.length) {
53      this.stopNotifying();
54      return;
55    }
56
57    if (this.url) {
58      await updateDevelopmentSessionAsync({
59        url: this.url,
60        runtime,
61        exp,
62        deviceIds,
63      });
64    }
65
66    this.stopNotifying();
67
68    this.timeout = setTimeout(() => this.startAsync({ exp, runtime }), UPDATE_FREQUENCY);
69  }
70
71  /** Get all recent devices for the project. */
72  private async getDeviceInstallationIdsAsync(): Promise<string[]> {
73    const { devices } = await ProjectDevices.getDevicesInfoAsync(this.projectRoot);
74    return devices.map(({ installationId }) => installationId);
75  }
76
77  /** Stop notifying the Expo servers that the development session is running. */
78  public stopNotifying() {
79    if (this.timeout) {
80      clearTimeout(this.timeout);
81    }
82    this.timeout = null;
83  }
84
85  public async closeAsync(): Promise<void> {
86    this.stopNotifying();
87
88    const deviceIds = await this.getDeviceInstallationIdsAsync();
89
90    if (!(await isAuthenticatedAsync()) && !deviceIds?.length) {
91      return;
92    }
93
94    if (this.url) {
95      await closeDevelopmentSessionAsync({
96        url: this.url,
97        deviceIds,
98      });
99    }
100  }
101}
102