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