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
2 changes: 1 addition & 1 deletion src/per_isolate/webstreams/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ private-brand dispatch, no `instanceof`) apply here — see
| ------------- | -------------------------------------------------------------------------------------------- |
| `ring-buffer.ts` | O(1) FIFO with indexed access backing every internal queue below; leaf module |
| `view-extent.ts` | A caller-supplied view's byte extent through captured getters, and any BufferSource's (`bufferSourceExtent`); detached/out-of-bounds inputs (DataViews included) read as empty; `copyExtent` copies an extent; leaf module |
| `common.ts` | Helpers shared by the webstreams modules, including `normalizeExpectedLength` (every `expectedLength` and `FixedLengthStream` length converts through it, as C++ converts a uint64_t); leaf module |
| `common.ts` | Helpers shared by the webstreams modules, including WebIDL argument conversion (`requireObject` for a source, sink, transformer or strategy `init`; `toDictionary` for an options dictionary; `convertQueuingStrategy`, then `extractHighWaterMark` and `extractSizeAlgorithm`, for a strategy) and `normalizeExpectedLength` (every `expectedLength` and `FixedLengthStream` length converts through it, as C++ converts a uint64_t); leaf module |
| `queue.ts` | QUEUED backend: single-queue/multi-cursor, JS sources; fence interfaces; invariant list |
| `native.ts` | NATIVE backend: C++-backed pull conduit; **the C++/JS contract** + invariants |
| `readable.ts` | Reader layer + queued controllers + the BACKEND-DISPATCH points (constructor, tee, chains, byte-capable gate, JS-to-C++ extraction) |
Expand Down
111 changes: 110 additions & 1 deletion src/per_isolate/webstreams/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

// Helpers shared by the webstreams modules. Leaf module: requires nothing.

const { BigInt, NumberIsFinite, ObjectFreeze, TypeError } = primordials;
import type { QueuingStrategy } from './types';

const {
BigInt,
NumberIsFinite,
NumberIsNaN,
ObjectFreeze,
RangeError,
TypeError,
uncurryThis,
} = primordials;

