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