diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js
index 2369175733c115..8d884c43c2f9c3 100644
--- a/lib/internal/webstreams/readablestream.js
+++ b/lib/internal/webstreams/readablestream.js
@@ -94,6 +94,7 @@ const {
ArrayBufferViewGetByteLength,
ArrayBufferViewGetByteOffset,
AsyncIterator,
+ canCopyArrayBuffer,
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback,
@@ -2552,6 +2553,15 @@ function readableByteStreamControllerCommitPullIntoDescriptor(stream, desc) {
}
}
+function readableByteStreamControllerCommitPullIntoDescriptors(stream, descriptors) {
+ for (let i = 0; i < descriptors.length; ++i) {
+ readableByteStreamControllerCommitPullIntoDescriptor(
+ stream,
+ descriptors[i],
+ );
+ }
+}
+
function readableByteStreamControllerInvalidateBYOBRequest(controller) {
if (controller[kState].byobRequest === null)
return;
@@ -2758,11 +2768,11 @@ function readableByteStreamControllerRespondInClosedState(controller, desc) {
stream,
} = controller[kState];
if (readableStreamHasBYOBReader(stream)) {
- while (readableStreamGetNumReadIntoRequests(stream) > 0) {
- readableByteStreamControllerCommitPullIntoDescriptor(
- stream,
- readableByteStreamControllerShiftPendingPullInto(controller));
+ const filledPullIntos = [];
+ for (let i = 0; i < readableStreamGetNumReadIntoRequests(stream); ++i) {
+ ArrayPrototypePush(filledPullIntos, readableByteStreamControllerShiftPendingPullInto(controller));
}
+ readableByteStreamControllerCommitPullIntoDescriptors(stream, filledPullIntos);
}
}
@@ -2843,8 +2853,9 @@ function readableByteStreamControllerEnqueue(controller, chunk) {
transferredBuffer,
byteOffset,
byteLength);
- readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(
+ const filledPullIntos = readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(
controller);
+ readableByteStreamControllerCommitPullIntoDescriptors(stream, filledPullIntos);
} else {
assert(!isReadableStreamLocked(stream));
readableByteStreamControllerEnqueueChunkToQueue(
@@ -2937,6 +2948,7 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue(
const maxAlignedBytes = maxBytesFilled - (maxBytesFilled % elementSize);
let totalBytesToCopyRemaining = maxBytesToCopy;
let ready = false;
+ assert(!ArrayBufferPrototypeGetDetached(buffer));
assert(bytesFilled < minimumFill);
if (maxAlignedBytes >= minimumFill) {
totalBytesToCopyRemaining = maxAlignedBytes - bytesFilled;
@@ -2952,12 +2964,12 @@ function readableByteStreamControllerFillPullIntoDescriptorFromQueue(
totalBytesToCopyRemaining,
headOfQueue.byteLength);
const destStart = byteOffset + desc.bytesFilled;
- const arrayBufferByteLength = ArrayBufferPrototypeGetByteLength(buffer);
- if (arrayBufferByteLength - destStart < bytesToCopy) {
- throw new ERR_INVALID_STATE.RangeError(
- 'view ArrayBuffer size is invalid');
- }
- assert(arrayBufferByteLength - destStart >= bytesToCopy);
+ assert(canCopyArrayBuffer(
+ buffer,
+ destStart,
+ headOfQueue.buffer,
+ headOfQueue.byteOffset,
+ bytesToCopy));
copyArrayBuffer(
buffer,
destStart,
@@ -2991,26 +3003,30 @@ function readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(
const {
closeRequested,
pendingPullIntos,
- stream,
} = controller[kState];
assert(!closeRequested);
+ const filledPullIntos = [];
while (pendingPullIntos.length) {
if (!controller[kState].queueTotalSize)
- return;
+ break;
const desc = pendingPullIntos[0];
if (readableByteStreamControllerFillPullIntoDescriptorFromQueue(
controller,
desc)) {
readableByteStreamControllerShiftPendingPullInto(controller);
- readableByteStreamControllerCommitPullIntoDescriptor(stream, desc);
+ ArrayPrototypePush(filledPullIntos, desc);
}
}
+ return filledPullIntos;
}
function readableByteStreamControllerRespondInReadableState(
controller,
bytesWritten,
desc) {
+ const {
+ stream,
+ } = controller[kState];
const {
buffer,
bytesFilled,
@@ -3031,9 +3047,10 @@ function readableByteStreamControllerRespondInReadableState(
controller,
desc,
);
- readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(
+ const filledPullIntos = readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(
controller,
);
+ readableByteStreamControllerCommitPullIntoDescriptors(stream, filledPullIntos);
return;
}
@@ -3059,10 +3076,10 @@ function readableByteStreamControllerRespondInReadableState(
ArrayBufferPrototypeGetByteLength(remainder));
}
desc.bytesFilled -= remainderSize;
- readableByteStreamControllerCommitPullIntoDescriptor(
- controller[kState].stream,
- desc);
- readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
+ const filledPullIntos = readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller);
+
+ readableByteStreamControllerCommitPullIntoDescriptor(stream, desc);
+ readableByteStreamControllerCommitPullIntoDescriptors(stream, filledPullIntos);
}
function readableByteStreamControllerRespondWithNewView(controller, view) {
diff --git a/lib/internal/webstreams/util.js b/lib/internal/webstreams/util.js
index 2c70ef7acdfe66..5bf016f73b7af5 100644
--- a/lib/internal/webstreams/util.js
+++ b/lib/internal/webstreams/util.js
@@ -1,6 +1,8 @@
'use strict';
const {
+ ArrayBufferPrototypeGetByteLength,
+ ArrayBufferPrototypeGetDetached,
ArrayBufferPrototypeSlice,
ArrayPrototypePush,
ArrayPrototypeShift,
@@ -107,6 +109,14 @@ function cloneAsUint8Array(view) {
);
}
+function canCopyArrayBuffer(toBuffer, toIndex, fromBuffer, fromIndex, count) {
+ return toBuffer !== fromBuffer &&
+ !ArrayBufferPrototypeGetDetached(toBuffer) &&
+ !ArrayBufferPrototypeGetDetached(fromBuffer) &&
+ toIndex + count <= ArrayBufferPrototypeGetByteLength(toBuffer) &&
+ fromIndex + count <= ArrayBufferPrototypeGetByteLength(fromBuffer);
+}
+
function isBrandCheck(brand) {
return (value) => {
return value != null &&
@@ -261,6 +271,7 @@ module.exports = {
ArrayBufferViewGetByteLength,
ArrayBufferViewGetByteOffset,
AsyncIterator,
+ canCopyArrayBuffer,
createPromiseCallback,
cloneAsUint8Array,
copyArrayBuffer,
diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md
index 86d83e913e6ed4..640129d187033b 100644
--- a/test/fixtures/wpt/README.md
+++ b/test/fixtures/wpt/README.md
@@ -27,7 +27,7 @@ Last update:
- performance-timeline: https://github.com/web-platform-tests/wpt/tree/94caab7038/performance-timeline
- resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing
- resources: https://github.com/web-platform-tests/wpt/tree/1e140d63ec/resources
-- streams: https://github.com/web-platform-tests/wpt/tree/2bd26e124c/streams
+- streams: https://github.com/web-platform-tests/wpt/tree/bc9dcbbf1a/streams
- url: https://github.com/web-platform-tests/wpt/tree/67880a4eb8/url
- user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing
- wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi
diff --git a/test/fixtures/wpt/streams/idlharness-shadowrealm.window.js b/test/fixtures/wpt/streams/idlharness-shadowrealm.window.js
deleted file mode 100644
index 099b2475ca7e87..00000000000000
--- a/test/fixtures/wpt/streams/idlharness-shadowrealm.window.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// META: script=/resources/idlharness-shadowrealm.js
-idl_test_shadowrealm(["streams"], ["dom"]);
diff --git a/test/fixtures/wpt/streams/idlharness.any.js b/test/fixtures/wpt/streams/idlharness.any.js
index 42a17da58c5ae3..0be03b2078f9bc 100644
--- a/test/fixtures/wpt/streams/idlharness.any.js
+++ b/test/fixtures/wpt/streams/idlharness.any.js
@@ -1,4 +1,4 @@
-// META: global=window,worker
+// META: global=window,worker,shadowrealm-in-window
// META: script=/resources/WebIDLParser.js
// META: script=/resources/idlharness.js
// META: timeout=long
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/general.any.js b/test/fixtures/wpt/streams/readable-byte-streams/general.any.js
index cdce2244c3c84b..4b0c73865f7cf9 100644
--- a/test/fixtures/wpt/streams/readable-byte-streams/general.any.js
+++ b/test/fixtures/wpt/streams/readable-byte-streams/general.any.js
@@ -870,11 +870,11 @@ promise_test(() => {
start(c) {
controller = c;
},
- async pull() {
+ pull() {
byobRequestDefined.push(controller.byobRequest !== null);
const initialByobRequest = controller.byobRequest;
- const transferredView = await transferArrayBufferView(controller.byobRequest.view);
+ const transferredView = transferArrayBufferView(controller.byobRequest.view);
transferredView[0] = 0x01;
controller.byobRequest.respondWithNewView(transferredView);
@@ -2288,7 +2288,7 @@ promise_test(async t => {
await pullCalledPromise;
// Transfer the original BYOB request's buffer, and respond with a new view on that buffer
- const transferredView = await transferArrayBufferView(controller.byobRequest.view);
+ const transferredView = transferArrayBufferView(controller.byobRequest.view);
const newView = transferredView.subarray(0, 1);
newView[0] = 42;
@@ -2328,7 +2328,7 @@ promise_test(async t => {
await pullCalledPromise;
// Transfer the original BYOB request's buffer, and respond with an empty view on that buffer
- const transferredView = await transferArrayBufferView(controller.byobRequest.view);
+ const transferredView = transferArrayBufferView(controller.byobRequest.view);
const newView = transferredView.subarray(0, 0);
controller.close();
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/patched-global.any.js b/test/fixtures/wpt/streams/readable-byte-streams/patched-global.any.js
new file mode 100644
index 00000000000000..ce2e9e9993ae57
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-byte-streams/patched-global.any.js
@@ -0,0 +1,54 @@
+// META: global=window,worker,shadowrealm
+// META: script=../resources/test-utils.js
+'use strict';
+
+// Tests which patch the global environment are kept separate to avoid
+// interfering with other tests.
+
+promise_test(async (t) => {
+ let controller;
+ const rs = new ReadableStream({
+ type: 'bytes',
+ start(c) {
+ controller = c;
+ }
+ });
+ const reader = rs.getReader({mode: 'byob'});
+
+ const length = 0x4000;
+ const buffer = new ArrayBuffer(length);
+ const bigArray = new BigUint64Array(buffer, length - 8, 1);
+
+ const read1 = reader.read(new Uint8Array(new ArrayBuffer(0x100)));
+ const read2 = reader.read(bigArray);
+
+ let flag = false;
+ Object.defineProperty(Object.prototype, 'then', {
+ get: t.step_func(() => {
+ if (!flag) {
+ flag = true;
+ assert_equals(controller.byobRequest, null, 'byobRequest should be null after filling both views');
+ }
+ }),
+ configurable: true
+ });
+ t.add_cleanup(() => {
+ delete Object.prototype.then;
+ });
+
+ controller.enqueue(new Uint8Array(0x110).fill(0x42));
+ assert_true(flag, 'patched then() should be called');
+
+ // The first read() is filled entirely with 0x100 bytes
+ const result1 = await read1;
+ assert_false(result1.done, 'result1.done');
+ assert_typed_array_equals(result1.value, new Uint8Array(0x100).fill(0x42), 'result1.value');
+
+ // The second read() is filled with the remaining 0x10 bytes
+ const result2 = await read2;
+ assert_false(result2.done, 'result2.done');
+ assert_equals(result2.value.constructor, BigUint64Array, 'result2.value constructor');
+ assert_equals(result2.value.byteOffset, length - 8, 'result2.value byteOffset');
+ assert_equals(result2.value.length, 1, 'result2.value length');
+ assert_array_equals([...result2.value], [0x42424242_42424242n], 'result2.value contents');
+}, 'Patched then() sees byobRequest after filling all pending pull-into descriptors');
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/tee.any.js b/test/fixtures/wpt/streams/readable-byte-streams/tee.any.js
index 7dd5ba3f3fb013..60d82b9cf6a1fd 100644
--- a/test/fixtures/wpt/streams/readable-byte-streams/tee.any.js
+++ b/test/fixtures/wpt/streams/readable-byte-streams/tee.any.js
@@ -934,3 +934,36 @@ promise_test(async () => {
assert_typed_array_equals(result4.value, new Uint8Array([0]).subarray(0, 0), 'second chunk from branch2 should be correct');
}, 'ReadableStream teeing with byte source: respond() and close() while both branches are pulling');
+
+promise_test(async t => {
+ let pullCount = 0;
+ const arrayBuffer = new Uint8Array([0x01, 0x02, 0x03]).buffer;
+ const enqueuedChunk = new Uint8Array(arrayBuffer, 2);
+ assert_equals(enqueuedChunk.length, 1);
+ assert_equals(enqueuedChunk.byteOffset, 2);
+ const rs = new ReadableStream({
+ type: 'bytes',
+ pull(c) {
+ ++pullCount;
+ if (pullCount === 1) {
+ c.enqueue(enqueuedChunk);
+ }
+ }
+ });
+
+ const [branch1, branch2] = rs.tee();
+ const reader1 = branch1.getReader();
+ const reader2 = branch2.getReader();
+
+ const [result1, result2] = await Promise.all([reader1.read(), reader2.read()]);
+ assert_equals(result1.done, false, 'reader1 done');
+ assert_equals(result2.done, false, 'reader2 done');
+
+ const view1 = result1.value;
+ const view2 = result2.value;
+ // The first stream has the transferred buffer, but the second stream has the
+ // cloned buffer.
+ const underlying = new Uint8Array([0x01, 0x02, 0x03]).buffer;
+ assert_typed_array_equals(view1, new Uint8Array(underlying, 2), 'reader1 value');
+ assert_typed_array_equals(view2, new Uint8Array([0x03]), 'reader2 value');
+}, 'ReadableStream teeing with byte source: reading an array with a byte offset should clone correctly');
diff --git a/test/fixtures/wpt/streams/readable-streams/crashtests/from-cross-realm.https.html b/test/fixtures/wpt/streams/readable-streams/crashtests/from-cross-realm.https.html
new file mode 100644
index 00000000000000..58a4371186ece7
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-streams/crashtests/from-cross-realm.https.html
@@ -0,0 +1,18 @@
+
+
+
diff --git a/test/fixtures/wpt/streams/readable-streams/owning-type-video-frame.any.js b/test/fixtures/wpt/streams/readable-streams/owning-type-video-frame.any.js
index b652f9c5fcb4b6..ec01fda0b3c737 100644
--- a/test/fixtures/wpt/streams/readable-streams/owning-type-video-frame.any.js
+++ b/test/fixtures/wpt/streams/readable-streams/owning-type-video-frame.any.js
@@ -1,4 +1,4 @@
-// META: global=window,worker,shadowrealm
+// META: global=window,worker
// META: script=../resources/test-utils.js
// META: script=../resources/rs-utils.js
'use strict';
diff --git a/test/fixtures/wpt/streams/resources/rs-utils.js b/test/fixtures/wpt/streams/resources/rs-utils.js
index f1a014275a2fbc..0f7742a5b3b190 100644
--- a/test/fixtures/wpt/streams/resources/rs-utils.js
+++ b/test/fixtures/wpt/streams/resources/rs-utils.js
@@ -1,5 +1,42 @@
'use strict';
(function () {
+ // Fake setInterval-like functionality in environments that don't have it
+ class IntervalHandle {
+ constructor(callback, delayMs) {
+ this.callback = callback;
+ this.delayMs = delayMs;
+ this.cancelled = false;
+ Promise.resolve().then(() => this.check());
+ }
+
+ async check() {
+ while (true) {
+ await new Promise(resolve => step_timeout(resolve, this.delayMs));
+ if (this.cancelled) {
+ return;
+ }
+ this.callback();
+ }
+ }
+
+ cancel() {
+ this.cancelled = true;
+ }
+ }
+
+ let localSetInterval, localClearInterval;
+ if (typeof globalThis.setInterval !== "undefined" &&
+ typeof globalThis.clearInterval !== "undefined") {
+ localSetInterval = globalThis.setInterval;
+ localClearInterval = globalThis.clearInterval;
+ } else {
+ localSetInterval = function setInterval(callback, delayMs) {
+ return new IntervalHandle(callback, delayMs);
+ }
+ localClearInterval = function clearInterval(handle) {
+ handle.cancel();
+ }
+ }
class RandomPushSource {
constructor(toPush) {
@@ -18,12 +55,12 @@
}
if (!this.started) {
- this._intervalHandle = setInterval(writeChunk, 2);
+ this._intervalHandle = localSetInterval(writeChunk, 2);
this.started = true;
}
if (this.paused) {
- this._intervalHandle = setInterval(writeChunk, 2);
+ this._intervalHandle = localSetInterval(writeChunk, 2);
this.paused = false;
}
@@ -37,7 +74,7 @@
if (source.toPush > 0 && source.pushed > source.toPush) {
if (source._intervalHandle) {
- clearInterval(source._intervalHandle);
+ localClearInterval(source._intervalHandle);
source._intervalHandle = undefined;
}
source.closed = true;
@@ -55,7 +92,7 @@
if (this.started) {
this.paused = true;
- clearInterval(this._intervalHandle);
+ localClearInterval(this._intervalHandle);
this._intervalHandle = undefined;
} else {
throw new Error('Can\'t pause reading an unstarted source.');
@@ -178,15 +215,7 @@
}
function transferArrayBufferView(view) {
- const noopByteStream = new ReadableStream({
- type: 'bytes',
- pull(c) {
- c.byobRequest.respond(c.byobRequest.view.byteLength);
- c.close();
- }
- });
- const reader = noopByteStream.getReader({ mode: 'byob' });
- return reader.read(view).then((result) => result.value);
+ return structuredClone(view, { transfer: [view.buffer] });
}
self.RandomPushSource = RandomPushSource;
diff --git a/test/fixtures/wpt/streams/transferable/transfer-with-messageport.window.js b/test/fixtures/wpt/streams/transferable/transfer-with-messageport.window.js
index 37f8c9df169607..3bfe634a6e153d 100644
--- a/test/fixtures/wpt/streams/transferable/transfer-with-messageport.window.js
+++ b/test/fixtures/wpt/streams/transferable/transfer-with-messageport.window.js
@@ -105,7 +105,7 @@ async function transferMessagePortWith(constructor) {
await transferMessagePortWithOrder3(new constructor());
}
-async function advancedTransferMesagePortWith(constructor) {
+async function advancedTransferMessagePortWith(constructor) {
await transferMessagePortWithOrder4(new constructor());
await transferMessagePortWithOrder5(new constructor());
await transferMessagePortWithOrder6(new constructor());
@@ -166,7 +166,7 @@ async function mixedTransferMessagePortWithOrder3() {
);
}
-async function mixedTransferMesagePortWith() {
+async function mixedTransferMessagePortWith() {
await mixedTransferMessagePortWithOrder1();
await mixedTransferMessagePortWithOrder2();
await mixedTransferMessagePortWithOrder3();
@@ -185,19 +185,19 @@ promise_test(async t => {
}, "Transferring a MessagePort with a TransformStream should set `.ports`");
promise_test(async t => {
- await transferMessagePortWith(ReadableStream);
+ await advancedTransferMessagePortWith(ReadableStream);
}, "Transferring a MessagePort with a ReadableStream should set `.ports`, advanced");
promise_test(async t => {
- await transferMessagePortWith(WritableStream);
+ await advancedTransferMessagePortWith(WritableStream);
}, "Transferring a MessagePort with a WritableStream should set `.ports`, advanced");
promise_test(async t => {
- await transferMessagePortWith(TransformStream);
+ await advancedTransferMessagePortWith(TransformStream);
}, "Transferring a MessagePort with a TransformStream should set `.ports`, advanced");
promise_test(async t => {
- await mixedTransferMesagePortWith();
+ await mixedTransferMessagePortWith();
}, "Transferring a MessagePort with multiple streams should set `.ports`");
test(() => {
diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json
index e29d2eabde1f26..c2ff3c4ed07d43 100644
--- a/test/fixtures/wpt/versions.json
+++ b/test/fixtures/wpt/versions.json
@@ -68,7 +68,7 @@
"path": "resources"
},
"streams": {
- "commit": "2bd26e124cf17b2f0a25c150794d640b07b2a870",
+ "commit": "bc9dcbbf1a4c2c741ef47f47d6ede6458f40c4a4",
"path": "streams"
},
"url": {