diff --git a/interfaces/streams.idl b/interfaces/streams.idl index f7084dd6061042..d7a0f12e9d955f 100644 --- a/interfaces/streams.idl +++ b/interfaces/streams.idl @@ -56,7 +56,7 @@ callback UnderlyingSourceStartCallback = any (ReadableStreamController controlle callback UnderlyingSourcePullCallback = Promise (ReadableStreamController controller); callback UnderlyingSourceCancelCallback = Promise (optional any reason); -enum ReadableStreamType { "bytes" }; +enum ReadableStreamType { "bytes", "owning" }; interface mixin ReadableStreamGenericReader { readonly attribute Promise closed; diff --git a/streams/readable-streams/owning-type-message-port.any.js b/streams/readable-streams/owning-type-message-port.any.js new file mode 100644 index 00000000000000..e9961ce042256a --- /dev/null +++ b/streams/readable-streams/owning-type-message-port.any.js @@ -0,0 +1,49 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +'use strict'; + +promise_test(async () => { + const channel = new MessageChannel; + const port1 = channel.port1; + const port2 = channel.port2; + + const source = { + start(controller) { + controller.enqueue(port1, { transfer : [ port1 ] }); + }, + type: 'owning' + }; + + const stream = new ReadableStream(source); + + const chunk = await stream.getReader().read(); + + assert_not_equals(chunk.value, port1); + + let promise = new Promise(resolve => port2.onmessage = e => resolve(e.data)); + chunk.value.postMessage("toPort2"); + assert_equals(await promise, "toPort2"); + + promise = new Promise(resolve => chunk.value.onmessage = e => resolve(e.data)); + port2.postMessage("toPort1"); + assert_equals(await promise, "toPort1"); +}, 'Transferred MessageChannel works as expected'); + +promise_test(async t => { + const channel = new MessageChannel; + const port1 = channel.port1; + const port2 = channel.port2; + + const source = { + start(controller) { + controller.enqueue({ port1 }, { transfer : [ port1 ] }); + }, + type: 'owning' + }; + + const stream = new ReadableStream(source); + const [clone1, clone2] = stream.tee(); + + await promise_rejects_dom(t, "DataCloneError", clone2.getReader().read()); +}, 'Second branch of owning ReadableStream tee should end up into errors with transfer only values'); diff --git a/streams/readable-streams/owning-type-video-frame.any.js b/streams/readable-streams/owning-type-video-frame.any.js new file mode 100644 index 00000000000000..ec01fda0b3c737 --- /dev/null +++ b/streams/readable-streams/owning-type-video-frame.any.js @@ -0,0 +1,128 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +'use strict'; + +function createVideoFrame() +{ + let init = { + format: 'I420', + timestamp: 1234, + codedWidth: 4, + codedHeight: 2 + }; + let data = new Uint8Array([ + 1, 2, 3, 4, 5, 6, 7, 8, // y + 1, 2, // u + 1, 2, // v + ]); + + return new VideoFrame(data, init); +} + +promise_test(async () => { + const videoFrame = createVideoFrame(); + videoFrame.test = 1; + const source = { + start(controller) { + assert_equals(videoFrame.format, 'I420'); + controller.enqueue(videoFrame, { transfer : [ videoFrame ] }); + assert_equals(videoFrame.format, null); + assert_equals(videoFrame.test, 1); + }, + type: 'owning' + }; + + const stream = new ReadableStream(source); + // Cancelling the stream should close all video frames, thus no console messages of GCing VideoFrames should happen. + stream.cancel(); +}, 'ReadableStream of type owning should close serialized chunks'); + +promise_test(async () => { + const videoFrame = createVideoFrame(); + videoFrame.test = 1; + const source = { + start(controller) { + assert_equals(videoFrame.format, 'I420'); + controller.enqueue({ videoFrame }, { transfer : [ videoFrame ] }); + assert_equals(videoFrame.format, null); + assert_equals(videoFrame.test, 1); + }, + type: 'owning' + }; + + const stream = new ReadableStream(source); + const reader = stream.getReader(); + + const chunk = await reader.read(); + assert_equals(chunk.value.videoFrame.format, 'I420'); + assert_equals(chunk.value.videoFrame.test, undefined); + + chunk.value.videoFrame.close(); +}, 'ReadableStream of type owning should transfer JS chunks with transferred values'); + +promise_test(async t => { + const videoFrame = createVideoFrame(); + videoFrame.close(); + const source = { + start(controller) { + assert_throws_dom("DataCloneError", () => controller.enqueue(videoFrame, { transfer : [ videoFrame ] })); + }, + type: 'owning' + }; + + const stream = new ReadableStream(source); + const reader = stream.getReader(); + + await promise_rejects_dom(t, "DataCloneError", reader.read()); +}, 'ReadableStream of type owning should error when trying to enqueue not serializable values'); + +promise_test(async () => { + const videoFrame = createVideoFrame(); + const source = { + start(controller) { + controller.enqueue(videoFrame, { transfer : [ videoFrame ] }); + }, + type: 'owning' + }; + + const stream = new ReadableStream(source); + const [clone1, clone2] = stream.tee(); + + const chunk1 = await clone1.getReader().read(); + const chunk2 = await clone2.getReader().read(); + + assert_equals(videoFrame.format, null); + assert_equals(chunk1.value.format, 'I420'); + assert_equals(chunk2.value.format, 'I420'); + + chunk1.value.close(); + chunk2.value.close(); +}, 'ReadableStream of type owning should clone serializable objects when teeing'); + +promise_test(async () => { + const videoFrame = createVideoFrame(); + videoFrame.test = 1; + const source = { + start(controller) { + assert_equals(videoFrame.format, 'I420'); + controller.enqueue({ videoFrame }, { transfer : [ videoFrame ] }); + assert_equals(videoFrame.format, null); + assert_equals(videoFrame.test, 1); + }, + type: 'owning' + }; + + const stream = new ReadableStream(source); + const [clone1, clone2] = stream.tee(); + + const chunk1 = await clone1.getReader().read(); + const chunk2 = await clone2.getReader().read(); + + assert_equals(videoFrame.format, null); + assert_equals(chunk1.value.videoFrame.format, 'I420'); + assert_equals(chunk2.value.videoFrame.format, 'I420'); + + chunk1.value.videoFrame.close(); + chunk2.value.videoFrame.close(); +}, 'ReadableStream of type owning should clone JS Objects with serializables when teeing'); diff --git a/streams/readable-streams/owning-type.any.js b/streams/readable-streams/owning-type.any.js new file mode 100644 index 00000000000000..27a3dda894e4d6 --- /dev/null +++ b/streams/readable-streams/owning-type.any.js @@ -0,0 +1,91 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +'use strict'; + +test(() => { + new ReadableStream({ type: 'owning' }); // ReadableStream constructed with 'owning' type +}, 'ReadableStream can be constructed with owning type'); + +test(() => { + let startCalled = false; + + const source = { + start(controller) { + assert_equals(this, source, 'source is this during start'); + assert_true(controller instanceof ReadableStreamDefaultController, 'default controller'); + startCalled = true; + }, + type: 'owning' + }; + + new ReadableStream(source); + assert_true(startCalled); +}, 'ReadableStream of type owning should call start with a ReadableStreamDefaultController'); + +test(() => { + let startCalled = false; + + const source = { + start(controller) { + controller.enqueue("a", { transfer: [] }); + controller.enqueue("a", { transfer: undefined }); + startCalled = true; + }, + type: 'owning' + }; + + new ReadableStream(source); + assert_true(startCalled); +}, 'ReadableStream should be able to call enqueue with an empty transfer list'); + +test(() => { + let startCalled = false; + + const uint8Array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const buffer = uint8Array.buffer; + let source = { + start(controller) { + startCalled = true; + assert_throws_js(TypeError, () => { controller.enqueue(buffer, { transfer : [ buffer ] }); }, "transfer list is not empty"); + } + }; + + new ReadableStream(source); + assert_true(startCalled); + + startCalled = false; + source = { + start(controller) { + startCalled = true; + assert_throws_js(TypeError, () => { controller.enqueue(buffer, { get transfer() { throw new TypeError(); } }) }, "getter throws"); + } + }; + + new ReadableStream(source); + assert_true(startCalled); +}, 'ReadableStream should check transfer parameter'); + +promise_test(async () => { + const uint8Array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]); + const buffer = uint8Array.buffer; + buffer.test = 1; + const source = { + start(controller) { + assert_equals(buffer.byteLength, 8); + controller.enqueue(buffer, { transfer : [ buffer ] }); + assert_equals(buffer.byteLength, 0); + assert_equals(buffer.test, 1); + }, + type: 'owning' + }; + + const stream = new ReadableStream(source); + const reader = stream.getReader(); + + const chunk = await reader.read(); + + assert_not_equals(chunk.value, buffer); + assert_equals(chunk.value.byteLength, 8); + assert_equals(chunk.value.test, undefined); +}, 'ReadableStream of type owning should transfer enqueued chunks');