From 3a58f2ffc942f6528524b65ff46384aba8e79f26 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 5 Aug 2025 09:08:25 +0200 Subject: [PATCH] feat: wait command to assert emulator is ready --- src/commands/dev.ts | 4 ++ src/help/dev.help.ts | 1 + src/services/dev/wait.services.ts | 83 +++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 src/services/dev/wait.services.ts diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 9abc08fa..1f4043e5 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -5,6 +5,7 @@ import {logHelpFunctionsBuild} from '../help/functions.build.help'; import {logHelpFunctionsEject} from '../help/functions.eject.help'; import {start} from '../services/dev/start.services'; import {stop} from '../services/dev/stop.services'; +import {wait} from '../services/dev/wait.services'; import {build} from '../services/functions/build/build.services'; import {eject} from '../services/functions/eject/eject.services'; @@ -18,6 +19,9 @@ export const dev = async (args?: string[]) => { case 'stop': await stop(); break; + case 'wait': + await wait(); + break; case 'eject': await eject(args); break; diff --git a/src/help/dev.help.ts b/src/help/dev.help.ts index 85770ef0..0087c498 100644 --- a/src/help/dev.help.ts +++ b/src/help/dev.help.ts @@ -8,6 +8,7 @@ const usage = `Usage: ${green('juno')} ${cyan('dev')} ${magenta('')} Subcommands: ${magenta('start')} ${DEV_START_DESCRIPTION} ${magenta('stop')} Stop the local network. + ${magenta('wait')} Wait until the emulator is ready. ${magenta('build')} Alias for ${green('juno')} ${cyan('functions')} ${magenta('build')}. ${magenta('eject')} Alias for ${green('juno')} ${cyan('functions')} ${magenta('eject')}.`; diff --git a/src/services/dev/wait.services.ts b/src/services/dev/wait.services.ts new file mode 100644 index 00000000..b974b03a --- /dev/null +++ b/src/services/dev/wait.services.ts @@ -0,0 +1,83 @@ +import {green, red} from 'kleur'; +import ora from 'ora'; +import {readEmulatorConfig} from '../../configs/emulator.config'; +import type {CliEmulatorConfig} from '../../types/emulator'; +import {dispatchRequest} from '../emulator/emulator.admin.services'; + +const TIMEOUT_IN_MILLISECONDS = 15000; +const RETRY_IN_MILLISECONDS = 500; + +export const wait = async () => { + const parsedResult = await readEmulatorConfig(); + + if (!parsedResult.success) { + return; + } + + const spinner = ora('Waiting for emulator...').start(); + + let status: 'ready' | 'timeout' | undefined = undefined; + + try { + status = await waitEmulatorReady({ + count: TIMEOUT_IN_MILLISECONDS / RETRY_IN_MILLISECONDS, + config: parsedResult.config + }); + } finally { + spinner.stop(); + } + + if (status === 'timeout') { + console.log(red('The emulator is not ready. Operation timed out.')); + process.exit(1); + } + + console.log(`Emulator is ${green('ready')}.`); +}; + +const waitEmulatorReady = async ({ + count, + config +}: { + count: number; + config: CliEmulatorConfig; +}): Promise<'ready' | 'timeout'> => { + const ready = await isEmulatorReady({config}); + + if (ready) { + return 'ready'; + } + + const nextCount = count - 1; + + if (nextCount === 0) { + return 'timeout'; + } + + // eslint-disable-next-line promise/avoid-new + await new Promise((resolve) => setTimeout(resolve, RETRY_IN_MILLISECONDS)); + + return await waitEmulatorReady({count: nextCount, config}); +}; + +// The emulator mounts its internal CLI/admin server as the final step of the startup flow. +// That's why we can use the health endpoint to check when the emulator is ready. +// It's also more convenient than pinging something like the satellite version, +// since initialization, agent creation, or fetching the version may hang until a timeout. +const isEmulatorReady = async ({config}: {config: CliEmulatorConfig}): Promise => { + try { + const {result} = await dispatchRequest({ + config, + request: 'health' + }); + + // At the moment, we only check whether the response succeeded. + // We don’t verify if the response body is "Ok" or "Unknown command". + // Since we don’t have specific use cases yet—other than checking if the server is mounted— + // this keeps the command compatible with older versions of the Docker image that didn’t + // expose the /health endpoint. + return result === 'ok'; + } catch (_e: unknown) { + return false; + } +};