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 private 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 ) {} 28 29 /** 30 * Notify the Expo servers that a project is running, this enables the Expo Go app 31 * and Dev Clients to offer a "recently in development" section for quick access. 32 * 33 * This method starts an interval that will continue to ping the servers until we stop it. 34 * 35 * @param projectRoot Project root folder, used for retrieving device installation IDs. 36 * @param props.exp Partial Expo config with values that will be used in the Expo Go app. 37 * @param props.runtime which runtime the app should be opened in. `native` for dev clients, `web` for web browsers. 38 * @returns 39 */ 40 public async startAsync({ 41 exp = getConfig(this.projectRoot).exp, 42 runtime, 43 }: { 44 exp?: Pick<ExpoConfig, 'name' | 'description' | 'slug' | 'primaryColor'>; 45 runtime: 'native' | 'web'; 46 }): Promise<void> { 47 if (APISettings.isOffline) { 48 debug('Development session will not ping because the server is offline.'); 49 this.stopNotifying(); 50 return; 51 } 52 53 const deviceIds = await this.getDeviceInstallationIdsAsync(); 54 55 if (!(await isAuthenticatedAsync()) && !deviceIds?.length) { 56 debug( 57 'Development session will not ping because the user is not authenticated and there are no devices.' 58 ); 59 this.stopNotifying(); 60 return; 61 } 62 63 if (this.url) { 64 debug(`Development session ping (runtime: ${runtime}, url: ${this.url})`); 65 await updateDevelopmentSessionAsync({ 66 url: this.url, 67 runtime, 68 exp, 69 deviceIds, 70 }); 71 } 72 73 this.stopNotifying(); 74 75 this.timeout = setTimeout(() => this.startAsync({ exp, runtime }), UPDATE_FREQUENCY); 76 } 77 78 /** Get all recent devices for the project. */ 79 private async getDeviceInstallationIdsAsync(): Promise<string[]> { 80 const { devices } = await ProjectDevices.getDevicesInfoAsync(this.projectRoot); 81 return devices.map(({ installationId }) => installationId); 82 } 83 84 /** Stop notifying the Expo servers that the development session is running. */ 85 public stopNotifying() { 86 if (this.timeout) { 87 clearTimeout(this.timeout); 88 } 89 this.timeout = null; 90 } 91 92 public async closeAsync(): Promise<void> { 93 this.stopNotifying(); 94 95 const deviceIds = await this.getDeviceInstallationIdsAsync(); 96 97 if (!(await isAuthenticatedAsync()) && !deviceIds?.length) { 98 return; 99 } 100 101 if (this.url) { 102 await closeDevelopmentSessionAsync({ 103 url: this.url, 104 deviceIds, 105 }); 106 } 107 } 108} 109