Skip to content

Commit 9968f58

Browse files
committed
feat: enhance startApp to wait for a URL and add waitForUrl utility
1 parent 4ce1732 commit 9968f58

4 files changed

Lines changed: 45 additions & 3 deletions

File tree

src/core/start-app.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { Config } from "./types.js";
22
import { executeCommand, type RunningProcess } from "./utils/execute-command.js";
3+
import { waitForUrl } from "./utils/wait-for-url.js";
34

45
/**
56
* Start the application using lifecycle.start commands
67
* @param config The validated configuration object
8+
* @param url Optional URL to wait for after starting commands with keepAlive
79
* @returns Cleanup function that stops all running processes
810
*/
9-
export async function startApp(config: Config): Promise<() => Promise<void>> {
11+
export async function startApp(config: Config, url?: string): Promise<() => Promise<void>> {
1012
const runningProcesses: RunningProcess[] = [];
1113

1214
try {
@@ -17,6 +19,14 @@ export async function startApp(config: Config): Promise<() => Promise<void>> {
1719
}
1820
}
1921

22+
// Wait for URL to become available if:
23+
// 1. A URL was provided AND
24+
// 2. At least one command has keepAlive: true
25+
const hasKeepAliveCommand = config.lifecycle.start.some((cmd) => cmd.keepAlive);
26+
if (url && hasKeepAliveCommand) {
27+
await waitForUrl(url);
28+
}
29+
2030
// Return cleanup function
2131
return async () => {
2232
for (const { process, command } of runningProcesses) {

src/core/utils/wait-for-url.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Wait for a URL to become available by polling with fetch
3+
* @param url The URL to check (e.g., "http://localhost:3000")
4+
* @param timeout Maximum time to wait in milliseconds (default: 30000)
5+
* @throws Error if URL doesn't become available within timeout
6+
*/
7+
export async function waitForUrl(url: string, timeout: number = 30000): Promise<void> {
8+
const startTime = Date.now();
9+
const interval = 1000; // Poll every 1 second
10+
11+
while (Date.now() - startTime < timeout) {
12+
try {
13+
// Use AbortSignal.timeout for per-request timeout (2 seconds)
14+
const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
15+
16+
// Consider 2xx-4xx as "available" - only 5xx errors mean server isn't ready
17+
// This handles cases where the app redirects or returns 404 before fully loading
18+
if (response.ok || response.status < 500) {
19+
return;
20+
}
21+
} catch (_error) {
22+
// Suppress errors during polling - connection errors are expected while server starts
23+
// Errors include: connection refused, timeout, DNS errors, etc.
24+
}
25+
26+
// Wait before next poll attempt
27+
await new Promise((resolve) => setTimeout(resolve, interval));
28+
}
29+
30+
// Timeout reached without successful connection
31+
throw new Error(`Timeout: ${url} did not become available within ${timeout}ms`);
32+
}

src/recorder/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export async function startRecording(storyId: string): Promise<void> {
2121
try {
2222
config = await loadConfig();
2323
const story = await loadStory(storyId);
24-
cleanup = await startApp(config);
24+
cleanup = await startApp(config, story.start.url);
2525
browserInstance = await launchBrowser(story);
2626
const { browser, page } = browserInstance;
2727

src/runner/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export async function runStory(
204204
const story = await loadStory(storyId);
205205

206206
// 3. Start application
207-
cleanup = await startApp(config);
207+
cleanup = await startApp(config, story.start.url);
208208

209209
// 4. Launch browser
210210
browserInstance = await launchBrowser(story);

0 commit comments

Comments
 (0)