Timeout with Cleanup
Medium · Async
A common Node pattern is Promise.race([task, timeout]). But the loser keeps running: an uncleared setTimeout keeps the Node process alive (and a forgotten task keeps using sockets or CPU). So after the race you must clean up the loser.
The starter has startTimer(ms, fn) / stopTimer(id) (wrappers around setTimeout / clearTimeout that track open timers in activeTimers) and fakeTask(ms, signal), which resolves with "done" after ms, or stops its timer and rejects when the AbortSignal is aborted.
Write async withTimeout(taskMs, limitMs):
1. Start the task with the signal of a new AbortController, and a timeout timer (use startTimer) that resolves with "timeout" after limitMs.
2. Race them.
3. If the task won, stop the timeout timer. If the timeout won, abort the task.
Return { result, openTimers: activeTimers.size }, where result is "done" or "timeout". With correct cleanup, openTimers is always 0.