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