| 178 | }; |
| 179 | |
| 180 | export const waitForHttp = async ( |
| 181 | url: string, |
| 182 | options: { |
| 183 | readonly timeoutMs?: number; |
| 184 | readonly expectRedirect?: boolean; |
| 185 | readonly signal?: AbortSignal; |
| 186 | } = {}, |
| 187 | ): Promise<void> => { |
| 188 | const timeoutMs = options.timeoutMs ?? 90_000; |
| 189 | const deadline = performance.now() + timeoutMs; |
| 190 | let lastError: unknown; |
| 191 | while (performance.now() < deadline) { |
| 192 | options.signal?.throwIfAborted(); |
| 193 | try { |
| 194 | const remainingMs = Math.max(1, deadline - performance.now()); |
| 195 | const probeTimeout = AbortSignal.timeout(Math.min(HTTP_PROBE_TIMEOUT_MS, remainingMs)); |
| 196 | const signal = options.signal |
| 197 | ? AbortSignal.any([options.signal, probeTimeout]) |
| 198 | : probeTimeout; |
| 199 | const response = await fetch(url, { redirect: "manual", signal }); |
| 200 | // During a cold vite compile /api/* falls back to the SPA's 200 HTML — |
| 201 | // expectRedirect waits for the real handler (302) instead. |
| 202 | if (options.expectRedirect ? response.status === 302 : response.status < 500) return; |
| 203 | lastError = new Error(`status ${response.status}`); |
| 204 | } catch (error) { |
| 205 | options.signal?.throwIfAborted(); |
| 206 | lastError = error; |
| 207 | } |
| 208 | await sleep(400, undefined, { signal: options.signal }); |
| 209 | } |
| 210 | throw new BootReadinessTimeoutError(url, timeoutMs, lastError); |
| 211 | }; |
| 212 | |
| 213 | /** |
| 214 | * Reboot DOWN-GATE: wait until `url` stops answering (fetch rejects — connection |