refactor(streams): derive dictionary arguments in conversion stages

Derive strategy and codec/iterator options while retaining highWaterMark presence and operation-specific defaults in the stream algorithm. Convert and validate TransformStream writable strategy before its readable strategy, then read transformer members. Derive QueuingStrategyInit with required unrestricted highWaterMark and cover conversion/error order across realms.

Validation on the combined tree: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (19639 passed, 16 skipped; CARGO_BUILD_JOBS=1).
This commit is contained in:
ldm0
2026-10-06 13:35:34 +08:00
committed by Donough Liu
parent 86ccaa5ee8
commit d15274d1e4
6 changed files with 165 additions and 149 deletions
@@ -54,13 +54,12 @@ mod utils;
mod writable;
pub(in crate::context_bootstrap) use callbacks::{
StreamQueuingStrategy, StreamWebIdlCallbackCarrier, WebIdlReadableStreamSource,
QueuingStrategyMembers, StreamWebIdlCallbackCarrier, WebIdlReadableStreamSource,
WebIdlTransformStreamTransformer, WebIdlWritableStreamSink, callback_carrier_value,
invoke_stored_stream_algorithm, invoke_stored_stream_promise_algorithm,
invoke_stored_stream_size_algorithm, invoke_stream_webidl_callback,
parse_readable_stream_source_object, parse_stream_strategy_arg,
parse_transform_stream_transformer_object, parse_writable_stream_sink_object,
stored_stream_algorithm_is_webidl,
parse_readable_stream_source_object, parse_transform_stream_transformer_object,
parse_writable_stream_sink_object, stored_stream_algorithm_is_webidl,
};
pub(super) use construction::{
initialize_transform_stream_object, initialize_webidl_readable_stream_object,
@@ -89,7 +89,7 @@ struct TransformerMembers<'s> {
/// `highWaterMark` before `size`.
#[derive(Default, webidl::WebIdlDictionary)]
#[webidl(prefix = "QueuingStrategy")]
struct QueuingStrategyMembers {
pub(in crate::context_bootstrap) struct QueuingStrategyMembers {
#[webidl(name = "highWaterMark", converter = "unrestricted_double")]
high_water_mark: Option<f64>,
#[webidl(converter = "callback_function")]
@@ -127,16 +127,6 @@ pub(in crate::context_bootstrap) struct StreamQueuingStrategy {
pub(in crate::context_bootstrap) size: Option<webidl::WebIdlCallbackFunction>,
}
impl StreamQueuingStrategy {
pub(in crate::context_bootstrap) const fn without_size(high_water_mark: f64) -> Self {
Self {
high_water_mark,
high_water_mark_provided: true,
size: None,
}
}
}
#[derive(Clone, Copy)]
pub(in crate::context_bootstrap) struct StreamWebIdlCallbackCarrier<'s> {
object: v8::Local<'s, v8::Object>,
@@ -238,35 +228,27 @@ pub(in crate::context_bootstrap) fn parse_transform_stream_transformer_object<'s
})
}
pub(in crate::context_bootstrap) fn parse_stream_strategy_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
prefix: &'static str,
default_high_water_mark: f64,
) -> Result<StreamQueuingStrategy, webidl::WebIdlError> {
let context = webidl::Context::argument(prefix, (index + 1) as usize);
let Some(object) = webidl::dictionary_arg(args, index, context)? else {
return Ok(StreamQueuingStrategy {
high_water_mark: default_high_water_mark,
high_water_mark_provided: false,
size: None,
});
};
let members = webidl::parse_dictionary_object::<QueuingStrategyMembers>(scope, object)?;
let high_water_mark = members.high_water_mark.unwrap_or(default_high_water_mark);
if moli_streams::numeric::validate_high_water_mark(high_water_mark).is_err() {
crate::util::throw_range_error(
scope,
"A queuing strategy's highWaterMark must be a nonnegative, non-NaN number",
);
return Err(webidl::WebIdlError::pending_exception(context));
impl QueuingStrategyMembers {
pub(in crate::context_bootstrap) fn resolve(
self,
scope: &mut v8::PinScope<'_, '_>,
context: webidl::Context,
default_high_water_mark: f64,
) -> Result<StreamQueuingStrategy, webidl::WebIdlError> {
let high_water_mark = self.high_water_mark.unwrap_or(default_high_water_mark);
if moli_streams::numeric::validate_high_water_mark(high_water_mark).is_err() {
crate::util::throw_range_error(
scope,
"A queuing strategy's highWaterMark must be a nonnegative, non-NaN number",
);
return Err(webidl::WebIdlError::pending_exception(context));
}
Ok(StreamQueuingStrategy {
high_water_mark,
high_water_mark_provided: self.high_water_mark.is_some(),
size: self.size,
})
}
Ok(StreamQueuingStrategy {
high_water_mark,
high_water_mark_provided: members.high_water_mark.is_some(),
size: members.size,
})
}
/// Invokes one page-supplied callback through Web IDL while leaving exception,
@@ -1,11 +1,10 @@
use super::specs::{ConstructorKind, ConstructorSpec};
use super::stream_adapter::{
StreamQueuingStrategy, cancel_readable_stream, close_stream, enqueue_chunk,
QueuingStrategyMembers, cancel_readable_stream, close_stream, enqueue_chunk,
initialize_transform_stream_object, initialize_webidl_readable_stream_object,
initialize_webidl_transform_stream_object, initialize_webidl_writable_stream_object,
parse_readable_stream_source_object, parse_stream_strategy_arg,
parse_transform_stream_transformer_object, parse_writable_stream_sink_object,
readable_stream_byob_request_respond_callback,
parse_readable_stream_source_object, parse_transform_stream_transformer_object,
parse_writable_stream_sink_object, readable_stream_byob_request_respond_callback,
readable_stream_byob_request_respond_with_new_view_callback,
readable_stream_byob_request_view_getter, readable_stream_is_byte_stream,
readable_stream_locked, rejected_promise_value, set_resolved_promise, stream_slot_object,
@@ -68,8 +68,8 @@ struct ByteLengthQueuingStrategyPrototypeDeclaration {
struct ReadableStreamConstructorArgs<'s> {
#[webidl(index = 0)]
source: Option<v8::Local<'s, v8::Object>>,
#[webidl(index = 1, with = parse_readable_stream_strategy_arg)]
strategy: StreamQueuingStrategy,
#[webidl(index = 1, dictionary)]
strategy: QueuingStrategyMembers,
}
#[derive(webidl::WebIdlArgs)]
@@ -77,8 +77,8 @@ struct ReadableStreamConstructorArgs<'s> {
struct WritableStreamConstructorArgs<'s> {
#[webidl(index = 0)]
sink: Option<v8::Local<'s, v8::Object>>,
#[webidl(index = 1, with = parse_writable_stream_strategy_arg)]
strategy: StreamQueuingStrategy,
#[webidl(index = 1, dictionary)]
strategy: QueuingStrategyMembers,
}
#[derive(webidl::WebIdlArgs)]
@@ -86,10 +86,17 @@ struct WritableStreamConstructorArgs<'s> {
struct TransformStreamConstructorArgs<'s> {
#[webidl(index = 0)]
transformer: Option<v8::Local<'s, v8::Object>>,
#[webidl(index = 1, with = parse_transform_stream_writable_strategy_arg)]
writable_strategy: StreamQueuingStrategy,
#[webidl(index = 2, with = parse_transform_stream_readable_strategy_arg)]
readable_strategy: StreamQueuingStrategy,
#[webidl(index = 1, dictionary)]
writable_strategy: QueuingStrategyMembers,
}
// Writable strategy conversion and validation must finish before reading the
// readable strategy. Keep this second conversion stage ahead of transformer reads.
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "TransformStream")]
struct TransformStreamReadableStrategyArgs {
#[webidl(index = 2, dictionary)]
readable_strategy: QueuingStrategyMembers,
}
#[derive(webidl::WebIdlArgs)]
@@ -97,22 +104,29 @@ struct TransformStreamConstructorArgs<'s> {
struct TextDecoderStreamConstructorArgs {
#[webidl(default = "utf-8")]
label: String,
#[webidl(index = 1, with = parse_text_decoder_stream_options_arg)]
#[webidl(index = 1, dictionary)]
options: TextDecoderStreamOptions,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "CountQueuingStrategy")]
struct CountQueuingStrategyConstructorArgs {
#[webidl(required, with = parse_queuing_strategy_init_arg)]
init: StreamQueuingStrategy,
#[webidl(required, dictionary)]
init: QueuingStrategyInit,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "ByteLengthQueuingStrategy")]
struct ByteLengthQueuingStrategyConstructorArgs {
#[webidl(required, with = parse_queuing_strategy_init_arg)]
init: StreamQueuingStrategy,
#[webidl(required, dictionary)]
init: QueuingStrategyInit,
}
#[derive(webidl::WebIdlDictionary)]
#[webidl(prefix = "QueuingStrategyInit")]
struct QueuingStrategyInit {
#[webidl(required, converter = "unrestricted_double")]
high_water_mark: f64,
}
#[derive(Default, webidl::WebIdlDictionary)]
@@ -139,6 +153,17 @@ pub(in crate::context_bootstrap) fn readable_stream_constructor_callback<'s>(
let Some(parsed) = webidl::parse_args::<ReadableStreamConstructorArgs<'s>>(scope, &args) else {
return;
};
let strategy =
match parsed
.strategy
.resolve(scope, webidl::Context::argument("ReadableStream", 2), 1.0)
{
Ok(strategy) => strategy,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let source = match parsed
.source
.map(|source| parse_readable_stream_source_object(scope, source))
@@ -151,7 +176,7 @@ pub(in crate::context_bootstrap) fn readable_stream_constructor_callback<'s>(
}
};
let byte_stream = source.as_ref().is_some_and(|source| source.byte_stream);
if byte_stream && parsed.strategy.size.is_some() {
if byte_stream && strategy.size.is_some() {
throw_range_error(
scope,
"A byte ReadableStream cannot use a queuing strategy size function",
@@ -167,10 +192,10 @@ pub(in crate::context_bootstrap) fn readable_stream_constructor_callback<'s>(
throw_type_error(scope, "autoAllocateChunkSize must be greater than zero");
return;
}
let high_water_mark = if byte_stream && !parsed.strategy.high_water_mark_provided {
let high_water_mark = if byte_stream && !strategy.high_water_mark_provided {
0.0
} else {
parsed.strategy.high_water_mark
strategy.high_water_mark
};
initialize_webidl_readable_stream_object(
scope,
@@ -178,7 +203,7 @@ pub(in crate::context_bootstrap) fn readable_stream_constructor_callback<'s>(
source,
byte_stream,
high_water_mark,
parsed.strategy.size,
strategy.size,
);
rv.set(args.this().into());
}
@@ -198,6 +223,17 @@ pub(in crate::context_bootstrap) fn writable_stream_constructor_callback<'s>(
let Some(parsed) = webidl::parse_args::<WritableStreamConstructorArgs<'s>>(scope, &args) else {
return;
};
let strategy =
match parsed
.strategy
.resolve(scope, webidl::Context::argument("WritableStream", 2), 1.0)
{
Ok(strategy) => strategy,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let sink = match parsed
.sink
.map(|sink| parse_writable_stream_sink_object(scope, sink))
@@ -213,8 +249,8 @@ pub(in crate::context_bootstrap) fn writable_stream_constructor_callback<'s>(
scope,
args.this(),
sink,
parsed.strategy.high_water_mark,
parsed.strategy.size,
strategy.high_water_mark,
strategy.size,
);
rv.set(args.this().into());
}
@@ -235,6 +271,32 @@ pub(in crate::context_bootstrap) fn transform_stream_constructor_callback<'s>(
else {
return;
};
let writable_strategy = match parsed.writable_strategy.resolve(
scope,
webidl::Context::argument("TransformStream", 2),
1.0,
) {
Ok(strategy) => strategy,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let Some(readable) = webidl::parse_args::<TransformStreamReadableStrategyArgs>(scope, &args)
else {
return;
};
let readable_strategy = match readable.readable_strategy.resolve(
scope,
webidl::Context::argument("TransformStream", 3),
0.0,
) {
Ok(strategy) => strategy,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let transformer = match parsed
.transformer
.map(|transformer| parse_transform_stream_transformer_object(scope, transformer))
@@ -251,10 +313,10 @@ pub(in crate::context_bootstrap) fn transform_stream_constructor_callback<'s>(
args.this(),
transformer,
None,
parsed.writable_strategy.high_water_mark,
parsed.writable_strategy.size,
parsed.readable_strategy.high_water_mark,
parsed.readable_strategy.size,
writable_strategy.high_water_mark,
writable_strategy.size,
readable_strategy.high_water_mark,
readable_strategy.size,
);
rv.set(args.this().into());
}
@@ -567,69 +629,3 @@ fn byte_length_queuing_strategy_size_callback<'s>(
};
rv.set(byte_length);
}
fn parse_readable_stream_strategy_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<StreamQueuingStrategy, webidl::WebIdlError> {
parse_stream_strategy_arg(scope, args, index, "ReadableStream", 1.0)
}
fn parse_writable_stream_strategy_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<StreamQueuingStrategy, webidl::WebIdlError> {
parse_stream_strategy_arg(scope, args, index, "WritableStream", 1.0)
}
fn parse_transform_stream_writable_strategy_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<StreamQueuingStrategy, webidl::WebIdlError> {
parse_stream_strategy_arg(scope, args, index, "TransformStream", 1.0)
}
fn parse_transform_stream_readable_strategy_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<StreamQueuingStrategy, webidl::WebIdlError> {
parse_stream_strategy_arg(scope, args, index, "TransformStream", 0.0)
}
fn parse_queuing_strategy_init_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<StreamQueuingStrategy, webidl::WebIdlError> {
let context = webidl::Context::argument("QueuingStrategyInit", (index + 1) as usize);
let object = webidl::dictionary_arg(args, index, context)?
.ok_or_else(|| webidl::WebIdlError::missing_required(context))?;
let high_water_mark = object
.get(scope, v8str(scope, "highWaterMark").into())
.ok_or_else(|| webidl::WebIdlError::pending_exception(context))?;
if high_water_mark.is_undefined() {
return Err(webidl::WebIdlError::custom_message(
"QueuingStrategyInit.highWaterMark is required",
));
}
let high_water_mark = high_water_mark.number_value(scope).ok_or_else(|| {
webidl::WebIdlError::custom_message("QueuingStrategyInit.highWaterMark must be a number")
})?;
Ok(StreamQueuingStrategy::without_size(high_water_mark))
}
fn parse_text_decoder_stream_options_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<TextDecoderStreamOptions, webidl::WebIdlError> {
let context = webidl::Context::argument("TextDecoderStream", (index + 1) as usize);
webidl::dictionary_arg(args, index, context)?
.map(|object| webidl::parse_dictionary_object(scope, object))
.transpose()
.map(|options| options.unwrap_or_default())
}
@@ -58,7 +58,7 @@ struct StreamPipeOptions<'s> {
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "ReadableStream.values")]
struct ReadableStreamValuesArgs {
#[webidl(with = readable_stream_iterator_options_arg)]
#[webidl(dictionary)]
options: ReadableStreamIteratorOptions,
}
@@ -609,18 +609,6 @@ fn new_readable_stream_async_iterator_object<'s>(
Some(v8::Local::new(scope, &iterator))
}
fn readable_stream_iterator_options_arg<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
index: i32,
) -> Result<ReadableStreamIteratorOptions, webidl::WebIdlError> {
let context = webidl::Context::argument("ReadableStream.values", (index + 1) as usize);
webidl::dictionary_arg(args, index, context)?
.map(|object| webidl::parse_dictionary_object(scope, object))
.transpose()
.map(|options| options.unwrap_or_default())
}
pub(in crate::context_bootstrap) fn readable_stream_locked_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -1,5 +1,57 @@
use super::*;
#[test]
fn stream_dictionary_arguments_preserve_required_members_and_staged_strategy_validation() {
let mut vm = new_parsed_test_vm(
"https://stream-dictionary-args.test/",
"<!doctype html><body><iframe id=child></iframe></body>",
);
let result = vm.eval(r#"
(() => {
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const child = document.getElementById('child').contentWindow;
const marker = {};
for (const realm of [window, child]) {
for (const C of [realm.CountQueuingStrategy, realm.ByteLengthQueuingStrategy]) {
for (const args of [[], [undefined], [null], [{}], [{highWaterMark: undefined}]]) {
let error;
try { new C(...args); } catch (caught) {error = caught;}
assert(error instanceof realm.TypeError, 'required highWaterMark in callee realm');
}
for (const highWaterMark of [NaN, Infinity, -1, 0]) {
assert(Object.is(new C({highWaterMark}).highWaterMark, highWaterMark), 'unrestricted highWaterMark');
}
let reads = 0;
const strategy = new C({
get highWaterMark() {reads++; return {valueOf() {reads++; return 2;}};},
get size() {throw 'size must not be read';}
});
assert(strategy.highWaterMark === 2 && reads === 2, 'one member read and one conversion');
let error;
try {new C({highWaterMark: {valueOf() {throw marker;}}});} catch (caught) {error = caught;}
assert(error === marker, 'numeric exception identity');
}
const reads = [];
let error;
try {
new realm.TransformStream({
get start() {reads.push('transformer');}
}, {
get highWaterMark() {reads.push('writable.highWaterMark'); return -1;},
get size() {reads.push('writable.size');}
}, {
get highWaterMark() {reads.push('readable.highWaterMark'); return 0;}
});
} catch (caught) {error = caught;}
assert(error instanceof realm.RangeError, 'strategy validation realm');
assert(reads.join() === 'writable.highWaterMark,writable.size', 'stop before readable strategy and transformer: ' + reads);
}
return 'ok';
})()
"#).unwrap();
assert_eq!(result, "ok");
}
mod compression;
mod intrinsic_promises;
mod transform_finish;