18d307f52SEvan Baconimport { ExpoConfig, getConfig } from '@expo/config';
28d307f52SEvan Bacon
3edc92349SJuwan Wheatleyimport {
4edc92349SJuwan Wheatley  closeDevelopmentSessionAsync,
5edc92349SJuwan Wheatley  updateDevelopmentSessionAsync,
6edc92349SJuwan Wheatley} from '../../api/updateDevelopmentSession';
78d307f52SEvan Baconimport { getUserAsync } from '../../api/user/user';
8*e32ccf9fSEvan Baconimport { env } from '../../utils/env';
98d307f52SEvan Baconimport * as ProjectDevices from '../project/devices';
108d307f52SEvan Bacon
11474a7a4bSEvan Baconconst debug = require('debug')('expo:start:server:developmentSession') as typeof console.log;
12474a7a4bSEvan Bacon
138d307f52SEvan Baconconst UPDATE_FREQUENCY = 20 * 1000; // 20 seconds
148d307f52SEvan Bacon
158d307f52SEvan Baconasync function isAuthenticatedAsync(): Promise<boolean> {
168d307f52SEvan Bacon  return !!(await getUserAsync().catch(() => null));
178d307f52SEvan Bacon}
188d307f52SEvan Bacon
198d307f52SEvan Baconexport class DevelopmentSession {
2081e9e3beSEvan Bacon  protected timeout: NodeJS.Timeout | null = null;
218d307f52SEvan Bacon
228d307f52SEvan Bacon  constructor(
238d307f52SEvan Bacon    /** Project root directory. */
248d307f52SEvan Bacon    private projectRoot: string,
258d307f52SEvan Bacon    /** Development Server URL. */
2681e9e3beSEvan Bacon    public url: string | null,
2781e9e3beSEvan Bacon    /** Catch any errors that may occur during the `startAsync` method. */
2881e9e3beSEvan Bacon    private onError: (error: Error) => void
298d307f52SEvan Bacon  ) {}
308d307f52SEvan Bacon
318d307f52SEvan Bacon  /**
328d307f52SEvan Bacon   * Notify the Expo servers that a project is running, this enables the Expo Go app
338d307f52SEvan Bacon   * and Dev Clients to offer a "recently in development" section for quick access.
348d307f52SEvan Bacon   *
358d307f52SEvan Bacon   * This method starts an interval that will continue to ping the servers until we stop it.
368d307f52SEvan Bacon   *
378d307f52SEvan Bacon   * @param projectRoot Project root folder, used for retrieving device installation IDs.
388d307f52SEvan Bacon   * @param props.exp Partial Expo config with values that will be used in the Expo Go app.
398d307f52SEvan Bacon   * @param props.runtime which runtime the app should be opened in. `native` for dev clients, `web` for web browsers.
408d307f52SEvan Bacon   */
418d307f52SEvan Bacon  public async startAsync({
428d307f52SEvan Bacon    exp = getConfig(this.projectRoot).exp,
438d307f52SEvan Bacon    runtime,
448d307f52SEvan Bacon  }: {
458d307f52SEvan Bacon    exp?: Pick<ExpoConfig, 'name' | 'description' | 'slug' | 'primaryColor'>;
468d307f52SEvan Bacon    runtime: 'native' | 'web';
478d307f52SEvan Bacon  }): Promise<void> {
4881e9e3beSEvan Bacon    try {
49*e32ccf9fSEvan Bacon      if (env.EXPO_OFFLINE) {
50*e32ccf9fSEvan Bacon        debug(
51*e32ccf9fSEvan Bacon          'This project will not be suggested in Expo Go or Dev Clients because Expo CLI is running in offline-mode.'
52*e32ccf9fSEvan Bacon        );
53edc92349SJuwan Wheatley        this.stopNotifying();
548d307f52SEvan Bacon        return;
558d307f52SEvan Bacon      }
568d307f52SEvan Bacon
578d307f52SEvan Bacon      const deviceIds = await this.getDeviceInstallationIdsAsync();
588d307f52SEvan Bacon
598d307f52SEvan Bacon      if (!(await isAuthenticatedAsync()) && !deviceIds?.length) {
60474a7a4bSEvan Bacon        debug(
61474a7a4bSEvan Bacon          'Development session will not ping because the user is not authenticated and there are no devices.'
62474a7a4bSEvan Bacon        );
63edc92349SJuwan Wheatley        this.stopNotifying();
648d307f52SEvan Bacon        return;
658d307f52SEvan Bacon      }
668d307f52SEvan Bacon
6729975bfdSEvan Bacon      if (this.url) {
68474a7a4bSEvan Bacon        debug(`Development session ping (runtime: ${runtime}, url: ${this.url})`);
6981e9e3beSEvan Bacon
708d307f52SEvan Bacon        await updateDevelopmentSessionAsync({
718d307f52SEvan Bacon          url: this.url,
728d307f52SEvan Bacon          runtime,
738d307f52SEvan Bacon          exp,
748d307f52SEvan Bacon          deviceIds,
758d307f52SEvan Bacon        });
7629975bfdSEvan Bacon      }
778d307f52SEvan Bacon
78edc92349SJuwan Wheatley      this.stopNotifying();
798d307f52SEvan Bacon
808d307f52SEvan Bacon      this.timeout = setTimeout(() => this.startAsync({ exp, runtime }), UPDATE_FREQUENCY);
8181e9e3beSEvan Bacon    } catch (error: any) {
8281e9e3beSEvan Bacon      debug(`Error updating development session API: ${error}`);
8381e9e3beSEvan Bacon      this.stopNotifying();
8481e9e3beSEvan Bacon      this.onError(error);
8581e9e3beSEvan Bacon    }
868d307f52SEvan Bacon  }
878d307f52SEvan Bacon
888d307f52SEvan Bacon  /** Get all recent devices for the project. */
898d307f52SEvan Bacon  private async getDeviceInstallationIdsAsync(): Promise<string[]> {
908d307f52SEvan Bacon    const { devices } = await ProjectDevices.getDevicesInfoAsync(this.projectRoot);
918d307f52SEvan Bacon    return devices.map(({ installationId }) => installationId);
928d307f52SEvan Bacon  }
938d307f52SEvan Bacon
948d307f52SEvan Bacon  /** Stop notifying the Expo servers that the development session is running. */
95edc92349SJuwan Wheatley  public stopNotifying() {
9629975bfdSEvan Bacon    if (this.timeout) {
978d307f52SEvan Bacon      clearTimeout(this.timeout);
9829975bfdSEvan Bacon    }
998d307f52SEvan Bacon    this.timeout = null;
1008d307f52SEvan Bacon  }
101edc92349SJuwan Wheatley
102edc92349SJuwan Wheatley  public async closeAsync(): Promise<void> {
103edc92349SJuwan Wheatley    this.stopNotifying();
104edc92349SJuwan Wheatley
105edc92349SJuwan Wheatley    const deviceIds = await this.getDeviceInstallationIdsAsync();
106edc92349SJuwan Wheatley
107edc92349SJuwan Wheatley    if (!(await isAuthenticatedAsync()) && !deviceIds?.length) {
108edc92349SJuwan Wheatley      return;
109edc92349SJuwan Wheatley    }
110edc92349SJuwan Wheatley
111edc92349SJuwan Wheatley    if (this.url) {
112edc92349SJuwan Wheatley      await closeDevelopmentSessionAsync({
113edc92349SJuwan Wheatley        url: this.url,
114edc92349SJuwan Wheatley        deviceIds,
115edc92349SJuwan Wheatley      });
116edc92349SJuwan Wheatley    }
117edc92349SJuwan Wheatley  }
1188d307f52SEvan Bacon}
119