1import { ExpoConfig, getConfig } from '@expo/config'; 2 3import { 4 closeDevelopmentSessionAsync, 5 updateDevelopmentSessionAsync, 6} from '../../api/updateDevelopmentSession'; 7import { getUserAsync } from '../../api/user/user'; 8import { env } from '../../utils/env'; 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 (env.EXPO_OFFLINE) { 50 debug( 51 'This project will not be suggested in Expo Go or Dev Clients because Expo CLI is running in offline-mode.' 52 ); 53 this.stopNotifying(); 54 return; 55 } 56 57 const deviceIds = await this.getDeviceInstallationIdsAsync(); 58 59 if (!(await isAuthenticatedAsync()) && !deviceIds?.length) { 60 debug( 61 'Development session will not ping because the user is not authenticated and there are no devices.' 62 ); 63 this.stopNotifying(); 64 return; 65 } 66 67 if (this.url) { 68 debug(`Development session ping (runtime: ${runtime}, url: ${this.url})`); 69 70 await updateDevelopmentSessionAsync({ 71 url: this.url, 72 runtime, 73 exp, 74 deviceIds, 75 }); 76 } 77 78 this.stopNotifying(); 79 80 this.timeout = setTimeout(() => this.startAsync({ exp, runtime }), UPDATE_FREQUENCY); 81 } catch (error: any) { 82 debug(`Error updating development session API: ${error}`); 83 this.stopNotifying(); 84 this.onError(error); 85 } 86 } 87 88 /** Get all recent devices for the project. */ 89 private async getDeviceInstallationIdsAsync(): Promise<string[]> { 90 const { devices } = await ProjectDevices.getDevicesInfoAsync(this.projectRoot); 91 return devices.map(({ installationId }) => installationId); 92 } 93 94 /** Stop notifying the Expo servers that the development session is running. */ 95 public stopNotifying() { 96 if (this.timeout) { 97 clearTimeout(this.timeout); 98 } 99 this.timeout = null; 100 } 101 102 public async closeAsync(): Promise<void> { 103 this.stopNotifying(); 104 105 const deviceIds = await this.getDeviceInstallationIdsAsync(); 106 107 if (!(await isAuthenticatedAsync()) && !deviceIds?.length) { 108 return; 109 } 110 111 if (this.url) { 112 await closeDevelopmentSessionAsync({ 113 url: this.url, 114 deviceIds, 115 }); 116 } 117 } 118} 119