function isActualObject(value: unknown): value is object {
return value != null && typeof value === 'object';
Expand Down Expand Up @@ -58,12 +68,104 @@ function normalizeExpectedLength(value: unknown): bigint | undefined {
return BigInt(length - (length % 1));
}

// WebIDL's check that a value is an object, a function included: null and
// every other primitive throw. Nothing is read. It converts an argument typed
// `object` (an underlying source or sink, a transformer), and a dictionary
// with a required member, which undefined and null cannot be.
function requireObject(value: unknown, message: string): void {
if (!isActualObject(value) && typeof value !== 'function') {
throw new TypeError(message);
}
}

// WebIDL conversion of an optional dictionary argument (a strategy, an
// options bag), as in C++: undefined and null mean an empty dictionary, and
// anything else must pass requireObject.
function toDictionary(value: unknown, message: string): object {
if (value == null) return kNullProtoObject;
requireObject(value, message);
return value as object;
}

function toStrategyDictionary(value: unknown): QueuingStrategy<unknown> {
return toDictionary(
value,
'The queuing strategy must be an object.'
) as QueuingStrategy<unknown>;
}

// A strategy argument as WebIDL converts a QueuingStrategy dictionary: each
// member read once, in lexicographic order. highWaterMark has been through
// ToNumber; size is a function. An absent member is undefined.
interface QueuingStrategyDict {
highWaterMark: number | undefined;
size: ((chunk: unknown) => number) | undefined;
}

function convertQueuingStrategy(value: unknown): QueuingStrategyDict {
const strategy = toStrategyDictionary(value);
const rawHighWaterMark: unknown = strategy.highWaterMark;
// Unary plus is ToNumber (see normalizeExpectedLength).
const highWaterMark =
rawHighWaterMark === undefined ? undefined : +(rawHighWaterMark as number);
const size: unknown = strategy.size;
if (size !== undefined && typeof size !== 'function') {
throw new TypeError('strategy.size must be a function');
}
return {
__proto__: kNullProtoObject,
highWaterMark,
size,
} as QueuingStrategyDict;
}

// ToNumber and the range check of the spec's ExtractHighWaterMark: NaN and
// negative numbers are a RangeError; Infinity and fractions are kept.
// Converting a number again has no side effects.
function convertHighWaterMark(value: unknown): number {
const highWaterMark = +(value as number);
if (NumberIsNaN(highWaterMark) || highWaterMark < 0) {
throw new RangeError('Invalid highWaterMark');
}
return highWaterMark;
}

// The spec's ExtractHighWaterMark, on a converted strategy.
function extractHighWaterMark(
strategy: QueuingStrategyDict,
defaultHighWaterMark: number
): number {
const { highWaterMark } = strategy;
return highWaterMark === undefined
? defaultHighWaterMark
: convertHighWaterMark(highWaterMark);
}

// The spec's ExtractSizeAlgorithm, on a converted strategy: size() runs with
// an undefined receiver and the chunk as its only argument. Without one,
// every chunk counts 1.
function extractSizeAlgorithm<T>(
strategy: QueuingStrategyDict
): (chunk: T) => number {
const { size } = strategy;
if (size === undefined) return () => 1;
const callSize = uncurryThis(size);
return (chunk: T) => callSize(undefined, chunk);
}

export type CommonHelpers = {
isActualObject: typeof isActualObject;
kNullProtoObject: typeof kNullProtoObject;
kEnumerable: typeof kEnumerable;
toStringTagProperty: typeof toStringTagProperty;
normalizeExpectedLength: typeof normalizeExpectedLength;
requireObject: typeof requireObject;
toDictionary: typeof toDictionary;
toStrategyDictionary: typeof toStrategyDictionary;
convertQueuingStrategy: typeof convertQueuingStrategy;
convertHighWaterMark: typeof convertHighWaterMark;
extractHighWaterMark: typeof extractHighWaterMark;
extractSizeAlgorithm: typeof extractSizeAlgorithm;
};

module.exports = ObjectFreeze({
Expand All @@ -72,4 +174,11 @@ module.exports = ObjectFreeze({
kEnumerable,
toStringTagProperty,
normalizeExpectedLength,
requireObject,
toDictionary,
toStrategyDictionary,
convertQueuingStrategy,
convertHighWaterMark,
extractHighWaterMark,
extractSizeAlgorithm,
});
25 changes: 2 additions & 23 deletions src/per_isolate/webstreams/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ const {
ArrayPrototypePush,
BigInt,
Number,
NumberIsNaN,
ObjectDefineProperties,
PromisePrototypeThen,
PromiseResolve,
Expand Down Expand Up @@ -136,6 +135,8 @@ const {
kEnumerable,
normalizeExpectedLength,
toStringTagProperty,
toStrategyDictionary,
convertHighWaterMark,
} = require('webstreams/common') as CommonHelpers;
const { bufferSourceExtent, copyExtent } =
require('webstreams/view-extent') as ViewExtentHelpers;
Expand Down Expand Up @@ -204,28 +205,6 @@ let assertIsIdentityTransformStream: (self: IdentityTransformStream) => void;

const kPrivateSymbol: symbol = Symbol('private');

// The writable's WHATWG high-water-mark conversion (ToNumber; NaN or a
// negative number is a RangeError), for FixedLengthStream, which caps the
// converted number at its length before IdentityTransformStream hands it
// on. Converting a number again there has no side effects.
function convertHighWaterMark(value: unknown): number {
const highWaterMark = +(value as number);
if (NumberIsNaN(highWaterMark) || highWaterMark < 0) {
throw new RangeError('Invalid highWaterMark');
}
return highWaterMark;
}

// WebIDL dictionary conversion of a constructor's strategy argument, as in
// C++: undefined and null mean no strategy; any other non-object throws.
function toStrategyDictionary(value: unknown): QueuingStrategy<unknown> {
if (value === undefined || value === null) return kNullProtoObject;
if (typeof value !== 'object' && typeof value !== 'function') {
throw new TypeError('The queuing strategy must be an object.');
}
return value as QueuingStrategy<unknown>;
}

// A write accepted by the writable, snapshotted in its size() callback (see
// sizeAndSnapshot). Entries are tagged with an own `ok` data property rather
// than discriminated with an `in` check so that a polluted Object.prototype
Expand Down
93 changes: 36 additions & 57 deletions src/per_isolate/webstreams/readable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ const {
kEnumerable,
normalizeExpectedLength,
toStringTagProperty,
requireObject,
toDictionary,
convertQueuingStrategy,
extractHighWaterMark,
extractSizeAlgorithm,
} = require('webstreams/common') as CommonHelpers;

import type { ViewExtentHelpers } from './view-extent';
Expand Down Expand Up @@ -1133,9 +1138,10 @@ class ReadableStreamBYOBReader implements ReadableStreamBYOBReaderType {
"view's backing buffer must have a non-zero byteLength"
);
}
if (!isActualObject(options)) {
throw new TypeError('options must be an object');
}
options = toDictionary(
options,
'options must be an object'
) as ReadableStreamBYOBReaderReadOptions;
// --- min validation (steps 4-6). min is in ELEMENTS for typed arrays
// (bytes for DataView, where info.length === byteLength). NO clamping:
// out-of-range is an error, not a request to do less.
Expand Down Expand Up @@ -2962,12 +2968,10 @@ interface ConvertedPipeOptions {
// Members are read once each in spec order (WPT
// piping/throwing-options.any.js).
function convertPipeOptions(options: unknown): ConvertedPipeOptions {
// WebIDL: null and undefined become {}.
if (options == null) options = kNullProtoObject;
if (!isActualObject(options)) {
throw new TypeError('Pipe options must be an object');
}
const dict = options as StreamPipeOptions;
const dict = toDictionary(
options,
'Pipe options must be an object'
) as StreamPipeOptions;
const preventAbort = !!dict.preventAbort;
const preventCancel = !!dict.preventCancel;
const preventClose = !!dict.preventClose;
Expand Down Expand Up @@ -4513,12 +4517,16 @@ class ReadableStream<R> {
return;
}

// WebIDL argument conversion: the source is typed `object`, so null and
// every other primitive throw before the strategy is read.
requireObject(underlyingSource, 'underlyingSource must be an object');

// BACKEND-DISPATCH: stream construction (one of the five sanctioned
// dispatch points): native vs queued byte vs queued default.
//
// The native check runs FIRST: native sources are forbidden from
// declaring `type` (enforced during native extraction), so a
// native-marked source must never fall through to the queued
// The native check runs before any member is read: native sources are
// forbidden from declaring `type` (enforced during native extraction),
// so a native-marked source must never fall through to the queued
// branches. The strategy argument is PERMANENTLY ignored on the
// native branch: native pacing is purely demand-driven (the source
// contractually ignores desiredSize).
Expand Down Expand Up @@ -4565,23 +4573,10 @@ class ReadableStream<R> {
return;
}

// --- WebIDL strategy dictionary conversion (BEFORE source reads) ---
// Per WebIDL, dictionary-typed arguments are converted (property reads
// happen) at the IDL layer before the constructor body runs. The
// `strategy` parameter is QueuingStrategy (a dictionary); the
// `underlyingSource` parameter is plain `object` (no dictionary
// conversion). We simulate this by reading strategy properties first.
const sizeFn = strategy.size;
let sizeAlgorithm: (chunk: R) => number;
if (sizeFn === undefined) {
sizeAlgorithm = () => 1;
} else if (typeof sizeFn !== 'function') {
throw new TypeError('strategy.size must be a function');
} else {
const callSize = uncurryThis(sizeFn);
sizeAlgorithm = (chunk: R) => callSize(undefined, chunk);
}
const rawHWM = strategy.highWaterMark;
// WebIDL converts the strategy dictionary at the IDL layer, before the
// constructor body converts the source to an UnderlyingSource
// dictionary and reads its members.
const strategyDict = convertQueuingStrategy(strategy);

// WebIDL: the 'type' member is a string enum — perform ToString() on
// non-undefined values so that objects with toString()/valueOf() work
Expand All @@ -4592,22 +4587,15 @@ class ReadableStream<R> {

if (type === 'bytes') {
// Byte streams: size() is forbidden, highWaterMark defaults to 0.
if (sizeFn !== undefined) {
if (strategyDict.size !== undefined) {
throw new RangeError(
'The strategy for a byte stream cannot have a size function'
);
}
let highWaterMark = 0;
if (rawHWM !== undefined) {
highWaterMark = +rawHWM;
if (NumberIsNaN(highWaterMark) || highWaterMark < 0) {
throw new RangeError('Invalid highWaterMark');
}
}
this.#controller = setupReadableByteStreamControllerFromUnderlyingSource(
this,
underlyingSource,
highWaterMark
extractHighWaterMark(strategyDict, 0)
);
} else {
// The spec says, "Assert: underlyingSourceDict["type"] does not exist"
Expand All @@ -4617,19 +4605,12 @@ class ReadableStream<R> {
throw new TypeError(`Invalid underlying source type: ${type}`);
}
// Default streams: highWaterMark defaults to 1.
let highWaterMark = 1;
if (rawHWM !== undefined) {
highWaterMark = +rawHWM;
if (NumberIsNaN(highWaterMark) || highWaterMark < 0) {
throw new RangeError('Invalid highWaterMark');
}
}
this.#controller =
setupReadableStreamDefaultControllerFromUnderlyingSource(
this,
underlyingSource,
sizeAlgorithm,
highWaterMark
extractSizeAlgorithm<R>(strategyDict),
extractHighWaterMark(strategyDict, 1)
);
}
}
Expand All @@ -4655,15 +4636,13 @@ class ReadableStream<R> {
options: { mode?: 'byob' } | null = kNullProtoObject
): ReadableStreamReaderType<R> {
assertIsReadableStream(this);
// WebIDL dictionary conversion: null and undefined become {},
// objects have their properties read, primitives are rejected.
if (options != null && !isActualObject(options)) {
throw new TypeError('Reader options must be an object');
}
const dict = toDictionary(options, 'Reader options must be an object') as {
mode?: 'byob';
};

// WebIDL: the 'mode' member is a string enum — perform ToString() on
// non-undefined values so that objects with toString()/valueOf() work.
const rawMode = isActualObject(options) ? options.mode : undefined;
const rawMode = dict.mode;
const mode = rawMode === undefined ? undefined : `${rawMode}`;
if (mode === undefined) {
return acquireReadableStreamDefaultReader(this);
Expand Down Expand Up @@ -4761,10 +4740,10 @@ class ReadableStream<R> {
options: { preventCancel?: boolean } = kNullProtoObject
): AsyncIterableIterator<R> {
assertIsReadableStream(this);
if (!isActualObject(options)) {
throw new TypeError('Options must be an object');
}
const { preventCancel } = options;
const { preventCancel } = toDictionary(
options,
'Options must be an object'
) as { preventCancel?: boolean };

if (isReadableStreamLocked(this)) {
throw new TypeError('Cannot get an iterator for a stream that is locked');
Expand Down
13 changes: 5 additions & 8 deletions src/per_isolate/webstreams/strategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { CommonHelpers } from './common';

const { ObjectDefineProperties, SymbolToStringTag, TypeError } = primordials;

const { isActualObject, kEnumerable, toStringTagProperty } =
const { isActualObject, kEnumerable, requireObject, toStringTagProperty } =
require('webstreams/common') as CommonHelpers;

// Per spec, the size function is the SAME function object for every
Expand Down Expand Up @@ -44,11 +44,10 @@ class ByteLengthQueuingStrategy {
}

// The init type is optional-shaped because user input is arbitrary; the
// required-member check is the explicit TypeError below.
// required-member check is the explicit TypeError below. A dictionary with
// a required member cannot be converted from undefined or null.
constructor(init: { highWaterMark?: number }) {
if (!isActualObject(init)) {
throw new TypeError('init must be an object');
}
requireObject(init, 'init must be an object');
// Read once, as WebIDL dictionary conversion reads each member once.
const highWaterMark = init.highWaterMark;
if (highWaterMark === undefined) {
Expand Down Expand Up @@ -82,9 +81,7 @@ class CountQueuingStrategy {
}

constructor(init: { highWaterMark?: number }) {
if (!isActualObject(init)) {
throw new TypeError('init must be an object');
}
requireObject(init, 'init must be an object');
// Read once, as WebIDL dictionary conversion reads each member once.
const highWaterMark = init.highWaterMark;
if (highWaterMark === undefined) {
Expand Down
Loading
Loading