WriteableStream.destroy leaves producers awaiting backpressure writes hung forever
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
### Bug description
`WriteableStream.destroy()` (`src/vs/base/common/stream.ts`) clears the pending write queue without settling it:
```ts
this.pendingWritePromises.length = 0;
```
`write()` documents returning a promise the producer must await before writing more (backpressure); those promises are the queued resolvers. After a destroy, every producer parked inside `await stream.write()` stays parked forever, and nothing rejects.
The codebase treats this class of hang as a bug elsewhere: `ResourceQueue.dispose` explicitly releases its drainers "to prevent hanging promises".
Reachable through the file service copy pipeline: `src/vs/platform/files/common/io.ts` awaits `target.write(...)` between chunks with consumer streams created with a `highWaterMark`; if the target stream is destroyed mid-transfer (error or cancellation path), the read loop hangs permanently and the `finally { provider.close(handle) }` never runs, so the source file handle leaks. The token is only checked between iterations, so cancellation cannot unblock the parked write either.
Minimal repro:
1. `newWriteableStream(string, { highWaterMark: 1 })`
2. `stream.write('a')` (accepted), then `const p = stream.write('b')` (returns pending backpressure promise)
3. `stream.destroy()`
4. Observe `p` never settles.
### Expected behavior
Destroying a stream settles pending writes - by error-rejecting them (`onError`) so producers unwind and their cleanup paths run.
Contributor guide
Assessment
This issue has not been assessed yet.