fix review finding: exitsWithin cleans up its listener and timer on both arms

Each timed-out wait used to leave the once('exit') listener from its inner
waitForExit attached to the child; the dispose ladder accumulates at most a
couple, but in a shared library a caller polling exitsWithin in a loop would
pile listeners onto one child (MaxListenersExceededWarning at 11) and retain
their closures. The race now owns its wiring: the timeout arm removes the
exit listener, the exit arm clears the (still unref'ed) grace timer, and an
already-exited child short-circuits true without attaching anything. Tests
pin listenerCount('exit') === 0 after every outcome.
This commit is contained in:
pku-xht
2026-07-09 10:42:34 +08:00
parent 7ccf31a59b
commit 2471e2b2bb
3 changed files with 31 additions and 7 deletions

View File

@@ -181,15 +181,27 @@ describe('waitForExit / exitsWithin', () => {
expect(fake.signalCode).toBe('SIGTERM')
})
it('exitsWithin resolves true immediately for an already-exited child (no listener attached)', async () => {
const fake = new FakeChild()
fake.exitCode = 0
await expect(exitsWithin(asChild(fake), 1000)).resolves.toBe(true)
expect(fake.listenerCount('exit')).toBe(0)
})
it('exitsWithin resolves true when the child exits inside the window', async () => {
const fake = new FakeChild({ diesOn: 'SIGTERM', delayMs: 5 })
fake.kill('SIGTERM')
await expect(exitsWithin(asChild(fake), 1000)).resolves.toBe(true)
// The once-listener fired and the grace timer was cleared — nothing lingers.
expect(fake.listenerCount('exit')).toBe(0)
})
it('exitsWithin resolves false on timeout for a child that never exits', async () => {
const fake = new FakeChild() // nothing short of SIGKILL fells it; no signal sent
await expect(exitsWithin(asChild(fake), 20)).resolves.toBe(false)
// The timeout arm removed its exit listener: repeated waits (a poll loop,
// the ladder's tiers) never accumulate listeners on the same child.
expect(fake.listenerCount('exit')).toBe(0)
})
})