(
claims: ReadonlyArray<PortClaim>,
boot: (ports: Record<string, number>) => Promise<{ teardown: () => Promise<void>; value: T }>,
options: {
readonly maxAttempts?: number;
readonly label?: string;
/** Additional acquisition failures that are safe to retry from scratch. */
readonly retryWhen?: (error: unknown) => boolean;
} = {},
)
| 262 | * returned `teardown` chains the caller's teardown then releases the block. |
| 263 | */ |
| 264 | export const claimAndBoot = async <T>( |
| 265 | claims: ReadonlyArray<PortClaim>, |
| 266 | boot: (ports: Record<string, number>) => Promise<{ teardown: () => Promise<void>; value: T }>, |
| 267 | options: { |
| 268 | readonly maxAttempts?: number; |
| 269 | readonly label?: string; |
| 270 | /** Additional acquisition failures that are safe to retry from scratch. */ |
| 271 | readonly retryWhen?: (error: unknown) => boolean; |
| 272 | } = {}, |
| 273 | ): Promise<{ ports: Record<string, number>; teardown: () => Promise<void>; value: T }> => { |
| 274 | const maxAttempts = options.maxAttempts ?? 3; |
| 275 | const label = options.label ?? "boot"; |
| 276 | let lastError: unknown; |
| 277 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| 278 | const { ports, release } = await claimPorts(claims); |
| 279 | try { |
| 280 | const booted = await boot(ports); |
| 281 | return { |
| 282 | ports, |
| 283 | value: booted.value, |
| 284 | teardown: async () => { |
| 285 | await booted.teardown(); |
| 286 | await release(); |
| 287 | }, |
| 288 | }; |
| 289 | } catch (error) { |
| 290 | await release(); |
| 291 | lastError = error; |
| 292 | const retryable = isAddrInUse(error) || options.retryWhen?.(error) === true; |
| 293 | if (!retryable || attempt === maxAttempts) throw error; |
| 294 | const collided = claims |
| 295 | .map((claim) => ports[claim.envVar]) |
| 296 | .filter((port): port is number => port !== undefined) |
| 297 | .join(", "); |
| 298 | const reason = isAddrInUse(error) ? `hit EADDRINUSE on port(s) ${collided}` : String(error); |
| 299 | console.warn( |
| 300 | `[e2e] ${label} acquisition failed (${reason}, attempt ${attempt}/${maxAttempts}); re-claiming a fresh block and retrying`, |
| 301 | ); |
| 302 | } |
| 303 | } |
| 304 | throw lastError; |
| 305 | }; |
no test coverage detected