closeWithGrace(closes: Readonly<Record<string, Promise<unknown> | null | undefined>>,graceMs?: number): Promise<Abandoned>
Awaits every cleanup promise in closes, but never longer than graceMs. Resolves
whichever happens first: every close settles (Promise.allSettled absorbs rejections
so a single failing close cannot wedge the others), or the grace timer fires. Pending
closes keep running in the background after a timeout — the caller is expected to
process.exit() shortly after, which terminates them anyway.
On timeout, writes one line to stderr so the user sees that shutdown was cut short.
This is an exceptional condition — not verbose output — so it fires regardless of
--debug: every user who hits the deadlock deserves to know browser/server cleanup
may have left orphans. Goes to stderr so it never lands in the TAP stream.
null / undefined entries are accepted as-is so optional-chained closes such as
connections.server?.close() flow in without per-call filtering. Keyed rather than positional
so the timeout can name what it gave up on.
Resolves with what it abandoned: the names, and settled — a promise for the moment those
finally finish. Giving up on a close is not the same as being done with it, and a caller that
outlives this one (a watch session restarting) has to be able to come back for it. Without
that, an abandoned browser close holds its transport open for the life of the process, and
nothing ever looks at it again.
const browserClose = Promise.resolve(); const serverClose: Promise<void> | undefined = undefined; // e.g. connections.server?.close() (await closeWithGrace({ browser: browserClose, server: serverClose })).names; // [] — all settled (await closeWithGrace({ wedged: Promise.reject(new Error('x')) }, 50)).names; // [] — a rejection settles
Promise<Abandoned>