Lifecycle-aligned Cancellation

When handling concurrent async work (streaming, tees, semaphores/limits, watchdogs), ensure cancellation, timeouts, and accounting are tied to the *actual* consumer lifecycle—release/decrement/teardown at the point where the work is truly finished, and don’t block on cancellation promises that can’t resolve until another branch drains.

copy reviewer prompt

Prompt

Reviewer Prompt

When handling concurrent async work (streaming, tees, semaphores/limits, watchdogs), ensure cancellation, timeouts, and accounting are tied to the actual consumer lifecycle—release/decrement/teardown at the point where the work is truly finished, and don’t block on cancellation promises that can’t resolve until another branch drains.

Practical rules:

  • Don’t await cancellation of detached/tee branches if it may remain pending until another consumer drains; cancel and proceed.
  • Release concurrency slots only after the stream is fully consumed (e.g., TransformStream.flush/end-of-stream), not when headers/initial chunks arrive.
  • Prevent double accounting: if you decrement pending counters at multiple phases (e.g., flush + handler), gate with a flag or ensure only one phase decrements.
  • Use abort timeouts for network calls and merge with any provided AbortSignal so requests can’t hang indefinitely.
  • Guard watchdog math: if the baseline timestamp/listener wiring can be missing, seed it or skip instead of computing stall against undefined.

Example pattern (release on flush, not header arrival):

const slot = await acquireSemaphore();
let released = false;
const releaseOnce = () => {
  if (released) return;
  released = true;
  slot();
};

return new TransformStream<Uint8Array, Uint8Array>({
  transform(chunk, controller) {
    controller.enqueue(chunk);
  },
  flush() {
    // stream fully consumed (client drained)
    releaseOnce();
  }
});

These practices reduce race conditions, deadlocks, leaked capacity, and spurious failures in highly concurrent streaming systems.

Source discussions