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 debug = require('debug')('expo:start:server:developmentSession') as typeof console.log; 12 13const UPDATE_FREQUENCY = 20 * 1000; // 20 seconds 14 15async function isAuthenticatedAsync(): Promise<boolean> { 16 return !!(await getUserAsync().catch(() => null)); 17} 18 19export class DevelopmentSession { 20 protected timeout: NodeJS.Timeout | null = null; 21 22 constructor( 23 /** Project root directory. */ 24 private projectRoot: string, 25 /** Development Server URL. */ 26 public url: string | null, 27 /** Catch any errors that may occur during the `startAsync` method. */ 28 private onError: (error: Error) => void 29 ) {} 30 31 /** 32 * Notify the Expo servers that a project is running, this enables the Expo Go app 33 * and Dev Clients to offer a "recently in development" section for quick access. 34 * 35 * This method starts an interval that will continue to ping the servers until we stop it. 36 * 37 * @param projectRoot Project root folder, used for retrieving device installation IDs. 38 * @param props.exp Partial Expo config with values that will be used in the Expo Go app. 39 * @param props.runtime which runtime the app should be opened in. `native` for dev clients, `web` for web browsers. 40 */ 41 public async startAsync({ 42 exp = getConfig(this.projectRoot).exp, 43 runtime, 44 }: { 45 exp?: Pick<ExpoConfig, 'name' | 'description' | 'slug' | 'primaryColor'>; 46 runtime: 'native' | 'web'; 47 }): Promise<void> { 48 try { 49 if (APISettings.isOffline) { 50 debug('Development session will not ping because the server is offline.'); 51 this.stopNotifying(); 52 return; 53 } 54 55 const deviceIds = await this.getDeviceInstallationIdsAsync(); 56 57 if (!(await isAuthenticatedAsync()) && !deviceIds?.length) { 58 debug( 59 'Development session will not ping because the user is not authenticated and there are no devices.' 60 ); 61 this.stopNotifying(); 62 return; 63 } 64 65 if (this.url) { 66 debug(`Development session ping (runtime: ${runtime}, url: ${this.url})`); 67 68 await updateDevelopmentSessionAsync({ 69 url: this.url, 70 runtime, 71 exp, 72 deviceIds, 73 }); 74 } 75 76 this.stopNotifying(); 77 78 this.timeout = setTimeout(() => this.startAsync({ exp, runtime }), UPDATE_FREQUENCY); 79 } catch (error: any) { 80 debug(`Error updating development session API: ${error}`); 81 this.stopNotifying(); 82 this.onError(error); 83 } 84 } 85 86 /** Get all recent devices for the project. */ 87 private async getDeviceInstallationIdsAsync(): Promise<string[]> { 88 const { devices } = await ProjectDevices.getDevicesInfoAsync(this.projectRoot); 89 return devices.map(({ installationId }) => installationId); 90 } 91 92 /** Stop notifying the Expo servers that the development session is running. */ 93 public stopNotifying() { 94 if (this.timeout) { 95 clearTimeout(this.timeout); 96 } 97 this.timeout = null; 98 } 99 100 public async closeAsync(): Promise<void> { 101 this.stopNotifying(); 102 103 const deviceIds = await this.getDeviceInstallationIdsAsync(); 104 105 if (!(await isAuthenticatedAsync()) && !deviceIds?.length) { 106 return; 107 } 108 109 if (this.url) { 110 await closeDevelopmentSessionAsync({ 111 url: this.url, 112 deviceIds, 113 }); 114 } 115 } 116} 117