Repository navigation
Settle a TS compression write or close cut short by a cancel - #7645
Merged
Merged
Conversation
Contributor
|
@jasnell Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
guybedford
approved these changes
Oct 7, 2026
jasnell
force-pushed
the
jasnell/ts-streams-compression-drain-cancel
branch
from
October 7, 2026 22:18
d00aa99 to
8c5370c
Compare
Contributor
|
Since last review: 1 resolved, 0 still open, 0 new. Not re-run: api-compat, docs, compat-flags, design-simplicity (no author changes in their files since the last review) Reviewed commit: 348d9ecc · github run |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7645 +/- ##
=======================================
Coverage 38.59% 38.60%
=======================================
Files 868 868
Lines 268583 268608 +25
Branches 25422 25426 +4
=======================================
+ Hits 103665 103687 +22
Misses 150704 150704
- Partials 14214 14217 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jasnell
force-pushed
the
jasnell/ts-streams-compression-drain-cancel
branch
6 times, most recently
from
October 8, 2026 23:23
63be3e4 to
3db618e
Compare
Base automatically changed from
jasnell/ts-streams-decoder-shared-buffer
to
main
October 9, 2026 12:08
Under TS a compression pair moves its output into the readable inside the write or close that produced it, in 64 KiB pieces. Resolving a waiting read looks up `then` on its result, so an Object.prototype.then getter can cancel the reader in the middle of that drain. The drain then stopped, and the write or close rejected with the cancel reason. With a gzip pair, a pending read and an Object.prototype.then getter that cancels the reader, TS / C++ / Node: write: rejected with the reason / resolved / resolved close: rejected with the reason / resolved / rejected A spec TransformStream whose transform or flush enqueues the same output resolves both. After a cancel during the transform the writable errors with the reason; after one during the flush, close, writer.closed and the cancel all resolve. That is what TS's own TransformStream and Node's do. Node's compression streams are a zlib Duplex adapter, not a TransformStream. How a transform splits its output into chunks is implementation-defined, so a drain cut short now settles as a transform that enqueued all its output as one chunk before the cancel: the rest is dropped and the write resolves, after which the writable is errored as before (compression ledger #13). The close resolves too, as do writer.closed and the cancel; for the close this is parity with C++. A readable errored during the drain (the Node.js interop hook) still rejects the close with its error, as the spec's flush does. Pins: compression reentrancy.js cancelFromReadResultThenGetterDuringWrite now expects the write to resolve in both implementations (TS: the writable errors with the reason; C++: untouched); cancelFromReadResultThenGetterDuringClose (parity: close, writer.closed and the cancel resolve, the read the getter fired on still gets the tail); interopErrorFromReadResultThenGetterDuringClose (TS only: the close rejects with the hook's error). Without the source change, the first two fail, in the TS cells only; the third passes either way and guards the errored-readable branch. Compatibility: the change is behind the experimental typescript_implemented_streams flag.
jasnell
force-pushed
the
jasnell/ts-streams-compression-drain-cancel
branch
from
October 9, 2026 12:09
3db618e to
39012b0
Compare
A read result's `then` getter that errors a compression pair through the Node.js interop hook while a write is delivering leaves that in-flight write resolving, as a spec transform's does: only the flush checks the readable's state. Both sides then expose the hook's error. Nothing pinned the write side of this, so restoring a throw there for an errored readable passed the suite. Pins: compression reentrancy.js interopErrorFromReadResultThenGetterDuringWrite (TS only: the write resolves, writer.closed and reader.closed reject with the hook's error). With `if (finished && readableErrored) throw finishReason;` added after the write's drain, it fails in the TS cell. Also corrects the compression AGENTS.md Reads bullet, which still said a reader cancel from the getter fails the write.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Under TS a compression pair moves its output into the readable inside
the write or close that produced it, in 64 KiB pieces. Resolving a
waiting read looks up
thenon its result, so an Object.prototype.thengetter can cancel the reader in the middle of that drain. The drain then
stopped, and the write or close rejected with the cancel reason. With a
gzip pair, a pending read and an Object.prototype.then getter that
cancels the reader, TS / C++ / Node:
write: rejected with the reason / resolved / resolved
close: rejected with the reason / resolved / rejected
A spec TransformStream whose transform or flush enqueues the same output
resolves both. After a cancel during the transform the writable errors
with the reason; after one during the flush, close, writer.closed and
the cancel all resolve. That is what TS's own TransformStream and Node's
do. Node's compression streams are a zlib Duplex adapter, not a
TransformStream.
How a transform splits its output into chunks is implementation-defined,
so a drain cut short now settles as a transform that enqueued all its
output as one chunk before the cancel: the rest is dropped and the write
resolves, after which the writable is errored as before (compression
ledger #13). The close resolves too, as do writer.closed and the
cancel; for the close this is parity with C++. A readable errored during
the drain (the Node.js interop hook) still rejects the close with its
error, as the spec's flush does.
Pins: compression reentrancy.js cancelFromReadResultThenGetterDuringWrite
now expects the write to resolve in both implementations (TS: the
writable errors with the reason; C++: untouched);
cancelFromReadResultThenGetterDuringClose (parity: close, writer.closed
and the cancel resolve, the read the getter fired on still gets the
tail); interopErrorFromReadResultThenGetterDuringClose (TS only: the
close rejects with the hook's error). Without the source change, the
first two fail, in the TS cells only; the third passes either way and
guards the errored-readable branch.