Skip to content

Pin how stream constructors read source, sink and transformer members - #7784

Merged
jasnell merged 6 commits into
mainfrom
jasnell/ts-streams-duplicate-consolidaton-13
Oct 16, 2026
Merged

jasnell merged 6 commits into
mainfrom
jasnell/ts-streams-duplicate-consolidaton-13

Conversation

@jasnell

@jasnell jasnell commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Deduplicate and test view argument validation. See individual commits for details

@jasnell
jasnell requested a review from a team October 16, 2026 12:47
@jasnell
jasnell requested review from a team as code owners October 16, 2026 12:47
@jasnell
jasnell added this pull request to stack #7753 October 16, 2026 12:47
@clanky-the-clanker

clanky-the-clanker Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

✅ Approved — No issues found. The view-validation consolidation keeps the existing checks and moves the WebIDL conversion ahead of the state checks, which matches the spec's order. The new tests cover the changed paths.

The decision is advisory: this comment never approves, blocks or requests changes on the pull request.


✅ All checks passed

🔒 Security: ✅ LGTM

The SharedArrayBuffer rejection and the fixed-length buffer transfer at the byte-stream boundary still happen. Metadata is still read only through the captured intrinsic getters.


📖 AGENTS.md: ✅ LGTM

The src/tests/streams/readable-byte/AGENTS.md update is accurate and matches the new tests.


📋 Code Quality: ✅ LGTM

getUnsharedViewInfo now combines the isArrayBufferView check, metadata capture and the SharedArrayBuffer rejection in one helper. getNonEmptyUnsharedViewInfo adds the zero-length check, and read(view), enqueue() and respondWithNewView() all use these helpers.

The pull request drops the separate "backing buffer has zero byteLength" checks. The new comment explains why they cannot fail: the byteLength getters return 0 for a detached or out-of-bounds typed array, and a DataView getter throws in that case. That reasoning holds.

respondWithNewView now passes the captured ViewInfo to the controller instead of the view. This avoids reading the view's metadata a second time.


⚡ Performance: ✅ LGTM

No performance concerns. Passing the captured ViewInfo to #respondWithNewView removes a second metadata read.


📝 Documentation: ✅ LGTM

The new comments describe the current behavior. In the Streams spec's WebIDL, enqueue() and respondWithNewView() take an ArrayBufferView, so the comments are right to call rejecting non-view values the spec's conversion.


Reviewed commit: c556a530

@ask-bonk

ask-bonk Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Since last review: 0 resolved, 0 still open, 0 new.
LGTM!


Reviewed commit: c556a530 · github run

@jasnell
jasnell force-pushed the jasnell/ts-streams-duplicate-consolidaton-13 branch from 43c56b3 to cff4d79 Compare October 16, 2026 15:39

@guybedford guybedford left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, two non-blocking notes inline.

Comment thread src/per_isolate/webstreams/readable.ts Outdated
Comment thread src/per_isolate/webstreams/readable.ts Outdated
Base automatically changed from jasnell/ts-streams-duplicate-consolidaton-12 to main October 16, 2026 18:28
DUP-22 step 1, test only. read(view), readAtLeast(), enqueue() and
respondWithNewView() take an ArrayBufferView in WebIDL. No test
passed them anything else: read(view) pinned only the error class,
and the other two nothing, so a wrong message from their type checks
would pass every test.

nonViewArgumentsRejected pins a string, an object and undefined at
all four, and a bare ArrayBuffer. Every non-view is a TypeError in
both implementations, with their own messages, except a bare
ArrayBuffer at enqueue() and respondWithNewView(): C++ types those
parameters as a BufferSource and takes the buffer as a view over the
whole of it, detaching it and delivering its bytes. TypeScript
rejects it, as the spec does. Readable-byte ledger #41 records the
divergence.
DUP-22 step 2. read(view) and enqueue() ran the same view checks,
twelve lines apart only in the noun, and all three byte-stream trust
boundaries (with respondWithNewView()) checked for an ArrayBufferView
before calling getUnsharedViewInfo.

getUnsharedViewInfo now makes that check itself, which completes
WebIDL's ArrayBufferView conversion in one place, and the new
getNonEmptyUnsharedViewInfo adds the non-empty checks that read(view)
and enqueue() share. The messages are unchanged. The comment on
validateAndTransferView named respondWithNewView(), which has never
called it; it now names enqueue() alone.
DUP-22 step 3. byobRequest.respondWithNewView() read the view's
metadata through getUnsharedViewInfo for its own checks, then handed
the controller the view, which read it again through getViewInfo. No
code runs in between, so the second read could only repeat the first.
The controller now takes the ViewInfo the request read, which saves
the read and its allocation on every call. Nothing observable
changes.
DUP-22 step 4. read(view) and enqueue() checked, after a view's own
byteLength, that its buffer's byteLength was not zero, as the spec's
second step does. That check could never fail: the byteLength getters
read 0 for a detached or out-of-bounds typed array, a DataView's
throw, and a view with bytes lies within its buffer. The spec needs
the step because a view's [[ByteLength]] slot keeps its value once
its buffer is detached.

A probe of seventeen kinds of input (non-views, shared, zero-length,
detached, out-of-bounds and emptied length-tracking views) at all
five entry points never reached it. The check goes, with its message,
and a comment says why the spec's step needs no code. This saves a
getter call per enqueue() and read(view). Nothing observable changes.
DUP-22 step 5. Since respondWithNewView()'s controller takes the
ViewInfo its request read, getViewInfo has had one caller,
getUnsharedViewInfo, which makes the isArrayBufferView check that
getViewInfo's precondition asked of its callers. getViewInfo folds
into it, and the precondition goes. The getters run in the same
order. Nothing observable changes.
DUP-22 step 6. WebIDL converts a method's arguments before its steps
run, and C++ converts its parameters before the method body.
respondWithNewView() checked that its request was still valid, and
enqueue() that its stream could still take a chunk, before converting
the view, so a value that was not a view met those checks first:
respondWithNewView('x') on an invalidated request threw 'This BYOB
request has been invalidated', and enqueue('x') after close() 'Cannot
enqueue a chunk into a stream that is closed or closing', where C++
threw its parameter-type error.

Both now convert the view first. enqueue() also checks that the chunk
is not empty (spec steps 1-2) before the stream's state (steps 3-4),
as C++ does, and transfers the buffer only once both pass, so
validateAndTransferView, which did both at once, goes. A view backed
by a SharedArrayBuffer, which the conversion rejects, now fails as
such on an invalidated request too, where C++, which converts it,
still reports the invalidated request. Every error is still a
TypeError; only which message wins changes.

viewConvertedBeforeMethodSteps pins the new order on both
implementations, and fails under TypeScript without the change.
@jasnell
jasnell force-pushed the jasnell/ts-streams-duplicate-consolidaton-13 branch from b7cb7d3 to c556a53 Compare October 16, 2026 19:33
@jasnell
jasnell merged commit 7a54999 into main Oct 16, 2026
24 checks passed
@jasnell
jasnell deleted the jasnell/ts-streams-duplicate-consolidaton-13 branch October 16, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants