Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/per_isolate/webstreams/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function toStringTagProperty(tag: string): PropertyDescriptor {

const kMaxUint64 = 0xffff_ffff_ffff_ffffn;

// Converts the non-standard expectedLength extension: the total bytes a byte
// Converts the non-standard expectedLength extension: the total bytes a
// source or a transformer promises, or a FixedLengthStream's length. It
// converts as the C++ implementation converts a uint64_t, so the two accept
// the same values and agree on what they mean. undefined means no length. A
Expand Down Expand Up @@ -328,9 +328,10 @@ function convertReadableStreamType(value: unknown): 'bytes' | undefined {
// An underlying source as WebIDL converts it to an UnderlyingSource
// dictionary, before the constructor checks anything: every member read
// once, in lexicographic order, each callback checked, type converted as
// its enum, and autoAllocateChunkSize and the expectedLength extension
// converted, on a default source too, although only a byte source uses
// them. The callbacks still run with the source itself as their receiver.
// its enum, the expectedLength extension converted, and
// autoAllocateChunkSize converted on a default source too, although only a
// byte source uses it. The callbacks still run with the source itself as
// their receiver.
// S narrows the callbacks to one kind of source, once type is known.
export interface UnderlyingSourceDict<
R,
Expand Down
120 changes: 66 additions & 54 deletions src/per_isolate/webstreams/readable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,12 +394,12 @@ let getReaderStream: <R>(reader: object) => ReadableStream<R> | undefined;
// ReadableStream's static block (needs private-field access).
let extractNativeSource: <R>(this: ReadableStream<R>) => object;

// The non-standard expectedLength pass-through for the DrainingReader
// (and the C++ bridge). Chained like the other controller helpers:
// default → the value installed by the TransformStream expectedLength
// extension (undefined otherwise); queued byte → cached construction
// value; native → cached construction value (joined in ReadableStream's
// static block).
// The non-standard expectedLength pass-through behind
// getReadableStreamExpectedLength. Chained like the other controller
// helpers, each reporting the value its underlying source declared,
// cached at construction: default and queued byte (a TransformStream's
// readable declares its transformer's), and native (joined in
// ReadableStream's static block).
let getControllerExpectedLength: (
controller: AnyReadableController
) => bigint | undefined;
Expand All @@ -411,10 +411,6 @@ let byteControllerSetConsumptionHook: (
controller: ReadableByteStreamController,
hook: (() => void) | undefined
) => void;
let setDefaultControllerExpectedLength: <R>(
controller: ReadableStreamDefaultController<R>,
length: bigint | undefined
) => void;
let defaultControllerCanCloseOrEnqueue: <R>(
controller: ReadableStreamDefaultController<R>
) => boolean;
Expand Down Expand Up @@ -1693,12 +1689,11 @@ class ReadableStreamDefaultController<
> implements ReadableStreamDefaultControllerType<R> {
#stream: ReadableStream<R>;
#core: QueuedControllerCore<R, R>;
// The non-standard expectedLength pass-through (undefined = unknown).
// Default controllers never read it from an underlying source — it is
// set ONLY through the internal setter, by the TransformStream
// constructor's workerd `expectedLength` extension, so the C++ bridge
// can derive Content-Length for bodies built from such transforms.
#expectedLength: bigint | undefined = undefined;
// The non-standard expectedLength the source declared (undefined =
// unknown): the TOTAL bytes the stream will deliver, from which the C++
// bridge derives a body's Content-Length. Chunks need not be bytes, so
// the controller does not enforce it; body consumption does.
#expectedLength: bigint | undefined;

static {
assertIsReadableStreamDefaultController = function <R>(
Expand Down Expand Up @@ -1726,18 +1721,11 @@ class ReadableStreamDefaultController<
queuedCoreOf(controller)?.callPullIfNeeded();
};

// Default controllers never read expectedLength from an underlying
// source (silently ignored if declared); it is populated only via the
// internal setter (the TransformStream `expectedLength` extension).
// The byte and native arms of this chain report their own cached
// values.
getControllerExpectedLength = (controller) =>
(controller as ReadableStreamDefaultController).#expectedLength;

setDefaultControllerExpectedLength = (controller, length) => {
controller.#expectedLength = length;
};

defaultControllerCanCloseOrEnqueue = (controller) =>
controller.#core.canCloseOrEnqueue();

Expand Down Expand Up @@ -1784,7 +1772,13 @@ class ReadableStreamDefaultController<
assertPrivateSymbol(privateSymbol);
this.#stream = stream;

const { cancel: cancelFn, pull: pullFn, start: startFn } = sourceDict;
const {
cancel: cancelFn,
expectedLength,
pull: pullFn,
start: startFn,
} = sourceDict;
this.#expectedLength = expectedLength;
Comment thread
jasnell marked this conversation as resolved.
Resolved
// Strategy already extracted by caller (WebIDL conversion order).
const core = new QueuedControllerCore<R, R>(
stream,
Expand Down Expand Up @@ -2133,10 +2127,10 @@ class ReadableByteStreamController implements ReadableByteStreamControllerType {
throw new TypeError('autoAllocateChunkSize cannot be zero');
}
this.#autoAllocateChunkSize = autoAllocateChunkSize;
// Non-standard extension, used by byte streams only: the TOTAL bytes
// this source promises to produce. This controller enforces the
// contract at enqueue/respond (overflow) and close() (underflow).
// Exposed to the C++ bridge via the DrainingReader.
// Non-standard extension: the TOTAL bytes this source promises to
// produce, from which the C++ bridge derives a body's Content-Length.
// This controller enforces the contract at enqueue/respond (overflow)
// and close() (underflow).
this.#expectedLength = expectedLength;

// Strategy already extracted by caller (WebIDL conversion order).
Expand Down Expand Up @@ -2709,20 +2703,14 @@ class ReadableStreamDrainingReader<R> {
}
}

// The non-standard expectedLength pass-through for the C++ bridge: the
// TOTAL bytes the underlying source declared it will produce
// (undefined = unknown → chunked encoding). A construction-time value,
// cached on the controller; backend-blind via the chained helper
// (byte/native report their cached value; a default controller the
// value the TransformStream expectedLength extension installed, if
// any). Returns undefined after release.
// The non-standard expectedLength: the bytes the stream still has to
// deliver, if known (undefined = unknown), as the C++ bridge sees it
// (getReadableStreamExpectedLength). Returns undefined after release.
get expectedLength(): bigint | undefined {
Comment thread
jasnell marked this conversation as resolved.
Resolved
assertIsReadableStreamDrainingReader(this);
const stream = getReaderStream<R>(this);
if (stream === undefined) return undefined;
const controller = getReadableStreamController(stream);
if (controller === undefined) return undefined;
return getControllerExpectedLength(controller);
return getReadableStreamExpectedLength(stream);
}

cancel(reason?: unknown): Promise<void> {
Expand Down Expand Up @@ -3588,6 +3576,10 @@ class ReadableStream<R> {
// teed again is closed at the tee (see readableStreamTee).
#consumer?: StreamConsumerType<R> | undefined;
#disturbed: boolean = false;
// Whether this stream is a tee branch or detached copy (makeShell) of a
// stream that had been read from, or was itself such a copy, before the
// fork (see getReadableStreamExpectedLength).
#forkedAfterRead: boolean = false;
#state: ReadableStreamState = 'readable';
#storedError?: unknown;
// The Node.js interop closed-promise (see kIsClosedPromise), created on
Expand Down Expand Up @@ -3657,9 +3649,23 @@ class ReadableStream<R> {
return promise;
};

// The total the stream still has to deliver, if known: the C++ bridge's
// length query (a body's Content-Length). A queued source's declaration
// is its stream's total, and the controller, shared by every tee branch,
// does not count what a stream has already passed. So a queued stream
// reports it only while nothing has been read from it, nor, for a tee
// branch or detached copy, before the fork. A native stream's forks are
// fresh streams over sources that count what is left, and a disturbed
// native stream cannot be pumped (extraction rejects it).
getReadableStreamExpectedLength = <R>(stream: ReadableStream<R>) => {
Comment thread
jasnell marked this conversation as resolved.
Resolved
const controller = stream.#controller;
if (controller === undefined) return undefined;
if (
!isNativeController(controller) &&
(stream.#disturbed || stream.#forkedAfterRead)
) {
return undefined;
}
return getControllerExpectedLength(controller);
};

Expand Down Expand Up @@ -3901,8 +3907,10 @@ class ReadableStream<R> {

// A shell carries another stream's state forward: a tee branch, or the
// stream detach() moves a body to. It copies the state, stored error and
// pending closure, and shares `controller`, if any; the caller gives it a
// consumer when it has data to read.
// pending closure, records whether anything was read before the fork,
// and shares `controller`, if any; the caller gives it a consumer when
// it has data to read. Callers make shells before handing `from` off,
// which disturbs it.
const makeShell = <R>(
from: ReadableStream<R>,
controller?: AnyReadableController
Expand All @@ -3911,6 +3919,7 @@ class ReadableStream<R> {
shell.#state = from.#state;
shell.#storedError = from.#storedError;
shell.#pendingClosure = from.#pendingClosure;
shell.#forkedAfterRead = from.#disturbed || from.#forkedAfterRead;
shell.#controller = controller;
return shell;
};
Expand Down Expand Up @@ -4571,9 +4580,10 @@ class ReadableStream<R> {

// Body consumption for the C++ bridge (arrayBuffer/bytes/text/json). Two
// byte bounds apply: the caller's memory limit, capped at 128 MB, and the
// stream's declared expectedLength. A breach names its cause and cancels
// the stream with it, as the C++ AllReader does; a declaration the limit
// cannot hold is refused before a byte is read.
// stream's declared expectedLength, an exact total that a stream closing
// short of also breaches. A breach names its cause and cancels the stream
// with it, as the C++ AllReader does; a declaration the limit cannot hold
// is refused before a byte is read.
const kMaximumAllowedLimit = 128n * 1024n * 1024n;

function acquireReadableStreamDrainingReader<R>(
Expand Down Expand Up @@ -4753,7 +4763,17 @@ async function collectChunks<R>(
}
collected.append(buffer, byteOffset, byteLength);
}
if (result.done) return collected;
if (result.done) {
if (declaredBinds && collected.length < bound) {
return failCollect(
reader,
new RangeError(
'stream delivered fewer bytes than its declared expectedLength'
)
);
}
return collected;
}
}
}

Expand Down Expand Up @@ -4906,8 +4926,8 @@ module.exports = {
ReadableStreamDrainingReader,
// Internal operations consumed by the transform pairs (transform.ts,
// identity.ts, compression.ts): the TransformStream cancel/flush
// coordination (finishPromise guard), the workerd expectedLength
// extension, and the interop error hook. Unreachable from user code.
// coordination (finishPromise guard) and the interop error hook.
// Unreachable from user code.
internalsForTransform: ObjectFreeze({
getState: <R>(stream: ReadableStream<R>) =>
getReadableStreamGetState(stream),
Expand All @@ -4931,14 +4951,6 @@ module.exports = {
controller as ReadableByteStreamController,
hook
),
setControllerExpectedLength: <R>(
controller: object,
length: bigint | undefined
) =>
setDefaultControllerExpectedLength(
controller as ReadableStreamDefaultController<R>,
length
),
// A transform pair's notification, called synchronously with the
// reason after the Node.js interop hook has errored this stream (the
// only external error path that bypasses the source's cancel steps).
Expand Down
15 changes: 5 additions & 10 deletions src/per_isolate/webstreams/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,11 @@ class TransformStream<I = unknown, O = unknown> {
},
pull: sourcePull,
cancel: (reason: unknown) => this.#finish('cancel', reason),
// Non-standard workerd extension: the TOTAL bytes the readable
// side will produce (undefined = unknown), from which the C++
// bridge derives a Content-Length for bodies built from this
// transform.
expectedLength,
},
{
__proto__: null,
Expand All @@ -574,16 +579,6 @@ class TransformStream<I = unknown, O = unknown> {
size: readableStrategyDict.size,
}
);
// Non-standard workerd extension: the TOTAL bytes the readable side
// will produce (undefined = unknown). Advertised through the
// readable's controller so the C++ bridge derives a Content-Length
// for bodies built from this transform; not enforced here.
if (expectedLength !== undefined) {
readableInternals.setControllerExpectedLength(
this.#readableController,
expectedLength
);
}

// --- Start the transformer (the elided path has none to start) ---
if (controller === undefined) {
Expand Down
6 changes: 6 additions & 0 deletions src/per_isolate/webstreams/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export interface UnderlyingDefaultSource<R = unknown> {
) => void | Promise<void>;
cancel?: (reason?: unknown) => void | Promise<void>;
type?: undefined;
// Non-standard workerd extension: the TOTAL bytes this source promises
// to produce (undefined = unknown), converted as on a byte source. Used
// by the C++ bridge to set Content-Length instead of chunked encoding.
// Chunks need not be bytes, so the controller does not enforce it; body
// consumption rejects a delivery past it.
expectedLength?: bigint | number | undefined;
}

export interface UnderlyingByteSource {
Expand Down
Loading
Loading