From ca89482a1949eeb49368fe7e174f093d414e9921 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Thu, 9 Apr 2020 19:05:10 -0400 Subject: [PATCH 01/30] Rewrite to use Web IDL, and generally modernize Normative changes, all stemming from the Web IDL adaptation: * All classes are now exposed globally. Formerly, ReadableStreamDefaultReader, ReadableStreamBYOBReader, ReadableStreamDefaultController, ReadableByteStreamController, WritableStreamDefaultWriter, WritableStreamDefaultController, and TransformStreamDefaultController were not exposed. Closes #586. * All classes now have [Symbol.toStringTag] properties. (At least, pending https://github.com/heycam/webidl/pull/357 resolution.) Closes #952. * All methods and accesors are now enumerable, per Web IDL defaults, instead of non-enumerable, per ECMAScript defaults. * For the queuing strategy classes, their size and highWaterMark properties are now getters on the prototype, instead of data properties on the prototype and instance (respectively). Closes #1005. Note that the size function is not settable anymore, but highWaterMark has a setter. * Some functions have changed their length property value. * Some exceptions are thrown earlier, at argument-conversion time. Editorial changes: * All APIs are specified to using Web IDL now, instead of using a modified version of the ECMAScript specification conventions. We continue using abstract operations and completion records for now, and we have to drop down to the ECMAScript level in a couple places (notably for dealing with %ObjectPrototype% vs. null-prototype iteration result objects, and transferring array buffers). But overall this removes a lot of type-checking and conversion boilerplate from the specification. * Individual abstract operations, constructors, methods, and properties no longer have their own heading. They are instead lumped together in sections. Closes #885. * The constructors, methods, and properties are now documented in a per-class block, using the usual WHATWG "domintro" style. Closes #907. * Abstract operations are now consistently alphabetized within their section. Closes #684. * By using Bikeshed's
feature, we now get automatic identifier highlighting. Closes #687. * Switched to 100-character line limits, 1-space indents, and omitting end tags, per WHATWG conventions. * Removed usage of emu-algify in favor of using some more of Bikeshed's built-in features, plus manually annotating a few things. * Switched to concise Bikeshed linking syntax, e.g. [=term=] and [$AbstractOp$]. * Eliminated a number of utility abstract operations, especially around calling functions, by better using Web IDL. Other bug fixes: * Web IDL makes constructor behavior clear, so this closes #965. --- .editorconfig | 15 +- .gitignore | 3 - .pr-preview.json | 6 - .travis.yml | 2 - Makefile | 7 +- README.md | 4 - index.bs | 10797 +++++++++++++++++++++++---------------------- local-watch.js | 83 - package.json | 11 - 9 files changed, 5476 insertions(+), 5452 deletions(-) delete mode 100644 local-watch.js delete mode 100644 package.json diff --git a/.editorconfig b/.editorconfig index 874a0b40a..f0b203f28 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,18 +4,19 @@ root = true end_of_line = lf insert_final_newline = true charset = utf-8 -indent_size = 2 +indent_size = 1 indent_style = space trim_trailing_whitespace = true - -[*.{js,bs}] -max_line_length = 120 - -[.gitmodules] -indent_style = tab +max_line_length = 100 [Makefile] indent_style = tab [.travis.yml] indent_size = 2 + +[*.js] +max_line_length = 120 + +[.gitmodules] +indent_style = tab diff --git a/.gitignore b/.gitignore index a9e77d4d4..491ac7aa1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,3 @@ /deploy_key.pub /index.html /review.sh -/index.html.* -/node_modules/ -/npm-debug.log diff --git a/.pr-preview.json b/.pr-preview.json index 4b2477666..03fdd6beb 100644 --- a/.pr-preview.json +++ b/.pr-preview.json @@ -5,11 +5,5 @@ "force": 1, "md-status": "LS-PR", "md-Text-Macro": "PR-NUMBER {{ pull_request.number }}" - }, - "post_processing": { - "name": "emu-algify", - "options": { - "throwingIndicators": true - } } } diff --git a/.travis.yml b/.travis.yml index 9f4f47a56..a525d76bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: node_js node_js: stable -sudo: false env: global: @@ -12,7 +11,6 @@ before_install: script: - npm test - cd .. - - npm install - make deploy branches: diff --git a/Makefile b/Makefile index cf8a4e24a..4443a981f 100644 --- a/Makefile +++ b/Makefile @@ -2,17 +2,14 @@ SHELL=/bin/bash -o pipefail .PHONY: local remote deploy review remote: index.bs - curl https://api.csswg.org/bikeshed/ -f -F file=@index.bs > index.html.postbs -F md-Text-Macro="SNAPSHOT-LINK LOCAL COPY" - node_modules/.bin/emu-algify --throwing-indicators < index.html.postbs > index.html + curl https://api.csswg.org/bikeshed/ -f -F file=@index.bs > index.html -F md-Text-Macro="COMMIT-SHA LOCAL COPY" local: index.bs - bikeshed spec index.bs index.html.postbs --md-Text-Macro="SNAPSHOT-LINK LOCAL COPY" - node_modules/.bin/emu-algify --throwing-indicators < index.html.postbs > index.html + bikeshed spec index.bs index.html --md-Text-Macro="COMMIT-SHA LOCAL COPY" deploy: index.bs curl --remote-name --fail https://resources.whatwg.org/build/deploy.sh EXTRA_FILES="demos/* demos/**/*" \ - POST_BUILD_STEP='node_modules/.bin/emu-algify --throwing-indicators < "$$DIR/index.html" > "$$DIR/index.html.tmp"; mv "$$DIR/index.html.tmp" "$$DIR/index.html"' \ bash ./deploy.sh review: index.bs diff --git a/README.md b/README.md index 6cb27f4a5..56b3577b6 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,6 @@ implementation in order to pass those tests. ## Building "locally" -This standard requires a recent version of [Node.js](https://nodejs.org/en/) to be installed as a -prerequisite. Once that's done, you'll need to do a one-time run of `npm install` to set up our -tooling. - For quick local iteration, run `make`. To verify your changes locally, run `make deploy`. See more in the [WHATWG Contributor Guidelines](https://github.com/whatwg/meta/blob/master/CONTRIBUTING.md#building). diff --git a/index.bs b/index.bs index d83fcd895..5edff586a 100644 --- a/index.bs +++ b/index.bs @@ -7,49 +7,59 @@ Abstract: This specification provides APIs for creating, composing, and consumin Abstract: to low-level I/O primitives. Translation: ja https://triple-underscore.github.io/Streams-ja.html !Demos: streams.spec.whatwg.org/demos -Opaque Elements: emu-alg +Indent: 1 +Markup Shorthands: markdown yes
-urlPrefix: https://tc39.github.io/ecma262/; spec: ECMASCRIPT
-    text: %Uint8Array%; url: #sec-typedarray-objects; type: constructor
-    text: %AsyncIteratorPrototype%; url: #sec-asynciteratorprototype; type: interface
-    text: AsyncIterator; url: #sec-asynciterator-interface; type: interface
-    text: ArrayBuffer; url: #sec-arraybuffer-objects; type: interface
-    text: DataView; url: #sec-dataview-objects; type: interface
-    text: Number; url: #sec-ecmascript-language-types-number-type; type: interface
-    text: Uint8Array; url: #sec-typedarray-objects; type: interface
-    text: typed array; url: #sec-typedarray-objects; type: dfn
-    text: the typed array constructors table; url: #table-49; type: dfn
-    text: TypeError; url: #sec-native-error-types-used-in-this-standard-typeerror; type: exception
-    text: Invoke; url: #sec-invoke; type: abstract-op
-    text: DestructuringAssignmentEvaluation; url: #sec-runtime-semantics-destructuringassignmentevaluation; type: abstract-op
-    text: map; url: #sec-array.prototype.map; type: method; for: Array.prototype
-    text: CreateIterResultObject; url: #sec-createiterresultobject; type: abstract-op
+urlPrefix: https://tc39.es/ecma262/; spec: ECMASCRIPT
+ type: constructor
+  text: %Uint8Array%; url: #sec-typedarray-objects
+  text: %DataView%; url: #sec-dataview-constructor
+  text: %ArrayBuffer%; url: #sec-arraybuffer-constructor
+ type: interface
+  text: %ObjectPrototype%; url: #sec-properties-of-the-object-prototype-object
+  text: ArrayBuffer; url: #sec-arraybuffer-objects
+  text: DataView; url: #sec-dataview-objects
+  text: Number; url: #sec-ecmascript-language-types-number-type
+  text: Uint8Array; url: #sec-typedarray-objects
+ type: dfn
+  text: abstract operation; url: #sec-algorithm-conventions-abstract-operations
+  text: completion record; url: #sec-completion-record-specification-type
+  text: internal slot; url: #sec-object-internal-methods-and-internal-slots
+  text: record; url: #sec-list-and-record-specification-type
+  text: the current Realm; url: #current-realm
+  text: the typed array constructors table; url: #table-49
+  text: typed array; url: #sec-typedarray-objects
+ type: abstract-op
+  text: CloneArrayBuffer; url: #sec-clonearraybuffer
+  text: CopyDataBlockBytes; url: #sec-copydatablockbytes
+  text: CreateArrayFromList; url: #sec-createarrayfromlist
+  text: CreateBuiltinFunction; url: #sec-createbuiltinfunction
+  text: CreateDataProperty; url: #sec-createdataproperty
+  text: CreateIterResultObject; url: #sec-createiterresultobject
+  text: Construct; url: #sec-construct
+  text: DetachArrayBuffer; url: #sec-detacharraybuffer
+  text: Get; url: #sec-get
+  text: GetV; url: #sec-getv
+  text: IsDetachedBuffer; url: #sec-isdetachedbuffer
+  text: IsInteger; url: #sec-isinteger
+  text: OrdinaryObjectCreate; url: #sec-ordinaryobjectcreate
+  text: SetFunctionLength; url: #sec-setfunctionlength
+  text: SetFunctionName; url: #sec-setfunctionname
+  text: Type; url: #sec-ecmascript-data-types-and-values
+ text: TypeError; url: #sec-native-error-types-used-in-this-standard-typeerror; type: exception
+ text: map; url: #sec-array.prototype.map; type: method; for: Array.prototype
 

Introduction

@@ -58,5398 +68,5532 @@ urlPrefix: https://tc39.github.io/ecma262/; spec: ECMASCRIPT This section is non-normative. -Large swathes of the web platform are built on streaming data: that is, data that is created, processed, and consumed -in an incremental fashion, without ever reading all of it into memory. The Streams Standard provides a common set of -APIs for creating and interfacing with such streaming data, embodied in readable streams, -writable streams, and transform streams. - -These APIs have been designed to efficiently map to low-level I/O primitives, including specializations for byte streams -where appropriate. They allow easy composition of multiple streams into pipe chains, or can be used directly via -readers and writers. Finally, they are designed to automatically provide backpressure and queuing. - -This standard provides the base stream primitives which other parts of the web platform can use to expose their -streaming data. For example, [[FETCH]] exposes {{Response}} bodies as {{ReadableStream}} instances. More generally, the -platform is full of streaming abstractions waiting to be expressed as streams: multimedia streams, file streams, -inter-global communication, and more benefit from being able to process data incrementally instead of buffering it all -into memory and processing it in one go. By providing the foundation for these streams to be exposed to developers, the -Streams Standard enables use cases like: - - - -Web developers can also use the APIs described here to create their own streams, with the same APIs as those provided by -the platform. Other developers can then transparently compose platform-provided streams with those supplied by -libraries. In this way, the APIs described here provide unifying abstraction for all streams, encouraging an -ecosystem to grow around these shared and composable interfaces. +Large swathes of the web platform are built on streaming data: that is, data that is created, +processed, and consumed in an incremental fashion, without ever reading all of it into memory. The +Streams Standard provides a common set of APIs for creating and interfacing with such streaming +data, embodied in [=readable streams=], [=writable streams=], and [=transform streams=]. + +These APIs have been designed to efficiently map to low-level I/O primitives, including +specializations for byte streams where appropriate. They allow easy composition of multiple streams +into [=pipe chains=], or can be used directly via [=readers=] and [=writers=]. Finally, they are +designed to automatically provide [=backpressure=] and queuing. + +This standard provides the base stream primitives which other parts of the web platform can use to +expose their streaming data. For example, [[FETCH]] exposes {{Response}} bodies as +{{ReadableStream}} instances. More generally, the platform is full of streaming abstractions waiting +to be expressed as streams: multimedia streams, file streams, inter-global communication, and more +benefit from being able to process data incrementally instead of buffering it all into memory and +processing it in one go. By providing the foundation for these streams to be exposed to developers, +the Streams Standard enables use cases like: + +* Video effects: piping a readable video stream through a transform stream that applies effects in + real time. +* Decompression: piping a file stream through a transform stream that selectively decompresses files + from a .tgz archive, turning them into <{img}> elements as the user scrolls through an + image gallery. +* Image decoding: piping an HTTP response stream through a transform stream that decodes bytes into + bitmap data, and then through another transform that translates bitmaps into PNGs. If installed + inside the {{ServiceWorkerGlobalScope/fetch}} hook of a service worker, this would allow + developers to transparently polyfill new image formats. [[SERVICE-WORKERS]] + +Web developers can also use the APIs described here to create their own streams, with the same APIs +as those provided by the platform. Other developers can then transparently compose platform-provided +streams with those supplied by libraries. In this way, the APIs described here provide unifying +abstraction for all streams, encouraging an ecosystem to grow around these shared and composable +interfaces.

Model

-A chunk is a single piece of data that is written to or read from a stream. It can be of any type; -streams can even contain chunks of different types. A chunk will often not be the most atomic unit of data for a given -stream; for example a byte stream might contain chunks consisting of 16 KiB {{Uint8Array}}s, instead of single bytes. +A chunk is a single piece of data that is written to or read from a stream. It can +be of any type; streams can even contain chunks of different types. A chunk will often not be the +most atomic unit of data for a given stream; for example a byte stream might contain chunks +consisting of 16 KiB {{Uint8Array}}s, instead of single bytes.

Readable streams

-A readable stream represents a source of data, from which you can read. In other words, data comes -out of a readable stream. Concretely, a readable stream is an instance of the {{ReadableStream}} class. +A readable stream represents a source of data, from which you can read. In other +words, data comes +out of a readable stream. Concretely, a readable stream is an instance of the +{{ReadableStream}} class. -Although a readable stream can be created with arbitrary behavior, most readable streams wrap a lower-level I/O source, -called the underlying source. There are two types of underlying source: push sources and pull sources. +Although a readable stream can be created with arbitrary behavior, most readable streams wrap a +lower-level I/O source, called the underlying source. There are two types of underlying +source: push sources and pull sources. -Push sources push data at you, whether or not you are listening for it. They may also -provide a mechanism for pausing and resuming the flow of data. An example push source is a TCP socket, where data is -constantly being pushed from the OS level, at a rate that can be controlled by changing the TCP window size. +Push sources push data at you, whether or not you are listening for it. +They may also provide a mechanism for pausing and resuming the flow of data. An example push source +is a TCP socket, where data is constantly being pushed from the OS level, at a rate that can be +controlled by changing the TCP window size. -Pull sources require you to request data from them. The data may be available -synchronously, e.g. if it is held by the operating system's in-memory buffers, or asynchronously, e.g. if it has to be -read from disk. An example pull source is a file handle, where you seek to specific locations and read specific amounts. +Pull sources require you to request data from them. The data may be +available synchronously, e.g. if it is held by the operating system's in-memory buffers, or +asynchronously, e.g. if it has to be read from disk. An example pull source is a file handle, where +you seek to specific locations and read specific amounts. -Readable streams are designed to wrap both types of sources behind a single, unified interface. For web -developer–created streams, the implementation details of a source are provided by an -object with certain methods and properties that is passed to the {{ReadableStream()}} constructor. +Readable streams are designed to wrap both types of sources behind a single, unified interface. For +web developer–created streams, the implementation details of a source are provided by an object with certain methods and properties that is passed to +the {{ReadableStream()}} constructor. -Chunks are enqueued into the stream by the stream's underlying source. They can then be read one at a -time via the stream's public interface, in particular by using a readable stream reader acquired using the -stream's {{ReadableStream/getReader()}} method. +[=Chunks=] are enqueued into the stream by the stream's [=underlying source=]. They can then be read +one at a time via the stream's public interface, in particular by using a [=readable stream reader=] +acquired using the stream's {{ReadableStream/getReader()}} method. Code that reads from a readable stream using its public interface is known as a consumer. -Consumers also have the ability to cancel a readable stream, using its -{{ReadableStream/cancel()}} method. This indicates that the consumer has lost interest in the stream, and will -immediately close the stream, throw away any queued chunks, and execute any cancellation mechanism of the -underlying source. +Consumers also have the ability to cancel a readable +stream, using its {{ReadableStream/cancel()}} method. This indicates that the consumer has lost +interest in the stream, and will immediately close the stream, throw away any queued [=chunks=], and +execute any cancellation mechanism of the [=underlying source=]. -Consumers can also tee a readable stream using its {{ReadableStream/tee()}} -method. This will lock the stream, making it no longer directly usable; however, it will -create two new streams, called branches, which can be consumed -independently. +Consumers can also tee a readable stream using its +{{ReadableStream/tee()}} method. This will [=locked to a reader|lock=] the stream, making it +no longer directly usable; however, it will create two new streams, called branches, which can be consumed independently. -For streams representing bytes, an extended version of the readable stream is provided to handle bytes -efficiently, in particular by minimizing copies. The underlying source for such a readable stream is called -an underlying byte source. A readable stream whose underlying source is an underlying byte source is -sometimes called a readable byte stream. Consumers of a readable byte stream can acquire a BYOB reader -using the stream's {{ReadableStream/getReader()}} method. +For streams representing bytes, an extended version of the [=readable stream=] is provided to handle +bytes efficiently, in particular by minimizing copies. The [=underlying source=] for such a readable +stream is called an underlying byte source. A readable stream whose underlying source is +an underlying byte source is sometimes called a readable byte stream. Consumers of a +readable byte stream can acquire a [=BYOB reader=] using the stream's {{ReadableStream/getReader()}} +method.

Writable streams

-A writable stream represents a destination for data, into which you can write. In other words, data -goes in to a writable stream. Concretely, a writable stream is an instance of the {{WritableStream}} class. +A writable stream represents a destination for data, into which you can write. In +other words, data goes in to a writable stream. Concretely, a writable stream is an +instance of the {{WritableStream}} class. Analogously to readable streams, most writable streams wrap a lower-level I/O sink, called the -underlying sink. Writable streams work to abstract away some of the complexity of the underlying sink, by -queuing subsequent writes and only delivering them to the underlying sink one by one. +underlying sink. Writable streams work to abstract away some of the complexity of the +underlying sink, by queuing subsequent writes and only delivering them to the underlying sink one by +one. -Chunks are written to the stream via its public interface, and are passed one at a time to the stream's -underlying sink. For web developer-created streams, the implementation details of the sink are provided by an object with certain methods that is passed to the {{WritableStream()}} constructor. +[=Chunks=] are written to the stream via its public interface, and are passed one at a time to the +stream's [=underlying sink=]. For web developer-created streams, the implementation details of the +sink are provided by an object with certain methods that is +passed to the {{WritableStream()}} constructor. -Code that writes into a writable stream using its public interface is known as a producer. +Code that writes into a writable stream using its public interface is known as a +producer. -Producers also have the ability to abort a writable stream, using its -{{WritableStream/abort()}} method. This indicates that the producer believes something has gone wrong, and that future -writes should be discontinued. It puts the stream in an errored state, even without a signal from the underlying -sink, and it discards all writes in the stream's internal queue. +Producers also have the ability to abort a writable stream, +using its {{WritableStream/abort()}} method. This indicates that the producer believes something has +gone wrong, and that future writes should be discontinued. It puts the stream in an errored state, +even without a signal from the [=underlying sink=], and it discards all writes in the stream's +[=internal queue=].

Transform streams

-A transform stream consists of a pair of streams: a writable stream, known as its writable side, and a readable stream, known as its readable side. In a manner -specific to the transform stream in question, writes to the writable side result in new data being made available for -reading from the readable side. - -Concretely, any object with a writable property and a readable property can serve as a -transform stream. However, the standard {{TransformStream}} class makes it much easier to create such a pair that is -properly entangled. It wraps a transformer, which defines algorithms for the specific transformation to be -performed. For web developer–created streams, the implementation details of a transformer are provided by an object with certain methods and properties that is passed to the {{TransformStream()}} constructor. - -An identity transform stream is a type of transform stream which forwards all chunks written -to its writable side to its readable side, without any changes. This can be useful in a variety of scenarios. By default, the {{TransformStream}} constructor will -create an identity transform stream, when no {{transformer/transform()}} method is present on the transformer -object. +A transform stream consists of a pair of streams: a [=writable stream=], known as +its writable side, and a [=readable stream=], known as its readable +side. In a manner specific to the transform stream in question, writes to the writable side +result in new data being made available for reading from the readable side. + +Concretely, any object with a writable property and a readable property +can serve as a transform stream. However, the standard {{TransformStream}} class makes it much +easier to create such a pair that is properly entangled. It wraps a transformer, which +defines algorithms for the specific transformation to be performed. For web developer–created +streams, the implementation details of a transformer are provided by an +object with certain methods and properties that is passed to the {{TransformStream()}} +constructor. + +An identity transform stream is a type of transform stream which forwards all +[=chunks=] written to its [=writable side=] to its [=readable side=], without any changes. This can +be useful in a variety of scenarios. By default, the +{{TransformStream}} constructor will create an identity transform stream, when no +{{Transformer/transform|transform()}} method is present on the [=transformer=] object. Some examples of potential transform streams include: - +* A GZIP compressor, to which uncompressed bytes are written and from which compressed bytes are + read; +* A video decoder, to which encoded bytes are written and from which uncompressed video frames are + read; +* A text decoder, to which bytes are written and from which strings are read; +* A CSV-to-JSON converter, to which strings representing lines of a CSV file are written and from + which corresponding JavaScript objects are read.

Pipe chains and backpressure

-Streams are primarily used by piping them to each other. A readable stream can be piped directly to a -writable stream, using its {{ReadableStream/pipeTo()}} method, or it can be piped through one or more transform streams -first, using its {{ReadableStream/pipeThrough()}} method. +Streams are primarily used by piping them to each other. A readable stream can be piped +directly to a writable stream, using its {{ReadableStream/pipeTo()}} method, or it can be piped +through one or more transform streams first, using its {{ReadableStream/pipeThrough()}} method. -A set of streams piped together in this way is referred to as a pipe chain. In a pipe chain, the -original source is the underlying source of the first readable stream in the chain; the -ultimate sink is the underlying sink of the final writable stream in the chain. +A set of streams piped together in this way is referred to as a pipe chain. In a pipe +chain, the original source is the [=underlying source=] of the first readable stream in +the chain; the ultimate sink is the [=underlying sink=] of the final writable stream in +the chain. -Once a pipe chain is constructed, it will propagate signals regarding how fast chunks should flow through it. If -any step in the chain cannot yet accept chunks, it propagates a signal backwards through the pipe chain, until -eventually the original source is told to stop producing chunks so fast. This process of normalizing flow from the -original source according to how fast the chain can process chunks is called backpressure. +Once a pipe chain is constructed, it will propagate signals regarding how fast [=chunks=] should +flow through it. If any step in the chain cannot yet accept chunks, it propagates a signal backwards +through the pipe chain, until eventually the original source is told to stop producing chunks so +fast. This process of normalizing flow from the original source according to how fast the chain can +process chunks is called backpressure. -Concretely, the original source is given the +Concretely, the [=original source=] is given the {{ReadableStreamDefaultController/desiredSize|controller.desiredSize}} (or -{{ReadableByteStreamController/desiredSize|byteController.desiredSize}}) value, and can then adjust its rate of data -flow accordingly. This value is derived from the {{WritableStreamDefaultWriter/desiredSize|writer.desiredSize}} -corresponding to the ultimate sink, which gets updated as the ultimate sink finishes writing chunks. The -{{ReadableStream/pipeTo()}} method used to construct the chain automatically ensures this information propagates back -through the pipe chain. - -When teeing a readable stream, the backpressure signals from its two -branches will aggregate, such that if neither branch is read from, a -backpressure signal will be sent to the underlying source of the original stream. - -Piping locks the readable and writable streams, preventing them from being manipulated for the duration of the -pipe operation. This allows the implementation to perform important optimizations, such as directly shuttling data from -the underlying source to the underlying sink while bypassing many of the intermediate queues. +{{ReadableByteStreamController/desiredSize|byteController.desiredSize}}) value, and can then adjust +its rate of data flow accordingly. This value is derived from the +{{WritableStreamDefaultWriter/desiredSize|writer.desiredSize}} corresponding to the [=ultimate +sink=], which gets updated as the ultimate sink finishes writing [=chunks=]. The +{{ReadableStream/pipeTo()}} method used to construct the chain automatically ensures this +information propagates back through the [=pipe chain=]. + +When [=tee a readable stream|teeing=] a readable stream, the [=backpressure=] signals from its two +[=branches of a readable stream tee|branches=] will aggregate, such that if neither branch is read +from, a backpressure signal will be sent to the [=underlying source=] of the original stream. + +Piping [=locks=] the readable and writable streams, preventing them from being manipulated for the +duration of the pipe operation. This allows the implementation to perform important optimizations, +such as directly shuttling data from the underlying source to the underlying sink while bypassing +many of the intermediate queues.

Internal queues and queuing strategies

-Both readable and writable streams maintain internal queues, which they use for similar purposes. In the -case of a readable stream, the internal queue contains chunks that have been enqueued by the underlying -source, but not yet read by the consumer. In the case of a writable stream, the internal queue contains -chunks which have been written to the stream by the producer, but not yet processed and acknowledged by the -underlying sink. - -A queuing strategy is an object that determines how a stream should signal backpressure based on -the state of its internal queue. The queuing strategy assigns a size to each chunk, and compares the -total size of all chunks in the queue to a specified number, known as the high water mark. The resulting -difference, high water mark minus total size, is used to determine the -desired size to fill the stream's queue. - -For readable streams, an underlying source can use this desired size as a backpressure signal, slowing down chunk -generation so as to try to keep the desired size above or at zero. For writable streams, a producer can behave -similarly, avoiding writes that would cause the desired size to go negative. - -Concretely, a queuing strategy for web developer–created streams is given by any JavaScript object -with a highWaterMark property. For byte streams the highWaterMark always has units of bytes. For other streams the default unit is chunks, but a -size() function can be included in the strategy object which returns the size -for a given chunk. This permits the highWaterMark to be specified in -arbitrary floating-point units. +Both readable and writable streams maintain internal queues, which they use for similar +purposes. In the case of a readable stream, the internal queue contains [=chunks=] that have been +enqueued by the [=underlying source=], but not yet read by the consumer. In the case of a writable +stream, the internal queue contains [=chunks=] which have been written to the stream by the +producer, but not yet processed and acknowledged by the [=underlying sink=]. + +A queuing strategy is an object that determines how a stream should signal +[=backpressure=] based on the state of its [=internal queue=]. The queuing strategy assigns a size +to each [=chunk=], and compares the total size of all chunks in the queue to a specified number, +known as the high water mark. The resulting difference, high water mark minus total size, +is used to determine the desired size to +fill the stream's queue. + +For readable streams, an underlying source can use this desired size as a backpressure signal, +slowing down chunk generation so as to try to keep the desired size above or at zero. For writable +streams, a producer can behave similarly, avoiding writes that would cause the desired size to go +negative. + +Concretely, a queuing strategy for web developer–created streams is given by +any JavaScript object with a {{QueuingStrategy/highWaterMark}} property. For byte streams the +{{QueuingStrategy/highWaterMark}} always has units of bytes. For other streams the default unit is +[=chunks=], but a {{QueuingStrategy/size|size()}} function can be included in the strategy object +which returns the size for a given chunk. This permits the {{QueuingStrategy/highWaterMark}} to be +specified in arbitrary floating-point units.
- A simple example of a queuing strategy would be one that assigns a size of one to each chunk, and has a high water - mark of three. This would mean that up to three chunks could be enqueued in a readable stream, or three chunks - written to a writable stream, before the streams are considered to be applying backpressure. - - In JavaScript, such a strategy could be written manually as { highWaterMark: 3, size() { return 1; }}, - or using the built-in {{CountQueuingStrategy}} class, as new CountQueuingStrategy({ highWaterMark: 3 }). + A simple example of a queuing strategy would be one that assigns a size of one to each chunk, and + has a high water mark of three. This would mean that up to three chunks could be enqueued in a + readable stream, or three chunks written to a writable stream, before the streams are considered to + be applying backpressure. + + In JavaScript, such a strategy could be written manually as { highWaterMark: + 3, size() { return 1; }}, or using the built-in {{CountQueuingStrategy}} class, as new CountQueuingStrategy({ highWaterMark: 3 }).

Locking

-A readable stream reader, or simply reader, is an object that allows -direct reading of chunks from a readable stream. Without a reader, a consumer can only perform -high-level operations on the readable stream: canceling the stream, or -piping the readable stream to a writable stream. A reader is acquired via the stream's -{{ReadableStream/getReader()}} method. +A readable stream reader, or simply reader, is an +object that allows direct reading of [=chunks=] from a [=readable stream=]. Without a reader, a +[=consumer=] can only perform high-level operations on the readable stream: [=cancel a readable +stream|canceling=] the stream, or [=piping=] the readable stream to a writable stream. A reader is +acquired via the stream's {{ReadableStream/getReader()}} method. + +A [=readable byte stream=] has the ability to vend two types of readers: default readers +and BYOB readers. BYOB ("bring your own buffer") readers allow reading into a +developer-supplied buffer, thus minimizing copies. A non-byte readable stream can only vend default +readers. Default readers are instances of the {{ReadableStreamDefaultReader}} class, while BYOB +readers are instances of {{ReadableStreamBYOBReader}}. + +Similarly, a writable stream writer, or simply +writer, is an object that allows direct writing of [=chunks=] to a [=writable stream=]. Without a +writer, a [=producer=] can only perform the high-level operations of [=abort a writable +stream|aborting=] the stream or [=piping=] a readable stream to the writable stream. Writers are +represented by the {{WritableStreamDefaultWriter}} class. + +

Under the covers, these high-level operations actually use a reader or writer +themselves.

+ +A given readable or writable stream only has at most one reader or writer at a time. We say in this +case the stream is locked, and that the +reader or writer is active. This state can be +determined using the {{ReadableStream/locked|readableStream.locked}} or +{{WritableStream/locked|writableStream.locked}} properties. + +A reader or writer also has the capability to release its lock, which makes it no longer active, and allows further readers or +writers to be acquired. This is done via the +{{ReadableStreamDefaultReader/releaseLock()|defaultReader.releaseLock()}}, +{{ReadableStreamBYOBReader/releaseLock()|byobReader.releaseLock()}}, or +{{WritableStreamDefaultWriter/releaseLock()|writer.releaseLock()}} method, as appropriate. -A readable byte stream has the ability to vend two types of readers: default readers and BYOB -readers. BYOB ("bring your own buffer") readers allow reading into a developer-supplied buffer, thus minimizing -copies. A non-byte readable stream can only vend default readers. Default readers are instances of the -{{ReadableStreamDefaultReader}} class, while BYOB readers are instances of {{ReadableStreamBYOBReader}}. +

Conventions

-Similarly, a writable stream writer, or simply writer, is an object that -allows direct writing of chunks to a writable stream. Without a writer, a producer can only perform -the high-level operations of aborting the stream or piping a readable stream -to the writable stream. Writers are represented by the {{WritableStreamDefaultWriter}} class. +This specification depends on the Infra Standard. [[!INFRA]] -

Under the covers, these high-level operations actually use a reader or writer themselves.

+This specification uses the [=abstract operation=] concept from [[!ECMASCRIPT]] for its internal +algorithms. This includes treating their return values as [=completion records=], the use of ! and +? prefixes for unwrapping those completion records. -A given readable or writable stream only has at most one reader or writer at a time. We say in this case the stream is -locked, and that the reader or writer is active. This state can be determined using the -{{ReadableStream/locked|readableStream.locked}} or {{WritableStream/locked|writableStream.locked}} properties. +This specification also uses the [=internal slot=] and [=record=] concepts and notation from +ECMAScript. (Although, the internal slots are on Web IDL [=platform objects=] instead of ECMAScript +objects.) -A reader or writer also has the capability to release -its lock, which makes it no longer active, and allows further readers or writers to be acquired. This is done via -the {{ReadableStreamDefaultReader/releaseLock()|defaultReader.releaseLock()}}, -{{ReadableStreamBYOBReader/releaseLock()|byobReader.releaseLock()}}, or -{{WritableStreamDefaultWriter/releaseLock()|writer.releaseLock()}} method, as appropriate. +Finally, as in [[!ECMASCRIPT]], all numbers are represented as double-precision floating point +values, and all arithmetic operations performed on them must be done in the standard way for such +values.

Readable streams

Using readable streams

- The simplest way to consume a readable stream is to simply pipe it to a writable stream. - This ensures that backpressure is respected, and any errors (either writing or reading) are propagated through - the chain: - -

-    readableStream.pipeTo(writableStream)
-      .then(() => console.log("All data successfully written!"))
-      .catch(e => console.error("Something went wrong!", e));
-  
+ The simplest way to consume a readable stream is to simply [=piping|pipe=] it to a [=writable + stream=]. This ensures that [=backpressure=] is respected, and any errors (either writing or + reading) are propagated through the chain: + + + readableStream.pipeTo(writableStream) + .then(() => console.log("All data successfully written!")) + .catch(e => console.error("Something went wrong!", e)); +
- If you simply want to be alerted of each new chunk from a readable stream, you can pipe it to a - new writable stream that you custom-create for that purpose: - -

-    readableStream.pipeTo(new WritableStream({
-      write(chunk) {
-        console.log("Chunk received", chunk);
-      },
-      close() {
-        console.log("All data successfully read!");
-      },
-      abort(e) {
-        console.error("Something went wrong!", e);
-      }
-    }));
-  
- - By returning promises from your {{underlying sink/write()}} implementation, you can signal backpressure to the - readable stream. + If you simply want to be alerted of each new chunk from a readable stream, you can [=piping|pipe=] + it to a new [=writable stream=] that you custom-create for that purpose: + + + readableStream.pipeTo(new WritableStream({ + write(chunk) { + console.log("Chunk received", chunk); + }, + close() { + console.log("All data successfully read!"); + }, + abort(e) { + console.error("Something went wrong!", e); + } + })); + + + By returning promises from your {{UnderlyingSink/write|write()}} implementation, you can signal + [=backpressure=] to the readable stream.
- Although readable streams will usually be used by piping them to a writable stream, you can also read them directly by - acquiring a reader and using its read() method to get successive chunks. For example, this code - logs the next chunk in the stream, if available: - -

-    const reader = readableStream.getReader();
-
-    reader.read().then(
-      ({ value, done }) => {
-        if (done) {
-          console.log("The stream was already closed!");
-        } else {
-          console.log(value);
-        }
-      },
-      e => console.error("The stream became errored and cannot be read from!", e)
-    );
-  
- - This more manual method of reading a stream is mainly useful for library authors building new high-level operations - on streams, beyond the provided ones of piping and teeing. + Although readable streams will usually be used by piping them to a writable stream, you can also + read them directly by acquiring a [=reader=] and using its read() method to get + successive chunks. For example, this code logs the next [=chunk=] in the stream, if available: + + + const reader = readableStream.getReader(); + + reader.read().then( + ({ value, done }) => { + if (done) { + console.log("The stream was already closed!"); + } else { + console.log(value); + } + }, + e => console.error("The stream became errored and cannot be read from!", e) + ); + + + This more manual method of reading a stream is mainly useful for library authors building new + high-level operations on streams, beyond the provided ones of [=piping=] and [=tee a readable + stream|teeing=].
- The above example showed using the readable stream's default reader. If the stream is a readable byte - stream, you can also acquire a BYOB reader for it, which allows more precise control over buffer - allocation in order to avoid copies. For example, this code reads the first 1024 bytes from the stream into a single - memory buffer: - -

-    const reader = readableStream.getReader({ mode: "byob" });
-
-    let startingAB = new ArrayBuffer(1024);
-    readInto(startingAB)
-      .then(buffer => console.log("The first 1024 bytes:", buffer))
-      .catch(e => console.error("Something went wrong!", e));
-
-    function readInto(buffer, offset = 0) {
-      if (offset === buffer.byteLength) {
-        return Promise.resolve(buffer);
-      }
+ The above example showed using the readable stream's [=default reader=]. If the stream is a
+ [=readable byte stream=], you can also acquire a [=BYOB reader=] for it, which allows more
+ precise control over buffer allocation in order to avoid copies. For example, this code reads the
+ first 1024 bytes from the stream into a single memory buffer:
+
+ 
+ const reader = readableStream.getReader({ mode: "byob" });
+
+ let startingAB = new ArrayBuffer(1024);
+ readInto(startingAB)
+   .then(buffer => console.log("The first 1024 bytes:", buffer))
+   .catch(e => console.error("Something went wrong!", e));
+
+ function readInto(buffer, offset = 0) {
+   if (offset === buffer.byteLength) {
+     return Promise.resolve(buffer);
+   }
+
+   const view = new Uint8Array(buffer, offset, buffer.byteLength - offset);
+   return reader.read(view).then(newView => {
+     return readInto(newView.buffer, offset + newView.byteLength);
+   });
+ }
+ 
+
+ An important thing to note here is that the final buffer value is different from the
+ startingAB, but it (and all intermediate buffers) shares the same backing memory
+ allocation. At each step, the buffer is transferred to a new
+ {{ArrayBuffer}} object. The newView is a new {{Uint8Array}}, with that {{ArrayBuffer}}
+ object as its buffer property, the offset that bytes were written to as its
+ byteOffset property, and the number of bytes that were written as its
+ byteLength property.
+
- const view = new Uint8Array(buffer, offset, buffer.byteLength - offset); - return reader.read(view).then(newView => { - return readInto(newView.buffer, offset + newView.byteLength); - }); - } - +

The {{ReadableStream}} class

- An important thing to note here is that the final buffer value is different from the - startingAB, but it (and all intermediate buffers) shares the same backing memory allocation. At each - step, the buffer is transferred to a new {{ArrayBuffer}} object. The - newView is a new {{Uint8Array}}, with that {{ArrayBuffer}} object as its buffer property, - the offset that bytes were written to as its byteOffset property, and the number of bytes that were - written as its byteLength property. - +The {{ReadableStream}} class is a concrete instance of the general [=readable stream=] concept. It +is adaptable to any [=chunk=] type, and maintains an internal queue to keep track of data supplied +by the [=underlying source=] but not yet read by any consumer. -

Class ReadableStream

+

Interface definition

-The {{ReadableStream}} class is a concrete instance of the general readable stream concept. It is -adaptable to any chunk type, and maintains an internal queue to keep track of data supplied by the underlying -source but not yet read by any consumer. +The Web IDL definition for the {{ReadableStream}} class is given as follows: -

Class definition

+ +[Exposed=(Window,Worker,Worklet)] +interface ReadableStream { + constructor(object underlyingSource, optional QueuingStrategy strategy = {}); -<em>This section is non-normative.</em> + readonly attribute boolean locked; -If one were to write the {{ReadableStream}} class in something close to the syntax of [[!ECMASCRIPT]], it would look -like + Promise<void> cancel(optional any reason); + ReadableStreamReader getReader(optional ReadableStreamGetReaderOptions options = {}); + ReadableStream pipeThrough(ReadableWritablePair transform, optional StreamPipeOptions options = {}); + Promise<void> pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); + sequence<ReadableStream> tee(); -<pre><code class="lang-javascript"> - class ReadableStream { - <a href="#rs-constructor">constructor</a>(<a href="#underlying-source-api">underlyingSource</a> = {}, <a href="#qs-api">strategy</a> = {}) + // TODO: async iterator +}; - get <a href="#rs-locked">locked</a>() +typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader; - <a href="#rs-cancel">cancel</a>(reason) - <a href="#rs-get-iterator">getIterator</a>({ preventCancel } = {}) - <a href="#rs-get-reader">getReader</a>({ mode } = {}) - <a href="#rs-pipe-through">pipeThrough</a>({ writable, readable }, - { preventClose, preventAbort, preventCancel, signal } = {}) - <a href="#rs-pipe-to">pipeTo</a>(dest, { preventClose, preventAbort, preventCancel, signal } = {}) - <a href="#rs-tee">tee</a>() +enum ReadableStreamReaderMode { "byob" }; - <a href="#rs-asynciterator">[@@asyncIterator]</a>({ preventCancel } = {}) - } -</code></pre> +dictionary ReadableStreamGetReaderOptions { + ReadableStreamReaderMode mode; +}; + +dictionary ReadableWritablePair { + required ReadableStream readable; + required WritableStream writable; +}; + +dictionary StreamPipeOptions { + boolean preventClose = false; + boolean preventAbort = false; + boolean preventCancel = false; + AbortSignal signal; +}; +

Internal slots

-Instances of {{ReadableStream}} are created with the internal slots described in the following table: +Instances of {{ReadableStream}} are created with the internal slots described in the following +table: - - - - - - + - + - + - + - + - + +
Internal SlotDescription (non-normative)
\[[disturbed]] - A boolean flag set to true when the stream has been read from or - canceled -
Internal Slot + Description (non-normative) +
\[[readableStreamController]] - A {{ReadableStreamDefaultController}} or {{ReadableByteStreamController}} created with - the ability to control the state and queue of this stream; also used for the IsReadableStream brand check -
\[[disturbed]] + A boolean flag set to true when the stream has been read from or + canceled
\[[reader]] - A {{ReadableStreamDefaultReader}} or {{ReadableStreamBYOBReader}} instance, if the stream - is locked to a reader, or undefined if it is not -
\[[readableStreamController]] + A {{ReadableStreamDefaultController}} or + {{ReadableByteStreamController}} created with the ability to control the state and queue of this + stream
\[[state]] - A string containing the stream's current state, used internally; one of - "readable", "closed", or "errored" -
\[[reader]] + A {{ReadableStreamDefaultReader}} or {{ReadableStreamBYOBReader}} + instance, if the stream is [=locked to a reader=], or undefined if it is not
\[[storedError]] - A value indicating how the stream failed, to be given as a failure reason or exception - when trying to operate on an errored stream -
\[[state]] + A string containing the stream's current state, used internally; one + of "readable", "closed", or "errored" +
\[[storedError]] + A value indicating how the stream failed, to be given as a failure + reason or exception when trying to operate on an errored stream
-

new -ReadableStream(underlyingSource = {}, strategy = {})

+

The underlying source API

-
- The underlyingSource argument represents the underlying source, as described in - [[#underlying-source-api]]. - - The strategy argument represents the stream's queuing strategy, as described in [[#qs-api]]. If it - is not provided, the default behavior will be the same as a {{CountQueuingStrategy}} with a high water mark - of 1. -
- - - 1. Perform ! InitializeReadableStream(*this*). - 1. Let _size_ be ? GetV(_strategy_, `"size"`). - 1. Let _highWaterMark_ be ? GetV(_strategy_, `"highWaterMark"`). - 1. Let _type_ be ? GetV(_underlyingSource_, `"type"`). - 1. Let _typeString_ be ? ToString(_type_). - 1. If _typeString_ is `"bytes"`, - 1. If _size_ is not *undefined*, throw a *RangeError* exception. - 1. If _highWaterMark_ is *undefined*, let _highWaterMark_ be *0*. - 1. Set _highWaterMark_ to ? ValidateAndNormalizeHighWaterMark(_highWaterMark_). - 1. Perform ? SetUpReadableByteStreamControllerFromUnderlyingSource(*this*, _underlyingSource_, _highWaterMark_). - 1. Otherwise, if _type_ is *undefined*, - 1. Let _sizeAlgorithm_ be ? MakeSizeAlgorithmFromSizeFunction(_size_). - 1. If _highWaterMark_ is *undefined*, let _highWaterMark_ be *1*. - 1. Set _highWaterMark_ to ? ValidateAndNormalizeHighWaterMark(_highWaterMark_). - 1. Perform ? SetUpReadableStreamDefaultControllerFromUnderlyingSource(*this*, _underlyingSource_, _highWaterMark_, - _sizeAlgorithm_). - 1. Otherwise, throw a *RangeError* exception. - - -

Underlying source API

+The {{ReadableStream()}} constructor accepts as its first argument a JavaScript object representing +the [=underlying source=]. Such objects can contain any of the following properties: -
+ +dictionary UnderlyingSource { + ReadableStreamControllerCallback start; + ReadableStreamControllerCallback pull; + ReadableStreamCancelCallback cancel; + ReadableStreamType type; + [EnforceRange] unsigned long long autoAllocateChunkSize; +}; -<em>This section is non-normative.</em> +typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController; -The {{ReadableStream()}} constructor accepts as its first argument a JavaScript object representing the <a>underlying -source</a>. Such objects can contain any of the following properties: +callback ReadableStreamControllerCallback = Promise<void> (ReadableStreamController controller); +callback ReadableStreamCancelCallback = Promise<void> (optional any reason); + + enum ReadableStreamType { "bytes" }; +
-
start(controller)
-
-

A function that is called immediately during creation of the {{ReadableStream}}.

+
start(controller)
+
+

A function that is called immediately during creation of the {{ReadableStream}}. + +

Typically this is used adapt a [=push source=] by setting up relevant event listeners, as in + the example of [[#example-rs-push-no-backpressure]], or to acquire access to a [=pull source=], + as in [[#example-rs-pull]]. + +

If this setup process is asynchronous, it can return a promise to signal success or failure; + a rejected promise will error the stream. Any thrown exceptions will be re-thrown by the + {{ReadableStream()}} constructor. + +

pull(controller)
+
+

A function that is called whenever the stream's [=internal queue=] of chunks becomes not full, + i.e. whenever the queue's [=desired size to fill a stream's internal queue|desired size=] becomes + positive. Generally, it will be called repeatedly until the queue reaches its [=high water mark=] + (i.e. until the desired size becomes + non-positive). + +

For [=push sources=], this can be used to resume a paused flow, as in + [[#example-rs-push-backpressure]]. For [=pull sources=], it is used to acquire new [=chunks=] to + enqueue into the stream, as in [[#example-rs-pull]]. + +

This function will not be called until {{UnderlyingSource/start|start()}} successfully + completes. Additionally, it will only be called repeatedly if it enqueues at least one chunk or + fulfills a BYOB request; a no-op {{UnderlyingSource/pull|pull()}} implementation will not be + continually called. + +

If the function returns a promise, then it will not be called again until that promise + fulfills. (If the promise rejects, the stream will become errored.) This is mainly used in the + case of pull sources, where the promise returned represents the process of acquiring a new chunk. + Throwing an exception is treated the same as returning a rejected promise. + +

cancel(reason)
+
+

A function that is called whenever the [=consumer=] [=cancel a readable stream|cancels=] the + stream, via {{ReadableStream/cancel()|stream.cancel()}}, + {{ReadableStreamDefaultReader/cancel()|defaultReader.cancel()}}, or + {{ReadableStreamBYOBReader/cancel()|byobReader.cancel()}}. It takes as its argument the same + value as was passed to those methods by the consumer. + +

Readable streams can additionally be canceled under certain conditions during [=piping=]; see + the definition of the {{ReadableStream/pipeTo()}} method for more details. + +

For all streams, this is generally used to release access to the underlying resource; see for + example [[#example-rs-push-no-backpressure]]. + +

If the shutdown process is asynchronous, it can return a promise to signal success or failure; + the result will be communicated via the return value of the cancel() + method that was called. Additionally, a rejected promise will error the stream, instead of + letting it close. Throwing an exception is treated the same as returning a rejected promise. + +

type (byte streams + only)
+
+

Can be set to "bytes" to signal that the + constructed {{ReadableStream}} is a readable byte stream. This ensures that the resulting + {{ReadableStream}} will successfully be able to vend [=BYOB readers=] via its + {{ReadableStream/getReader()}} method. It also affects the |controller| argument passed to the + {{UnderlyingSource/start|start()}} and {{UnderlyingSource/pull|pull()}} methods; see below. + +

For an example of how to set up a readable byte stream, including using the different + controller interface, see [[#example-rbs-push]]. + +

Setting any value other than "{{ReadableStreamType/bytes}}" or undefined will cause the + {{ReadableStream()}} constructor to throw an exception. + +

autoAllocateChunkSize (byte streams only)
+
+

Can be set to a positive integer to cause the implementation to automatically allocate buffers + for the underlying source code to write into. In this case, when a [=consumer=] is using a + [=default reader=], the stream implementation will automatically allocate an {{ArrayBuffer}} of + the given size, so that {{ReadableByteStreamController/byobRequest|controller.byobRequest}} is + always present, as if the consumer was using a [=BYOB reader=]. + +

This is generally used to cut down on the amount of code needed to handle consumers that use + default readers, as can be seen by comparing [[#example-rbs-push]] without auto-allocation to + [[#example-rbs-pull]] with auto-allocation. +

-

Typically this is used adapt a push source by setting up relevant event listeners, as in the example of - [[#example-rs-push-no-backpressure]], or to acquire access to a pull source, as in [[#example-rs-pull]].

+The type of the |controller| argument passed to the {{UnderlyingSource/start|start()}} and +{{UnderlyingSource/pull|pull()}} methods depends on the value of the {{UnderlyingSource/type}} +option. If {{UnderlyingSource/type}} is set to undefined (including via omission), then +|controller| will be a {{ReadableStreamDefaultController}}. If it's set to +"{{ReadableStreamType/bytes}}", then |controller| will be a {{ReadableByteStreamController}}. -

If this setup process is asynchronous, it can return a promise to signal success or failure; a rejected promise - will error the stream. Any thrown exceptions will be re-thrown by the {{ReadableStream()}} constructor.

- +

Constructor, methods, and properties

-
pull(controller)
-
-

A function that is called whenever the stream's internal queue of chunks becomes not full, i.e. whenever - the queue's desired size becomes positive. Generally, it - will be called repeatedly until the queue reaches its high water mark (i.e. until the desired size becomes non-positive).

- -

For push sources, this can be used to resume a paused flow, as in [[#example-rs-push-backpressure]]. For - pull sources, it is used to acquire new chunks to enqueue into the stream, as in - [[#example-rs-pull]].

- -

This function will not be called until {{underlying source/start()}} successfully completes. Additionally, it - will only be called repeatedly if it enqueues at least one chunk or fulfills a BYOB request; a no-op - {{underlying source/pull()}} implementation will not be continually called.

- -

If the function returns a promise, then it will not be called again until that promise fulfills. (If the - promise rejects, the stream will become errored.) This is mainly used in the case of pull sources, where the - promise returned represents the process of acquiring a new chunk. Throwing an exception is treated the same as - returning a rejected promise.

-
- -
cancel(reason)
+
+
stream = new {{ReadableStream/constructor(underlyingSource, strategy)|ReadableStream}}(underlyingSource[, strategy])
-

A function that is called whenever the consumer cancels the stream, - via {{ReadableStream/cancel()|stream.cancel()}}, {{ReadableStreamDefaultReader/cancel()|defaultReader.cancel()}}, or - {{ReadableStreamBYOBReader/cancel()|byobReader.cancel()}}. It takes as its argument the same value as was passed to - those methods by the consumer.

- -

Readable streams can additionally be canceled under certain conditions during piping; see the definition - of the {{ReadableStream/pipeTo()}} method for more details.

+

Creates a new {{ReadableStream}} wrapping the provided [=underlying source=]. See + [[#underlying-source-api]] for more details on the underlyingSource argument. + +

The |strategy| argument represents the stream's [=queuing strategy=], as described in + [[#qs-api]]. If it is not provided, the default behavior will be the same as a + {{CountQueuingStrategy}} with a [=high water mark=] of 1. + +

isLocked = stream.{{ReadableStream/locked}} +
+

Returns whether or not the readable stream is [=locked to a reader=]. + +

await stream.{{ReadableStream/cancel(reason)|cancel}}([ reason ]) +
+

[=cancel a readable stream|Cancels=] the stream, signaling a loss of interest in the stream by + a consumer. The supplied reason argument will be given to the underlying source's + {{UnderlyingSource/cancel|cancel()}} method, which might or might not use it. + +

The returned promise will fulfill if the stream shuts down successfully, or reject if the + underlying source signaled that there was an error doing so. Additionally, it will reject with a + {{TypeError}} (without attempting to cancel the stream) if the stream is currently [=locked to a + reader|locked=]. + +

reader = stream.{{ReadableStream/getReader(options)|getReader}}([{ {{ReadableStreamGetReaderOptions/mode}}: "{{ReadableStreamReaderMode/byob}}" }]) +
+

Creates a reader of the type specified by the {{ReadableStreamGetReaderOptions/mode}} option + and [=locked to a reader|locks=] the stream to the new reader. While the stream is locked, no + other reader can be acquired until this one is [=release a read lock|released=]. + +

This functionality is especially useful for creating abstractions that desire the ability to + consume a stream in its entirety. By getting a reader for the stream, you can ensure nobody else + can interleave reads with yours or cancel the stream, which would interfere with your + abstraction. + +

When {{ReadableStreamGetReaderOptions/mode}} is undefined or not provided, the method creates + a [=default reader=] (an instance of {{ReadableStreamDefaultReader}}). The reader provides the + ability to directly read individual [=chunks=] from the stream via the reader's + {{ReadableStreamDefaultReader/read()}} method. + +

When {{ReadableStreamGetReaderOptions/mode}} is "{{ReadableStreamReaderMode/byob}}", the + method creates a [=BYOB reader=] (an instance of {{ReadableStreamBYOBReader}}). This feature only + works on [=readable byte streams=], i.e. streams which were constructed specifically with the + ability to handle "bring your own buffer" reading. The reader provides the ability to directly + read individual [=chunks=] from the stream via the reader's {{ReadableStreamBYOBReader/read()}} + method, into developer-supplied buffers, allowing more precise control over allocation. -

For all streams, this is generally used to release access to the underlying resource; see for example - [[#example-rs-push-no-backpressure]].

+
readable = stream.{{ReadableStream/pipeThrough(transform, options)|pipeThrough}}({ {{ReadableWritablePair/writable}}, {{ReadableWritablePair/readable}} }[, { {{StreamPipeOptions/preventClose}}, {{StreamPipeOptions/preventCancel}}, {{StreamPipeOptions/preventCancel}}, {{StreamPipeOptions/signal}} }])
+
+

Provides a convenient, chainable way of [=piping=] this [=readable stream=] through a + [=transform stream=] (or any other { writable, readable } pair). It simply pipes the + stream into the writable side of the supplied pair, and returns the readable side for further use. -

If the shutdown process is asynchronous, it can return a promise to signal success or failure; the result will be - communicated via the return value of the cancel() method that was called. Additionally, a rejected - promise will error the stream, instead of letting it close. Throwing an exception is treated the same as returning - a rejected promise.

-
+

Piping a stream will [=locked to a reader|lock=] it for the duration of the pipe, preventing + any other consumer from acquiring a reader. -

type (byte streams only)
-
-

Can be set to "bytes" to signal that the constructed {{ReadableStream}} is a readable byte - stream. This ensures that the resulting {{ReadableStream}} will successfully be able to vend BYOB readers - via its {{ReadableStream/getReader()}} method. It also affects the controller argument passed to the - {{underlying source/start()}} and {{underlying source/pull()}} methods; see below.

+
await stream.{{ReadableStream/pipeTo(destination, options)|pipeTo}}(destination[, { {{StreamPipeOptions/preventClose}}, {{StreamPipeOptions/preventCancel}}, {{StreamPipeOptions/preventCancel}}, {{StreamPipeOptions/signal}} }])
+
+

[=piping|Pipes=] this [=readable stream=] to a given [=writable stream=] |destination|. The + way in which the piping process behaves under various error conditions can be customized with a + number of passed options. It returns a promise that fulfills when the piping process completes + successfully, or rejects if any errors were encountered. -

For an example of how to set up a readable byte stream, including using the different controller interface, see - [[#example-rbs-push]].

+ Piping a stream will [=locked to a reader|lock=] it for the duration of the pipe, preventing any + other consumer from acquiring a reader. -

Setting any value other than "bytes" or undefined will cause the - {{ReadableStream()}} constructor to throw an exception.

-
+ Errors and closures of the source and destination streams propagate as follows: -
autoAllocateChunkSize (byte streams only)
-
-

Can be set to a positive integer to cause the implementation to automatically allocate buffers for the - underlying source code to write into. In this case, when a consumer is using a default reader, the - stream implementation will automatically allocate an {{ArrayBuffer}} of the given size, so that - {{ReadableByteStreamController/byobRequest|controller.byobRequest}} is always present, as if the consumer was using - a BYOB reader.

- -

This is generally used to cut down on the amount of code needed to handle consumers that use default readers, as - can be seen by comparing [[#example-rbs-push]] without auto-allocation to [[#example-rbs-pull]] with - auto-allocation.

-
+ * An error in this source [=readable stream=] will [=abort a writable stream|abort=] + |destination|, unless {{StreamPipeOptions/preventAbort}} is truthy. The returned promise will be + rejected with the source's error, or with any error that occurs during aborting the destination. + + * An error in |destination| will [=cancel a readable stream|cancel=] this source [=readable + stream=], unless {{StreamPipeOptions/preventCancel}} is truthy. The returned promise will be + rejected with the destination's error, or with any error that occurs during canceling the source. + + * When this source [=readable stream=] closes, |destination| will be closed, unless + {{StreamPipeOptions/preventCancel}} is truthy. The returned promise will be fulfilled once this + process completes, unless an error is encountered while closing the destination, in which case + it will be rejected with that error. + + * If |destination| starts out closed or closing, this source [=readable stream=] will be [=cancel + a readable stream|canceled=], unless {{StreamPipeOptions/preventCancel}} is true. The returned + promise will be rejected with an error indicating piping to a closed stream failed, or with any + error that occurs during canceling the source. + +

The {{StreamPipeOptions/signal}} option can be set to an {{AbortSignal}} to allow aborting an + ongoing pipe operation via the corresponding {{AbortController}}. In this case, this source + [=readable stream=] will be [=cancel a readable stream|canceled=], and |destination| [=abort a + writable stream|aborted=], unless the respective options {{StreamPipeOptions/preventCancel}} or + {{StreamPipeOptions/preventAbort}} are set. + +

[branch1, branch2] = stream.{{ReadableStream/tee()|tee}}() +
+

[=tee a readable stream|Tees=] this readable stream, returning a two-element array containing + the two resulting branches as new {{ReadableStream}} instances. + +

Teeing a stream will [=locked to a reader|lock=] it, preventing any other consumer from + acquiring a reader. To [=cancel a readable stream|cancel=] the stream, cancel both of the + resulting branches; a composite cancellation reason will then be propagated to the stream's + [=underlying source=]. + +

Note that the [=chunks=] seen in each branch will be the same object. If the chunks are not + immutable, this could allow interference between the two branches.

-The type of the controller argument passed to the {{underlying source/start()}} and -{{underlying source/pull()}} methods depends on the value of the type -option. If type is set to undefined (including via omission), -controller will be a {{ReadableStreamDefaultController}}. If it's set to "bytes", -controller will be a {{ReadableByteStreamController}}. - +
+ The ReadableStream(|underlyingSource|, + |strategy|) constructor steps are: + + 1. Let |underlyingSourceDict| be |underlyingSource|, [=converted to an IDL value=] of type + {{UnderlyingSource}}. +

We cannot declare the |underlyingSource| argument as having the + {{UnderlyingSource}} type directly, because doing so would lose the reference to the original + object. We need to retain the object so we can [=invoke=] the various methods on it. + 1. Perform ! [$InitializeReadableStream$]([=this=]). + 1. If |underlyingSourceDict|["{{UnderlyingSource/type}}"] is "{{ReadableStreamType/bytes}}": + 1. If |strategy|["{{QueuingStrategy/size}}"] [=map/exists=], throw a {{RangeError}} exception. + 1. Let |highWaterMark| be ? [$ExtractHighWaterMark$](|strategy|, 0). + 1. Perform ? [$SetUpReadableByteStreamControllerFromUnderlyingSource$]([=this=], + |underlyingSource|, |underlyingSourceDict|, |highWaterMark|). + 1. Otherwise, + 1. Assert: |underlyingSourceDict|["{{UnderlyingSource/type}}"] does not [=map/exist=]. + 1. Let |sizeAlgorithm| be ! [$ExtractSizeAlgorithm$](|strategy|). + 1. Let |highWaterMark| be ? [$ExtractHighWaterMark$](|strategy|, 1). + 1. Perform ? [$SetUpReadableStreamDefaultControllerFromUnderlyingSource$]([=this=], + |underlyingSource|, |underlyingSourceDict|, |highWaterMark|, |sizeAlgorithm|).

-

Properties of the {{ReadableStream}} prototype

+
+ The locked attribute's getter steps are: -
get locked
- -
- The locked getter returns whether or not the readable stream is locked to a reader. -
- - - 1. If ! IsReadableStream(*this*) is *false*, throw a *TypeError* exception. - 1. Return ! IsReadableStreamLocked(*this*). - - -
cancel(reason)
- -
- The cancel method cancels the stream, signaling a loss of interest - in the stream by a consumer. The supplied reason argument will be given to the underlying source's - {{underlying source/cancel()}} method, which might or might not use it. + 1. Return ! [$IsReadableStreamLocked$]([=this=]).
- - 1. If ! IsReadableStream(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If ! IsReadableStreamLocked(*this*) is *true*, return a promise rejected with a *TypeError* exception. - 1. Return ! ReadableStreamCancel(*this*, _reason_). - - -
getIterator({ preventCancel } = {})
+
+ The cancel(|reason|) method steps are: -
- The getIterator method returns an async iterator which can be used to consume the stream. The - {{ReadableStreamAsyncIteratorPrototype/return()}} method of this iterator object will, by default, - cancel the stream; it will also release the reader. + 1. If ! [$IsReadableStreamLocked$]([=this=]) is true, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. Return ! [$ReadableStreamCancel$]([=this=], |reason|).
- - 1. If ! IsReadableStream(*this*) is *false*, throw a *TypeError* exception. - 1. Let _reader_ be ? AcquireReadableStreamDefaultReader(*this*). - 1. Let _iterator_ be ! ObjectCreate(`ReadableStreamAsyncIteratorPrototype`). - 1. Set _iterator_.[[asyncIteratorReader]] to _reader_. - 1. Set _iterator_.[[preventCancel]] to ! ToBoolean(_preventCancel_). - 1. Return _iterator_. - - -
getReader({ mode } = {})
- -
- The getReader method creates a reader of the type specified by the mode option and locks the stream to the new reader. While the stream is locked, no other reader can be - acquired until this one is released. - - This functionality is especially useful for creating abstractions that desire the ability to consume a stream in its - entirety. By getting a reader for the stream, you can ensure nobody else can interleave reads with yours or cancel - the stream, which would interfere with your abstraction. - - When mode is undefined, the method creates a default reader (an instance of - {{ReadableStreamDefaultReader}}). The reader provides the ability to directly read individual chunks from the - stream via the reader's {{ReadableStreamDefaultReader/read()}} method. - - When mode is "byob", the getReader method creates a BYOB reader (an - instance of {{ReadableStreamBYOBReader}}). This feature only works on readable byte streams, i.e. streams which - were constructed specifically with the ability to handle "bring your own buffer" reading. The reader provides the - ability to directly read individual chunks from the stream via the reader's {{ReadableStreamBYOBReader/read()}} - method, into developer-supplied buffers, allowing more precise control over allocation. -
+
+ The getReader(|options|) method steps + are: - - 1. If ! IsReadableStream(*this*) is *false*, throw a *TypeError* exception. - 1. If _mode_ is *undefined*, return ? AcquireReadableStreamDefaultReader(*this*, *true*). - 1. Set _mode_ to ? ToString(_mode_). - 1. If _mode_ is `"byob"`, return ? AcquireReadableStreamBYOBReader(*this*, *true*). - 1. Throw a *RangeError* exception. - + 1. If |options|["{{ReadableStreamGetReaderOptions/mode}}"] does not [=map/exist=], return ? + [$AcquireReadableStreamDefaultReader$]([=this=], true). + 1. Assert: |options|["{{ReadableStreamGetReaderOptions/mode}}"] is + "{{ReadableStreamReaderMode/byob}}". + 1. Return ? [$AcquireReadableStreamBYOBReader$]([=this=], true). -
- An example of an abstraction that might benefit from using a reader is a function like the following, which is - designed to read an entire readable stream into memory as an array of chunks. +
+ An example of an abstraction that might benefit from using a reader is a function like the + following, which is designed to read an entire readable stream into memory as an array of + [=chunks=]. -

-    function readAllChunks(readableStream) {
-      const reader = readableStream.getReader();
-      const chunks = [];
+  
+  function readAllChunks(readableStream) {
+    const reader = readableStream.getReader();
+    const chunks = [];
 
-      return pump();
+    return pump();
 
-      function pump() {
-        return reader.read().then(({ value, done }) => {
-          if (done) {
-            return chunks;
-          }
+    function pump() {
+      return reader.read().then(({ value, done }) => {
+        if (done) {
+          return chunks;
+        }
 
-          chunks.push(value);
-          return pump();
-        });
-      }
+        chunks.push(value);
+        return pump();
+      });
     }
-  </code></pre>
-
-  Note how the first thing it does is obtain a reader, and from then on it uses the reader exclusively. This ensures
-  that no other consumer can interfere with the stream, either by reading chunks or by
-  <a lt="cancel a readable stream">canceling</a> the stream.
-</div>
-
-<h5 id="rs-pipe-through" method for="ReadableStream" lt="pipeThrough(transform, options)">pipeThrough({
-<var ignore>writable</var>, <var ignore>readable</var> }, { <var>preventClose</var>, <var>preventAbort</var>,
-<var>preventCancel</var>, <var>signal</var> } = {})</h5>
-
-<div class="note">
-  The <code>pipeThrough</code> method provides a convenient, chainable way of <a>piping</a> this <a>readable stream</a>
-  through a <a>transform stream</a> (or any other <code>{ writable, readable }</code> pair). It simply pipes the stream
-  into the writable side of the supplied pair, and returns the readable side for further use.
-
-  Piping a stream will <a lt="locked to a reader">lock</a> it for the duration of the pipe, preventing any other
-  consumer from acquiring a reader.
-</div>
-
-<emu-alg>
-  1. If ! IsReadableStream(*this*) is *false*, throw a *TypeError* exception.
-  1. If ! IsWritableStream(_writable_) is *false*, throw a *TypeError* exception.
-  1. If ! IsReadableStream(_readable_) is *false*, throw a *TypeError* exception.
-  1. Set _preventClose_ to ! ToBoolean(_preventClose_), set _preventAbort_ to ! ToBoolean(_preventAbort_), and set
-     _preventCancel_ to ! ToBoolean(_preventCancel_).
-  1. If _signal_ is not *undefined*, and _signal_ is not an instance of the `<a idl>AbortSignal</a>` interface, throw a
-     *TypeError* exception.
-  1. If ! IsReadableStreamLocked(*this*) is *true*, throw a *TypeError* exception.
-  1. If ! IsWritableStreamLocked(_writable_) is *true*, throw a *TypeError* exception.
-  1. Let _promise_ be ! ReadableStreamPipeTo(*this*, _writable_, _preventClose_, _preventAbort_, _preventCancel_,
-     _signal_).
-  1. Set _promise_.[[PromiseIsHandled]] to *true*.
-  1. Return _readable_.
-</emu-alg>
-
-<div class="example" id="example-pipe-chain">
-  A typical example of constructing <a>pipe chain</a> using {{ReadableStream/pipeThrough(transform, options)}} would
-  look like
-
-  <pre><code class="lang-javascript">
-    httpResponseBody
-      .pipeThrough(decompressorTransform)
-      .pipeThrough(ignoreNonImageFilesTransform)
-      .pipeTo(mediaGallery);
-  </code></pre>
-</div>
-
-<h5 id="rs-pipe-to" method for="ReadableStream" lt="pipeTo(dest, options)">pipeTo(<var>dest</var>,
-{ <var>preventClose</var>, <var>preventAbort</var>, <var>preventCancel</var>, <var>signal</var> } = {})</h5>
-
-<div class="note">
-  The <code>pipeTo</code> method <a lt="piping">pipes</a> this <a>readable stream</a> to a given <a>writable
-  stream</a>. The way in which the piping process behaves under various error conditions can be customized with a
-  number of passed options. It returns a promise that fulfills when the piping process completes successfully, or
-  rejects if any errors were encountered.
-
-  Piping a stream will <a lt="locked to a reader">lock</a> it for the duration of the pipe, preventing any other
-  consumer from acquiring a reader.
-
-  Errors and closures of the source and destination streams propagate as follows:
-
-  <ul>
-    <li><p>An error in the source <a>readable stream</a> will <a lt="abort a writable stream">abort</a> the destination
-    <a>writable stream</a>, unless <code>preventAbort</code> is truthy. The returned promise will be rejected with the
-    source's error, or with any error that occurs during aborting the destination.</p></li>
-
-    <li><p>An error in the destination <a>writable stream</a> will <a lt="cancel a readable stream">cancel</a> the
-    source <a>readable stream</a>, unless <code>preventCancel</code> is truthy. The returned promise will be rejected
-    with the destination's error, or with any error that occurs during canceling the source.</p></li>
-
-    <li><p>When the source <a>readable stream</a> closes, the destination <a>writable stream</a> will be closed, unless
-    <code>preventClose</code> is true. The returned promise will be fulfilled once this process completes, unless an
-    error is encountered while closing the destination, in which case it will be rejected with that error.</p></li>
-
-    <li><p>If the destination <a>writable stream</a> starts out closed or closing, the source <a>readable stream</a>
-    will be <a lt="cancel a readable stream">canceled</a>, unless <code>preventCancel</code> is true. The returned
-    promise will be rejected with an error indicating piping to a closed stream failed, or with any error that occurs
-    during canceling the source.</p></li>
-  </ul>
-
-  The <code>signal</code> option can be set to an {{AbortSignal}} to allow aborting an ongoing pipe operation via the
-  corresponding {{AbortController}}. In this case, the source <a>readable stream</a> will be <a lt="cancel a readable
-  stream">canceled</a>, and the destination <a>writable stream</a> <a lt="abort a writable stream">aborted</a>, unless
-  the respective options <code>preventCancel</code> or <code>preventAbort</code> are set.
+  }
+  
 
+  Note how the first thing it does is obtain a reader, and from then on it uses the reader
+  exclusively. This ensures that no other consumer can interfere with the stream, either by reading
+  chunks or by [=cancel a readable stream|canceling=] the stream.
+ 
- - 1. If ! IsReadableStream(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If ! IsWritableStream(_dest_) is *false*, return a promise rejected with a *TypeError* exception. - 1. Set _preventClose_ to ! ToBoolean(_preventClose_), set _preventAbort_ to ! ToBoolean(_preventAbort_), and set - _preventCancel_ to ! ToBoolean(_preventCancel_). - 1. If _signal_ is not *undefined*, and _signal_ is not an instance of the `AbortSignal` interface, return - a promise rejected with a *TypeError* exception. - 1. If ! IsReadableStreamLocked(*this*) is *true*, return a promise rejected with a *TypeError* exception. - 1. If ! IsWritableStreamLocked(_dest_) is *true*, return a promise rejected with a *TypeError* exception. - 1. Return ! ReadableStreamPipeTo(*this*, _dest_, _preventClose_, _preventAbort_, _preventCancel_, _signal_). - - -
tee()
- -
- The tee method tees this readable stream, returning a two-element - array containing the two resulting branches as new {{ReadableStream}} instances. - - Teeing a stream will lock it, preventing any other consumer from acquiring a reader. - To cancel the stream, cancel both of the resulting branches; a composite - cancellation reason will then be propagated to the stream's underlying source. - - Note that the chunks seen in each branch will be the same object. If the chunks are not immutable, this could - allow interference between the two branches. +
+ The pipeThrough(|transform|, |options|) + method steps are: + + 1. If ! [$IsReadableStreamLocked$]([=this=]) is true, throw a {{TypeError}} exception. + 1. If ! [$IsWritableStreamLocked$](|transform|["{{ReadableWritablePair/writable}}"]) is true, throw + a {{TypeError}} exception. + 1. Let |signal| be |options|["{{StreamPipeOptions/signal}}"] if it [=map/exists=], or undefined + otherwise. + 1. Let |promise| be ! [$ReadableStreamPipeTo$]([=this=], + |transform|["{{ReadableWritablePair/writable}}"], + |options|["{{StreamPipeOptions/preventClose}}"], + |options|["{{StreamPipeOptions/preventAbort}}"], + |options|["{{StreamPipeOptions/preventCancel}}"], |signal|). + 1. Set |promise|.\[[PromiseIsHandled]] to true. + 1. Return |transform|["{{ReadableWritablePair/readable}}"]. + +
+ A typical example of constructing [=pipe chain=] using {{ReadableStream/pipeThrough(transform, + options)}} would look like + + + httpResponseBody + .pipeThrough(decompressorTransform) + .pipeThrough(ignoreNonImageFilesTransform) + .pipeTo(mediaGallery); + +
- - 1. If ! IsReadableStream(*this*) is *false*, throw a *TypeError* exception. - 1. Let _branches_ be ? ReadableStreamTee(*this*, *false*). - 1. Return ! CreateArrayFromList(_branches_). - - -
- Teeing a stream is most useful when you wish to let two independent consumers read from the stream in parallel, - perhaps even at different speeds. For example, given a writable stream cacheEntry representing an - on-disk file, and another writable stream httpRequestBody representing an upload to a remote server, - you could pipe the same readable stream to both destinations at once: - -

-    const [forLocal, forRemote] = readableStream.tee();
-
-    Promise.all([
-      forLocal.pipeTo(cacheEntry),
-      forRemote.pipeTo(httpRequestBody)
-    ])
-    .then(() => console.log("Saved the stream to the cache and also uploaded it!"))
-    .catch(e => console.error("Either caching or uploading failed: ", e));
-  
+
+ The pipeTo(|destination|, |options|) + method steps are: + + 1. If ! [$IsReadableStreamLocked$]([=this=]) is true, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. If ! [$IsWritableStreamLocked$](|destination|) is true, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. Let |signal| be |options|["{{StreamPipeOptions/signal}}"] if it [=map/exists=], or undefined + otherwise. + 1. Return ! [$ReadableStreamPipeTo$]([=this=], |destination|, + |options|["{{StreamPipeOptions/preventClose}}"], + |options|["{{StreamPipeOptions/preventAbort}}"], + |options|["{{StreamPipeOptions/preventCancel}}"], |signal|).
- -
[@@asyncIterator]({ preventCancel } = {})
- -

- The @@asyncIterator method is an alias of {{ReadableStream/getIterator()}}. -

- -The initial value of the @@asyncIterator method is the same function object as the initial value of the -{{ReadableStream/getIterator()}} method. - -

ReadableStreamAsyncIteratorPrototype

- -{{ReadableStreamAsyncIteratorPrototype}} is an ordinary object that is used by {{ReadableStream/getIterator()}} to -construct the objects it returns. Instances of {{ReadableStreamAsyncIteratorPrototype}} implement the {{AsyncIterator}} -abstract interface from the JavaScript specification. [[!ECMASCRIPT]] - -The {{ReadableStreamAsyncIteratorPrototype}} object must have its \[[Prototype]] internal slot set to -{{%AsyncIteratorPrototype%}}. - -

Internal slots

- -Objects created by {{ReadableStream/getIterator()}}, using {{ReadableStreamAsyncIteratorPrototype}} as their -prototype, are created with the internal slots described in the following table: - - - - - - - - - - - - -
Internal SlotDescription (non-normative)
\[[asyncIteratorReader]] - A {{ReadableStreamDefaultReader}} instance -
\[[preventCancel]] - A boolean value indicating if the stream will be canceled when the async iterator's {{ReadableStreamAsyncIteratorPrototype/return()}} method is called -
- -

next()

- - - 1. If ! IsReadableStreamAsyncIterator(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. Let _reader_ be *this*.[[asyncIteratorReader]]. - 1. If _reader_.[[ownerReadableStream]] is *undefined*, return a promise rejected with a *TypeError* exception. - 1. Return the result of reacting to ! ReadableStreamDefaultReaderRead(_reader_) with the following fulfillment - steps given the argument _result_: - 1. Assert: Type(_result_) is Object. - 1. Let _done_ be ! Get(_result_, `"done"`). - 1. Assert: Type(_done_) is Boolean. - 1. If _done_ is *true*, perform ! ReadableStreamReaderGenericRelease(_reader_). - 1. Let _value_ be ! Get(_result_, `"value"`). - 1. Return ! ReadableStreamCreateReadResult(_value_, _done_, *true*). - - -

return( value )

- - - 1. If ! IsReadableStreamAsyncIterator(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. Let _reader_ be *this*.[[asyncIteratorReader]]. - 1. If _reader_.[[ownerReadableStream]] is *undefined*, return a promise rejected with a *TypeError* exception. - 1. If _reader_.[[readRequests]] is not empty, return a promise rejected with a *TypeError* exception. - 1. If *this*.[[preventCancel]] is *false*, then: - 1. Let _result_ be ! ReadableStreamReaderGenericCancel(_reader_, _value_). - 1. Perform ! ReadableStreamReaderGenericRelease(_reader_). - 1. Return the result of reacting to _result_ with a fulfillment step that returns ! - ReadableStreamCreateReadResult(_value_, *true*, *true*). - 1. Perform ! ReadableStreamReaderGenericRelease(_reader_). - 1. Return a promise resolved with ! ReadableStreamCreateReadResult(_value_, *true*, *true*). - - -

General readable stream abstract operations

- -The following abstract operations, unlike most in this specification, are meant to be generally useful by other -specifications, instead of just being part of the implementation of this spec's classes. - -

AcquireReadableStreamBYOBReader ( stream[, forAuthorCode ] )

- -This abstract operation is meant to be called from other specifications that may wish to acquire a BYOB reader -for a given stream. - - - 1. If _forAuthorCode_ was not passed, set it to *false*. - 1. Let _reader_ be ? Construct(`ReadableStreamBYOBReader`, « _stream_ »). - 1. Set _reader_.[[forAuthorCode]] to _forAuthorCode_. - 1. Return _reader_. - - -

AcquireReadableStreamDefaultReader ( stream[, forAuthorCode ] )

- -This abstract operation is meant to be called from other specifications that may wish to acquire a default -reader for a given stream. - -

Other specifications ought to leave forAuthorCode as its default value of -false, unless they are planning to directly expose the resulting { value, done } object -to authors. See the note regarding ReadableStreamCreateReadResult for more -information.

- - - 1. If _forAuthorCode_ was not passed, set it to *false*. - 1. Let _reader_ be ? Construct(`ReadableStreamDefaultReader`, « _stream_ »). - 1. Set _reader_.[[forAuthorCode]] to _forAuthorCode_. - 1. Return _reader_. - - -

CreateReadableStream ( -startAlgorithm, pullAlgorithm, cancelAlgorithm [, highWaterMark [, -sizeAlgorithm ] ] )

- -This abstract operation is meant to be called from other specifications that wish to create {{ReadableStream}} -instances. The pullAlgorithm and cancelAlgorithm algorithms must return -promises; if supplied, sizeAlgorithm must be an algorithm accepting chunk objects and returning a -number; and if supplied, highWaterMark must be a non-negative, non-NaN number. - -

CreateReadableStream throws an exception if and only if the supplied startAlgorithm -throws.

- - - 1. If _highWaterMark_ was not passed, set it to *1*. - 1. If _sizeAlgorithm_ was not passed, set it to an algorithm that returns *1*. - 1. Assert: ! IsNonNegativeNumber(_highWaterMark_) is *true*. - 1. Let _stream_ be ObjectCreate(the original value of `ReadableStream`'s `prototype` property). - 1. Perform ! InitializeReadableStream(_stream_). - 1. Let _controller_ be ObjectCreate(the original value of `ReadableStreamDefaultController`'s `prototype` - property). - 1. Perform ? SetUpReadableStreamDefaultController(_stream_, _controller_, _startAlgorithm_, _pullAlgorithm_, - _cancelAlgorithm_, _highWaterMark_, _sizeAlgorithm_). - 1. Return _stream_. - - -

CreateReadableByteStream ( -startAlgorithm, pullAlgorithm, cancelAlgorithm [, highWaterMark [, -autoAllocateChunkSize ] ] )

- -This abstract operation is meant to be called from other specifications that wish to create {{ReadableStream}} -instances of type "bytes". The pullAlgorithm and cancelAlgorithm algorithms must return -promises; if supplied, highWaterMark must be a non-negative, non-NaN number, and if supplied, -autoAllocateChunkSize must be a positive integer. - -

CreateReadableByteStream throws an exception if and only if the supplied startAlgorithm -throws.

- - - 1. If _highWaterMark_ was not passed, set it to *0*. - 1. If _autoAllocateChunkSize_ was not passed, set it to *undefined*. - 1. Assert: ! IsNonNegativeNumber(_highWaterMark_) is *true*. - 1. If _autoAllocateChunkSize_ is not *undefined*, - 1. Assert: ! IsInteger(_autoAllocateChunkSize_) is *true*. - 1. Assert: _autoAllocateChunkSize_ is positive. - 1. Let _stream_ be ObjectCreate(the original value of `ReadableStream`'s `prototype` property). - 1. Perform ! InitializeReadableStream(_stream_). - 1. Let _controller_ be ObjectCreate(the original value of `ReadableByteStreamController`'s `prototype` - property). - 1. Perform ? SetUpReadableByteStreamController(_stream_, _controller_, _startAlgorithm_, _pullAlgorithm_, - _cancelAlgorithm_, _highWaterMark_, _autoAllocateChunkSize_). - 1. Return _stream_. - - -

InitializeReadableStream ( -stream )

- - - 1. Set _stream_.[[state]] to `"readable"`. - 1. Set _stream_.[[reader]] and _stream_.[[storedError]] to *undefined*. - 1. Set _stream_.[[disturbed]] to *false*. - - -

IsReadableStream ( x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have a [[readableStreamController]] internal slot, return *false*. - 1. Return *true*. - - -

IsReadableStreamDisturbed ( -stream )

- -This abstract operation is meant to be called from other specifications that may wish to query whether or not a -readable stream has ever been read from or canceled. - - - 1. Assert: ! IsReadableStream(_stream_) is *true*. - 1. Return _stream_.[[disturbed]]. - - -

IsReadableStreamLocked ( -stream )

- -This abstract operation is meant to be called from other specifications that may wish to query whether or not a -readable stream is locked to a reader. - - - 1. Assert: ! IsReadableStream(_stream_) is *true*. - 1. If _stream_.[[reader]] is *undefined*, return *false*. - 1. Return *true*. - - -

IsReadableStreamAsyncIterator ( x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have a [[asyncIteratorReader]] internal slot, return *false*. - 1. Return *true*. - - -

ReadableStreamTee ( stream, -cloneForBranch2 )

- -This abstract operation is meant to be called from other specifications that may wish to tee a given readable stream. - -The second argument, cloneForBranch2, governs whether or not the data from the original stream will be cloned -(using HTML's serializable objects framework) before appearing in the second of the returned branches. This is -useful for scenarios where both branches are to be consumed in such a way that they might otherwise interfere with each -other, such as by transferring their chunks. However, it does introduce a -noticeable asymmetry between the two branches, and limits the possible chunks to serializable ones. [[!HTML]] - -

In this standard ReadableStreamTee is always called with cloneForBranch2 set to -false; other specifications pass true.

- - - 1. Assert: ! IsReadableStream(_stream_) is *true*. - 1. Assert: Type(_cloneForBranch2_) is Boolean. - 1. Let _reader_ be ? AcquireReadableStreamDefaultReader(_stream_). - 1. Let _reading_ be *false*. - 1. Let _canceled1_ be *false*. - 1. Let _canceled2_ be *false*. - 1. Let _reason1_ be *undefined*. - 1. Let _reason2_ be *undefined*. - 1. Let _branch1_ be *undefined*. - 1. Let _branch2_ be *undefined*. - 1. Let _cancelPromise_ be a new promise. - 1. Let _pullAlgorithm_ be the following steps: - 1. If _reading_ is *true*, return a promise resolved with *undefined*. - 1. Set _reading_ to *true*. - 1. Let _readPromise_ be the result of reacting to ! ReadableStreamDefaultReaderRead(_reader_) with the - following fulfillment steps given the argument _result_: - 1. Set _reading_ to *false*. - 1. Assert: Type(_result_) is Object. - 1. Let _done_ be ! Get(_result_, `"done"`). - 1. Assert: Type(_done_) is Boolean. - 1. If _done_ is *true*, - 1. If _canceled1_ is *false*, - 1. Perform ! ReadableStreamDefaultControllerClose(_branch1_.[[readableStreamController]]). - 1. If _canceled2_ is *false*, - 1. Perform ! ReadableStreamDefaultControllerClose(_branch2_.[[readableStreamController]]). - 1. Return. - 1. Let _value_ be ! Get(_result_, `"value"`). - 1. Let _value1_ and _value2_ be _value_. - 1. If _canceled2_ is *false* and _cloneForBranch2_ is *true*, set _value2_ to ? StructuredDeserialize(? StructuredSerialize(_value2_), the current Realm - Record). - 1. If _canceled1_ is *false*, perform ? - ReadableStreamDefaultControllerEnqueue(_branch1_.[[readableStreamController]], _value1_). - 1. If _canceled2_ is *false*, perform ? - ReadableStreamDefaultControllerEnqueue(_branch2_.[[readableStreamController]], _value2_). - 1. Set _readPromise_.[[PromiseIsHandled]] to *true*. - 1. Return a promise resolved with *undefined*. - 1. Let _cancel1Algorithm_ be the following steps, taking a _reason_ argument: - 1. Set _canceled1_ to *true*. - 1. Set _reason1_ to _reason_. - 1. If _canceled2_ is *true*, - 1. Let _compositeReason_ be ! CreateArrayFromList(« _reason1_, _reason2_ »). - 1. Let _cancelResult_ be ! ReadableStreamCancel(_stream_, _compositeReason_). - 1. Resolve _cancelPromise_ with _cancelResult_. - 1. Return _cancelPromise_. - 1. Let _cancel2Algorithm_ be the following steps, taking a _reason_ argument: - 1. Set _canceled2_ to *true*. - 1. Set _reason2_ to _reason_. - 1. If _canceled1_ is *true*, - 1. Let _compositeReason_ be ! CreateArrayFromList(« _reason1_, _reason2_ »). - 1. Let _cancelResult_ be ! ReadableStreamCancel(_stream_, _compositeReason_). - 1. Resolve _cancelPromise_ with _cancelResult_. - 1. Return _cancelPromise_. - 1. Let _startAlgorithm_ be an algorithm that returns *undefined*. - 1. Set _branch1_ to ! CreateReadableStream(_startAlgorithm_, _pullAlgorithm_, _cancel1Algorithm_). - 1. Set _branch2_ to ! CreateReadableStream(_startAlgorithm_, _pullAlgorithm_, _cancel2Algorithm_). - 1. Upon rejection of _reader_.[[closedPromise]] with reason _r_, - 1. Perform ! ReadableStreamDefaultControllerError(_branch1_.[[readableStreamController]], _r_). - 1. Perform ! ReadableStreamDefaultControllerError(_branch2_.[[readableStreamController]], _r_). - 1. Return « _branch1_, _branch2_ ». - - -

ReadableStreamPipeTo ( -source, dest, preventClose, preventAbort, preventCancel, -signal )

- - - 1. Assert: ! IsReadableStream(_source_) is *true*. - 1. Assert: ! IsWritableStream(_dest_) is *true*. - 1. Assert: Type(_preventClose_) is Boolean, Type(_preventAbort_) is Boolean, and Type(_preventCancel_) is Boolean. - 1. Assert: _signal_ is *undefined* or _signal_ is an instance of the `AbortSignal` interface. - 1. Assert: ! IsReadableStreamLocked(_source_) is *false*. - 1. Assert: ! IsWritableStreamLocked(_dest_) is *false*. - 1. If ! IsReadableByteStreamController(_source_.[[readableStreamController]]) is *true*, let _reader_ be either ! - AcquireReadableStreamBYOBReader(_source_) or ! AcquireReadableStreamDefaultReader(_source_), at the user agent's - discretion. - 1. Otherwise, let _reader_ be ! AcquireReadableStreamDefaultReader(_source_). - 1. Let _writer_ be ! AcquireWritableStreamDefaultWriter(_dest_). - 1. Set _source_.[[disturbed]] to *true*. - 1. Let _shuttingDown_ be *false*. - 1. Let _promise_ be a new promise. - 1. If _signal_ is not *undefined*, - 1. Let _abortAlgorithm_ be the following steps: - 1. Let _error_ be a new "`AbortError`" `DOMException`. - 1. Let _actions_ be an empty ordered set. - 1. If _preventAbort_ is *false*, append the following action to _actions_: - 1. If _dest_.[[state]] is `"writable"`, return ! WritableStreamAbort(_dest_, _error_). - 1. Otherwise, return a promise resolved with *undefined*. - 1. If _preventCancel_ is *false*, append the following action action to _actions_: - 1. If _source_.[[state]] is `"readable"`, return ! ReadableStreamCancel(_source_, _error_). - 1. Otherwise, return a promise resolved with *undefined*. - 1. Shutdown with an action consisting of - getting a promise to wait for all of the actions in _actions_, and with _error_. - 1. If _signal_'s aborted flag is set, perform _abortAlgorithm_ and return _promise_. - 1. Add _abortAlgorithm_ to _signal_. - 1. In parallel but not really; see #905, using _reader_ and _writer_, read all - chunks from _source_ and write them to _dest_. Due to the locking provided by the reader and writer, the exact - manner in which this happens is not observable to author code, and so there is flexibility in how this is done. The - following constraints apply regardless of the exact algorithm used: - * Public API must not be used: while reading or writing, or performing any of the operations - below, the JavaScript-modifiable reader, writer, and stream APIs (i.e. methods on the appropriate prototypes) - must not be used. Instead, the streams must be manipulated directly. - * Backpressure must be enforced: - * While WritableStreamDefaultWriterGetDesiredSize(_writer_) is ≤ *0* or is *null*, the user agent must not read - from _reader_. - * If _reader_ is a BYOB reader, WritableStreamDefaultWriterGetDesiredSize(_writer_) should be used as a - basis to determine the size of the chunks read from _reader_. -

It's frequently inefficient to read chunks that are too small or too large. Other information - might be factored in to determine the optimal chunk size.

- * Reads or writes should not be delayed for reasons other than these backpressure signals. -

An implementation that waits for each write to successfully - complete before proceeding to the next read/write operation violates this recommendation. In doing so, such an - implementation makes the internal queue of _dest_ useless, as it ensures _dest_ always contains at most - one queued chunk.

- * Shutdown must stop activity: if _shuttingDown_ becomes *true*, the user agent must not - initiate further reads from _reader_, and must only perform writes of already-read chunks, as described - below. In particular, the user agent must check the below conditions before performing any reads or writes, - since they might lead to immediate shutdown. - * Error and close states must be propagated: the following conditions must be applied in order. - 1. Errors must be propagated forward: if _source_.[[state]] is or becomes `"errored"`, then - 1. If _preventAbort_ is *false*, shutdown with an action of ! - WritableStreamAbort(_dest_, _source_.[[storedError]]) and with _source_.[[storedError]]. - 1. Otherwise, shutdown with _source_.[[storedError]]. - 1. Errors must be propagated backward: if _dest_.[[state]] is or becomes `"errored"`, then - 1. If _preventCancel_ is *false*, shutdown with an action of ! - ReadableStreamCancel(_source_, _dest_.[[storedError]]) and with _dest_.[[storedError]]. - 1. Otherwise, shutdown with _dest_.[[storedError]]. - 1. Closing must be propagated forward: if _source_.[[state]] is or becomes `"closed"`, then - 1. If _preventClose_ is *false*, shutdown with an action of ! - WritableStreamDefaultWriterCloseWithErrorPropagation(_writer_). - 1. Otherwise, shutdown. - 1. Closing must be propagated backward: if ! WritableStreamCloseQueuedOrInFlight(_dest_) is *true* - or _dest_.[[state]] is `"closed"`, then - 1. Assert: no chunks have been read or written. - 1. Let _destClosed_ be a new *TypeError*. - 1. If _preventCancel_ is *false*, shutdown with an action of ! - ReadableStreamCancel(_source_, _destClosed_) and with _destClosed_. - 1. Otherwise, shutdown with _destClosed_. - * Shutdown with an action: if any of the above requirements ask to - shutdown with an action _action_, optionally with an error _originalError_, then: - 1. If _shuttingDown_ is *true*, abort these substeps. - 1. Set _shuttingDown_ to *true*. - 1. If _dest_.[[state]] is `"writable"` and ! WritableStreamCloseQueuedOrInFlight(_dest_) is *false*, - 1. If any chunks have been read but not yet written, write them to _dest_. - 1. Wait until every chunk that has been read has been written (i.e. the corresponding promises have - settled). - 1. Let _p_ be the result of performing _action_. - 1. Upon fulfillment of _p_, finalize, passing along _originalError_ if - it was given. - 1. Upon rejection of _p_ with reason _newError_, finalize with - _newError_. - * Shutdown: if any of the above requirements or steps ask to shutdown, optionally - with an error _error_, then: - 1. If _shuttingDown_ is *true*, abort these substeps. - 1. Set _shuttingDown_ to *true*. - 1. If _dest_.[[state]] is `"writable"` and ! WritableStreamCloseQueuedOrInFlight(_dest_) is *false*, - 1. If any chunks have been read but not yet written, write them to _dest_. - 1. Wait until every chunk that has been read has been written (i.e. the corresponding promises have - settled). - 1. Finalize, passing along _error_ if it was given. - * Finalize: both forms of shutdown will eventually ask to finalize, optionally with - an error _error_, which means to perform the following steps: - 1. Perform ! WritableStreamDefaultWriterRelease(_writer_). - 1. Perform ! ReadableStreamReaderGenericRelease(_reader_). - 1. If _signal_ is not *undefined*, remove _abortAlgorithm_ from _signal_. - 1. If _error_ was given, reject _promise_ with _error_. - 1. Otherwise, resolve _promise_ with *undefined*. - 1. Return _promise_. -
- -

- Various abstract operations performed here include object creation (often of promises), which usually would require - specifying a realm for the created object. However, because of the locking, none of these objects can be observed by - author code. As such, the realm used to create them does not matter. -

- -

The interface between readable streams and controllers

- -In terms of specification factoring, the way that the {{ReadableStream}} class encapsulates the behavior of -both simple readable streams and readable byte streams into a single class is by centralizing most of the -potentially-varying logic inside the two controller classes, {{ReadableStreamDefaultController}} and -{{ReadableByteStreamController}}. Those classes define most of the stateful internal slots and abstract -operations for how a stream's internal queue is managed and how it interfaces with its underlying source -or underlying byte source. - -Each controller class defines two internal methods, which are called by the {{ReadableStream}} algorithms: - -
-
\[[CancelSteps]](reason)
-
The controller's steps that run in reaction to the stream being canceled, - used to clean up the state stored in the controller and inform the underlying source.
- -
\[[PullSteps]]()
-
The controller's steps that run when a default reader is read from, used to pull from the controller any - queued chunks, or pull from the underlying source to get more chunks.
-
- -(These are defined as internal methods, instead of as abstract operations, so that they can be called polymorphically by -the {{ReadableStream}} algorithms, without having to branch on which type of controller is present.) - -The rest of this section concerns abstract operations that go in the other direction: they are used by the controller -implementations to affect their associated {{ReadableStream}} object. This translates internal state changes of the -controller into developer-facing results visible through the {{ReadableStream}}'s public API. - -

ReadableStreamAddReadIntoRequest ( stream )

- - - 1. Assert: ! IsReadableStreamBYOBReader(_stream_.[[reader]]) is *true*. - 1. Assert: _stream_.[[state]] is `"readable"` or `"closed"`. - 1. Let _promise_ be a new promise. - 1. Let _readIntoRequest_ be Record {[[promise]]: _promise_}. - 1. Append _readIntoRequest_ as the last element of _stream_.[[reader]].[[readIntoRequests]]. - 1. Return _promise_. - - -

ReadableStreamAddReadRequest ( -stream )

- - - 1. Assert: ! IsReadableStreamDefaultReader(_stream_.[[reader]]) is *true*. - 1. Assert: _stream_.[[state]] is `"readable"`. - 1. Let _promise_ be a new promise. - 1. Let _readRequest_ be Record {[[promise]]: _promise_}. - 1. Append _readRequest_ as the last element of _stream_.[[reader]].[[readRequests]]. - 1. Return _promise_. - - -

ReadableStreamCancel ( stream, -reason )

- - - 1. Set _stream_.[[disturbed]] to *true*. - 1. If _stream_.[[state]] is `"closed"`, return a promise resolved with *undefined*. - 1. If _stream_.[[state]] is `"errored"`, return a promise rejected with _stream_.[[storedError]]. - 1. Perform ! ReadableStreamClose(_stream_). - 1. Let _sourceCancelPromise_ be ! _stream_.[[readableStreamController]].[[CancelSteps]](_reason_). - 1. Return the result of reacting to _sourceCancelPromise_ with a fulfillment step that returns *undefined*. - - -

ReadableStreamClose ( stream )

- - - 1. Assert: _stream_.[[state]] is `"readable"`. - 1. Set _stream_.[[state]] to `"closed"`. - 1. Let _reader_ be _stream_.[[reader]]. - 1. If _reader_ is *undefined*, return. - 1. If ! IsReadableStreamDefaultReader(_reader_) is *true*, - 1. Repeat for each _readRequest_ that is an element of _reader_.[[readRequests]], - 1. Resolve _readRequest_.[[promise]] with ! ReadableStreamCreateReadResult(*undefined*, *true*, - _reader_.[[forAuthorCode]]). - 1. Set _reader_.[[readRequests]] to an empty List. - 1. Resolve _reader_.[[closedPromise]] with *undefined*. - - -
- The case where stream.\[[state]] is "closed", but stream.\[[closeRequested]] is - false, will happen if the stream was closed without its controller's close method ever being - called: i.e., if the stream was closed by a call to {{ReadableStream/cancel(reason)}}. In this case we allow the - controller's close method to be called and silently do nothing, since the cancelation was outside the - control of the underlying source. +
+ The tee() method steps are: + + 1. Return ? [$ReadableStreamTee$]([=this=], false). + +
+ Teeing a stream is most useful when you wish to let two independent consumers read from the stream + in parallel, perhaps even at different speeds. For example, given a writable stream + cacheEntry representing an on-disk file, and another writable stream + httpRequestBody representing an upload to a remote server, you could pipe the same + readable stream to both destinations at once: + + + const [forLocal, forRemote] = readableStream.tee(); + + Promise.all([ + forLocal.pipeTo(cacheEntry), + forRemote.pipeTo(httpRequestBody) + ]) + .then(() => console.log("Saved the stream to the cache and also uploaded it!")) + .catch(e => console.error("Either caching or uploading failed: ", e)); + +
-

ReadableStreamCreateReadResult -( value, done, forAuthorCode )

+ -
- When forAuthorCode is true, this abstract operation gives the same result - as CreateIterResultObject(value, done). This provides the expected semantics - when the object is to be returned from the {{ReadableStreamDefaultReader/read()|defaultReader.read()}} or - {{ReadableStreamBYOBReader/read()|byobReader.read()}} methods. +

The {{ReadableStreamDefaultReader}} class

- However, resolving promises with such objects will unavoidably result in an access to - Object.prototype.then. For internal use, particularly in {{ReadableStream/pipeTo()}} and in other - specifications, it is important that reads not be observable by author code—even if that author code has tampered with - Object.prototype. For this reason, a false value of forAuthorCode results - in an object with a null prototype, keeping promise resolution unobservable. - - The underlying issue here is that reading from streams always uses promises for { value, done } objects, - even in specifications. Although it is conceivable we could rephrase all of the internal algorithms to not use - promises and not use JavaScript objects, and instead only package up the results into promise-for-{ value, done - } when a read() method is called, this would be a large undertaking, which we have not done. See - whatwg/infra#181 for more background on this subject. -
- - - 1. Let _prototype_ be *null*. - 1. If _forAuthorCode_ is *true*, set _prototype_ to %ObjectPrototype%. - 1. Assert: Type(_done_) is Boolean. - 1. Let _obj_ be ObjectCreate(_prototype_). - 1. Perform CreateDataProperty(_obj_, `"value"`, _value_). - 1. Perform CreateDataProperty(_obj_, `"done"`, _done_). - 1. Return _obj_. - - -

ReadableStreamError ( stream, e -)

- - - 1. Assert: ! IsReadableStream(_stream_) is *true*. - 1. Assert: _stream_.[[state]] is `"readable"`. - 1. Set _stream_.[[state]] to `"errored"`. - 1. Set _stream_.[[storedError]] to _e_. - 1. Let _reader_ be _stream_.[[reader]]. - 1. If _reader_ is *undefined*, return. - 1. If ! IsReadableStreamDefaultReader(_reader_) is *true*, - 1. Repeat for each _readRequest_ that is an element of _reader_.[[readRequests]], - 1. Reject _readRequest_.[[promise]] with _e_. - 1. Set _reader_.[[readRequests]] to a new empty List. - 1. Otherwise, - 1. Assert: ! IsReadableStreamBYOBReader(_reader_). - 1. Repeat for each _readIntoRequest_ that is an element of _reader_.[[readIntoRequests]], - 1. Reject _readIntoRequest_.[[promise]] with _e_. - 1. Set _reader_.[[readIntoRequests]] to a new empty List. - 1. Reject _reader_.[[closedPromise]] with _e_. - 1. Set _reader_.[[closedPromise]].[[PromiseIsHandled]] to *true*. - - -

ReadableStreamFulfillReadIntoRequest ( stream, chunk, done )

- - - 1. Let _reader_ be _stream_.[[reader]]. - 1. Let _readIntoRequest_ be the first element of _reader_.[[readIntoRequests]]. - 1. Remove _readIntoRequest_ from _reader_.[[readIntoRequests]], shifting all other elements downward (so that the - second becomes the first, and so on). - 1. Resolve _readIntoRequest_.[[promise]] with ! ReadableStreamCreateReadResult(_chunk_, _done_, - _reader_.[[forAuthorCode]]). - - -

ReadableStreamFulfillReadRequest ( stream, chunk, done )

- - - 1. Let _reader_ be _stream_.[[reader]]. - 1. Let _readRequest_ be the first element of _reader_.[[readRequests]]. - 1. Remove _readRequest_ from _reader_.[[readRequests]], shifting all other elements downward (so that the second - becomes the first, and so on). - 1. Resolve _readRequest_.[[promise]] with ! ReadableStreamCreateReadResult(_chunk_, _done_, - _reader_.[[forAuthorCode]]). - - -

ReadableStreamGetNumReadIntoRequests ( stream )

- - - 1. Return the number of elements in _stream_.[[reader]].[[readIntoRequests]]. - - -

ReadableStreamGetNumReadRequests ( stream )

- - - 1. Return the number of elements in _stream_.[[reader]].[[readRequests]]. - - -

ReadableStreamHasBYOBReader ( -stream )

- - - 1. Let _reader_ be _stream_.[[reader]]. - 1. If _reader_ is *undefined*, return *false*. - 1. If ! IsReadableStreamBYOBReader(_reader_) is *false*, return *false*. - 1. Return *true*. - - -

ReadableStreamHasDefaultReader ( -stream )

- - - 1. Let _reader_ be _stream_.[[reader]]. - 1. If _reader_ is *undefined*, return *false*. - 1. If ! IsReadableStreamDefaultReader(_reader_) is *false*, return *false*. - 1. Return *true*. - - -

Class -ReadableStreamDefaultReader

- -The {{ReadableStreamDefaultReader}} class represents a default reader designed to be vended by a +The {{ReadableStreamDefaultReader}} class represents a [=default reader=] designed to be vended by a {{ReadableStream}} instance. -

Class definition

- -
- -This section is non-normative. - -If one were to write the {{ReadableStreamDefaultReader}} class in something close to the syntax of [[!ECMASCRIPT]], it -would look like +

Interface definition

-

-  class ReadableStreamDefaultReader {
-    constructor(stream)
+The Web IDL definition for the {{ReadableStreamDefaultReader}} class is given as follows:
 
-    get closed()
+
+[Exposed=(Window,Worker,Worklet)]
+interface ReadableStreamDefaultReader {
+  constructor(ReadableStream stream);
 
-    <a href="#default-reader-cancel">cancel</a>(reason)
-    <a href="#default-reader-read">read</a>()
-    <a href="#default-reader-release-lock">releaseLock</a>()
-  }
-</code></pre>
+  readonly attribute Promise<void> closed;
 
-</div>
+  Promise<void> cancel(optional any reason);
+  Promise<any> read();
+  void releaseLock();
+};
+
 
 

Internal slots

-Instances of {{ReadableStreamDefaultReader}} are created with the internal slots described in the following table: +Instances of {{ReadableStreamDefaultReader}} are created with the internal slots described in the +following table: - - - - - - + - + - + - + - + +
Internal SlotDescription (non-normative)
\[[closedPromise]] - A promise returned by the reader's {{ReadableStreamDefaultReader/closed}} getter -
Internal Slot + Description (non-normative) +
\[[forAuthorCode]] - A boolean flag indicating whether this reader is visible to author code -
\[[closedPromise]] + A promise returned by the reader's + {{ReadableStreamDefaultReader/closed}} getter
\[[ownerReadableStream]] - A {{ReadableStream}} instance that owns this reader -
\[[forAuthorCode]] + A boolean flag indicating whether this reader is visible to author code
\[[readRequests]] - A List of promises returned by calls to the reader's - {{ReadableStreamDefaultReader/read()}} method that have not yet been resolved, due to the consumer - requesting chunks sooner than they are available; also used for the IsReadableStreamDefaultReader brand check -
\[[ownerReadableStream]] + A {{ReadableStream}} instance that owns this reader +
\[[readRequests]] + A [=list=] of promises returned by calls to the reader's + {{ReadableStreamDefaultReader/read()}} method that have not yet been resolved, due to the + [=consumer=] requesting [=chunks=] sooner than they are available
-

new ReadableStreamDefaultReader(stream)

+

Constructor, methods, and properties

-
- The ReadableStreamDefaultReader constructor is generally not meant to be used directly; instead, a - stream's {{ReadableStream/getReader()}} method ought to be used. -
+
+
reader = new {{ReadableStreamDefaultReader(stream)|ReadableStreamDefaultReader}}(|stream|) +
+

This is equivalent to calling |stream|.{{ReadableStream/getReader()}}. - - 1. If ! IsReadableStream(_stream_) is *false*, throw a *TypeError* exception. - 1. If ! IsReadableStreamLocked(_stream_) is *true*, throw a *TypeError* exception. - 1. Perform ! ReadableStreamReaderGenericInitialize(*this*, _stream_). - 1. Set *this*.[[readRequests]] to a new empty List. - +

await reader.{{ReadableStreamDefaultReader/closed}} +
+

Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the + stream ever errors or the reader's lock is [=release a read lock|released=] before the stream + finishes closing. -

Properties of the {{ReadableStreamDefaultReader}} prototype

+
await reader.{{ReadableStreamDefaultReader/cancel(reason)|cancel}}([ reason ]) +
+

If the reader is [=active reader|active=], behaves the same as + |stream|.{{ReadableStream/cancel(reason)|cancel}}(reason). -

get closed
+
{ value, done } = await reader.{{ReadableStreamDefaultReader/read()|read}}() +
+

Returns a promise that allows access to the next [=chunk=] from the stream's internal queue, if + available. -

- The closed getter returns a promise that will be fulfilled when the stream becomes closed, or rejected if - the stream ever errors or the reader's lock is released before the stream finishes - closing. -
+
    +
  • If the chunk does become available, the promise will be fulfilled with an object of the form + { value: theChunk, done: false }. - - 1. If ! IsReadableStreamDefaultReader(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. Return *this*.[[closedPromise]]. - +
  • If the stream becomes closed, the promise will be fulfilled with an object of the form + { value: undefined, done: true }. -
    cancel(reason)
    +
  • If the stream becomes errored, the promise will be rejected with the relevant error. +
-
- If the reader is active, the cancel method behaves the same as that for the - associated stream. -
+

If reading a chunk causes the queue to become empty, more data will be pulled from the + [=underlying source=]. - - 1. If ! IsReadableStreamDefaultReader(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If *this*.[[ownerReadableStream]] is *undefined*, return a promise rejected with a *TypeError* exception. - 1. Return ! ReadableStreamReaderGenericCancel(*this*, _reason_). - +

reader.{{ReadableStreamDefaultReader/releaseLock()|releaseLock}}() +
+

[=release a read lock|Releases the reader's lock=] on the corresponding stream. After the lock + is released, the reader is no longer [=active reader|active=]. If the associated stream is errored + when the lock is released, the reader will appear errored in the same way from now on; otherwise, + the reader will appear closed. -

read()
+

A reader's lock cannot be released while it still has a pending read request, i.e., if a + promise returned by the reader's {{ReadableStreamDefaultReader/read()}} method has not yet been + settled. Attempting to do so will throw a {{TypeError}} and leave the reader locked to the stream. +

-
- The read method will return a promise that allows access to the next chunk from the stream's - internal queue, if available. - -
    -
  • If the chunk does become available, the promise will be fulfilled with an object of the form - { value: theChunk, done: false }. -
  • If the stream becomes closed, the promise will be fulfilled with an object of the form - { value: undefined, done: true }. -
  • If the stream becomes errored, the promise will be rejected with the relevant error. -
+
+ The ReadableStreamDefaultReader(|stream|) constructor steps + are: - If reading a chunk causes the queue to become empty, more data will be pulled from the underlying source. + 1. Perform ? [$SetUpReadableStreamDefaultReader$]([=this=], |stream|).
- - 1. If ! IsReadableStreamDefaultReader(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If *this*.[[ownerReadableStream]] is *undefined*, return a promise rejected with a *TypeError* exception. - 1. Return ! ReadableStreamDefaultReaderRead(*this*). - +
+ The closed + getter steps are: -
releaseLock()
+ 1. Return [=this=].\[[closedPromise]]. +
-
- The releaseLock method releases the reader's lock on the corresponding - stream. After the lock is released, the reader is no longer active. If the associated - stream is errored when the lock is released, the reader will appear errored in the same way from now on; otherwise, - the reader will appear closed. +
+ The cancel(|reason|) + method steps are: - A reader's lock cannot be released while it still has a pending read request, i.e., if a promise returned by the - reader's {{ReadableStreamDefaultReader/read()}} method has not yet been settled. Attempting to do so will throw - a TypeError and leave the reader locked to the stream. + 1. If [=this=].\[[ownerReadableStream]] is undefined, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. Return ! [$ReadableStreamReaderGenericCancel$]([=this=], |reason|).
- - 1. If ! IsReadableStreamDefaultReader(*this*) is *false*, throw a *TypeError* exception. - 1. If *this*.[[ownerReadableStream]] is *undefined*, return. - 1. If *this*.[[readRequests]] is not empty, throw a *TypeError* exception. - 1. Perform ! ReadableStreamReaderGenericRelease(*this*). - +
+ The read() + method steps are: -

Class ReadableStreamBYOBReader

+ 1. If [=this=].\[[ownerReadableStream]] is undefined, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. Return ! [$ReadableStreamDefaultReaderRead$]([=this=]). +
-The {{ReadableStreamBYOBReader}} class represents a BYOB reader designed to be vended by a {{ReadableStream}} -instance. +
+ The releaseLock() method steps are: -

Class definition

+ 1. If [=this=].\[[ownerReadableStream]] is undefined, return. + 1. If [=this=].\[[readRequests]] is not [=list/is empty|empty=], throw a {{TypeError}} exception. + 1. Perform ! [$ReadableStreamReaderGenericRelease$]([=this=]). +
-
+

The {{ReadableStreamBYOBReader}} class

-This section is non-normative. +The {{ReadableStreamDefaultReader}} class represents a [=BYOB reader=] designed to be vended by a +{{ReadableStream}} instance. -If one were to write the {{ReadableStreamBYOBReader}} class in something close to the syntax of [[!ECMASCRIPT]], it -would look like +

Interface definition

-

-  class ReadableStreamBYOBReader {
-    constructor(stream)
+The Web IDL definition for the {{ReadableStreamBYOBReader}} class is given as follows:
 
-    get closed()
+
+[Exposed=(Window,Worker,Worklet)]
+interface ReadableStreamBYOBReader {
+  constructor(ReadableStream stream);
 
-    <a href="#byob-reader-cancel">cancel</a>(reason)
-    <a href="#byob-reader-read">read</a>(view)
-    <a href="#byob-reader-release-lock">releaseLock</a>()
-  }
-</code></pre>
+  readonly attribute Promise<void> closed;
 
-</div>
+  Promise<void> cancel(optional any reason);
+  Promise<any> read(ArrayBufferView view);
+  void releaseLock();
+};
+
 
 

Internal slots

-Instances of {{ReadableStreamBYOBReader}} are created with the internal slots described in the following table: +Instances of {{ReadableStreamBYOBReader}} are created with the internal slots described in the +following table: - - - - - - + + + - + - + - + - +
Internal SlotDescription (non-normative)
Internal Slot + Description (non-normative) +
\[[closedPromise]] - A promise returned by the reader's {{ReadableStreamBYOBReader/closed}} getter -
\[[closedPromise]] + A promise returned by the reader's + {{ReadableStreamBYOBReader/closed}} getter
\[[forAuthorCode]] - A boolean flag indicating whether this reader is visible to author code -
\[[forAuthorCode]] + A boolean flag indicating whether this reader is visible to author code
\[[ownerReadableStream]] - A {{ReadableStream}} instance that owns this reader -
\[[ownerReadableStream]] + A {{ReadableStream}} instance that owns this reader
\[[readIntoRequests]] - A List of promises returned by calls to the reader's - {{ReadableStreamBYOBReader/read(view)}} method that have not yet been resolved, due to the consumer - requesting chunks sooner than they are available; also used for the IsReadableStreamBYOBReader brand check -
\[[readIntoRequests]] + A [=list=] of promises returned by calls to the reader's + {{ReadableStreamBYOBReader/read(view)}} method that have not yet been resolved, due to the + [=consumer=] requesting [=chunks=] sooner than they are available
-

new -ReadableStreamBYOBReader(stream)

- -
- The ReadableStreamBYOBReader constructor is generally not meant to be used directly; instead, a stream's - {{ReadableStream/getReader()}} method ought to be used. -
- - - 1. If ! IsReadableStream(_stream_) is *false*, throw a *TypeError* exception. - 1. If ! IsReadableByteStreamController(_stream_.[[readableStreamController]]) is *false*, throw a *TypeError* exception. - 1. If ! IsReadableStreamLocked(_stream_) is *true*, throw a *TypeError* exception. - 1. Perform ! ReadableStreamReaderGenericInitialize(*this*, _stream_). - 1. Set *this*.[[readIntoRequests]] to a new empty List. - +

Constructor, methods, and properties

-

Properties of the {{ReadableStreamBYOBReader}} prototype

+
+
reader = new {{ReadableStreamBYOBReader(stream)|ReadableStreamBYOBReader}}(|stream|) +
+

This is equivalent to calling |stream|.{{ReadableStream/getReader}}({ + {{ReadableStreamGetReaderOptions/mode}}: "{{ReadableStreamReaderMode/byob}}" }). + +

await reader.{{ReadableStreamBYOBReader/closed}} +
+

Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the + stream ever errors or the reader's lock is [=release a read lock|released=] before the stream + finishes closing. + +

await reader.{{ReadableStreamBYOBReader/cancel(reason)|cancel}}([ reason ]) +
+

If the reader is [=active reader|active=], behaves the same + |stream|.{{ReadableStream/cancel(reason)|cancel}}(reason). + +

{ value, done } = await reader.{{ReadableStreamBYOBReader/read()|read}}(view) +
+

Attempts to reads bytes into |view|, and returns a promise resolved with the result: + +

    +
  • If the chunk does become available, the promise will be fulfilled with an object of the form + { value: theChunk, done: false }. In this case, |view| will be + [=ArrayBuffer/detached=] and no longer usable, but theChunk will be a new view (of + the same type) onto the same backing memory region, with the chunk's data written into it. + +
  • If the stream becomes closed, the promise will be fulfilled with an object of the form + { value: undefined, done: true }. + +
  • If the stream becomes errored, the promise will be rejected with the relevant error. +
-
get closed
+

If reading a chunk causes the queue to become empty, more data will be pulled from the + [=underlying source=]. -

- The closed getter returns a promise that will be fulfilled when the stream becomes closed, or rejected if - the stream ever errors or the reader's lock is released before the stream finishes - closing. -
+
reader.{{ReadableStreamBYOBReader/releaseLock()|releaseLock}}() +
+

[=release a read lock|Releases the reader's lock=] on the corresponding stream. After the lock + is released, the reader is no longer [=active reader|active=]. If the associated stream is errored + when the lock is released, the reader will appear errored in the same way from now on; otherwise, + the reader will appear closed. - - 1. If ! IsReadableStreamBYOBReader(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. Return *this*.[[closedPromise]]. - +

A reader's lock cannot be released while it still has a pending read request, i.e., if a + promise returned by the reader's {{ReadableStreamBYOBReader/read()}} method has not yet been + settled. Attempting to do so will throw a {{TypeError}} and leave the reader locked to the stream. +

-
cancel(reason)
+
+ The ReadableStreamBYOBReader(|stream|) constructor steps + are: -
- If the reader is active, the cancel method behaves the same as that for the - associated stream. + 1. Perform ? [$SetUpReadableStreamBYOBReader$]([=this=], |stream|).
- - 1. If ! IsReadableStreamBYOBReader(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If *this*.[[ownerReadableStream]] is *undefined*, return a promise rejected with a *TypeError* exception. - 1. Return ! ReadableStreamReaderGenericCancel(*this*, _reason_). - +
+ The closed + getter steps are: -
read(view)
+ 1. Return [=this=].\[[closedPromise]]. +
-
- The read method will write read bytes into view and return a promise resolved with a - possibly transferred buffer as described below. - -
    -
  • If the chunk does become available, the promise will be fulfilled with an object of the form - { value: theChunk, done: false }. -
  • If the stream becomes closed, the promise will be fulfilled with an object of the form - { value: undefined, done: true }. -
  • If the stream becomes errored, the promise will be rejected with the relevant error. -
+
+ The cancel(|reason|) + method steps are: - If reading a chunk causes the queue to become empty, more data will be pulled from the underlying byte source. + 1. If [=this=].\[[ownerReadableStream]] is undefined, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. Return ! [$ReadableStreamReaderGenericCancel$]([=this=], |reason|).
- - 1. If ! IsReadableStreamBYOBReader(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If *this*.[[ownerReadableStream]] is *undefined*, return a promise rejected with a *TypeError* exception. - 1. If Type(_view_) is not Object, return a promise rejected with a *TypeError* exception. - 1. If _view_ does not have a [[ViewedArrayBuffer]] internal slot, return a promise rejected with a *TypeError* - exception. - 1. If ! IsDetachedBuffer(_view_.[[ViewedArrayBuffer]]) is *true*, return a promise rejected with a *TypeError* - exception. - 1. If _view_.[[ByteLength]] is *0*, return a promise rejected with a *TypeError* exception. - 1. Return ! ReadableStreamBYOBReaderRead(*this*, _view_). - +
+ The read(|view|) + method steps are: -
releaseLock()
- -
- The releaseLock method releases the reader's lock on the corresponding - stream. After the lock is released, the reader is no longer active. If the associated - stream is errored when the lock is released, the reader will appear errored in the same way from now on; otherwise, - the reader will appear closed. + 1. If [=this=].\[[ownerReadableStream]] is undefined, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. If |view|.\[[ByteLength]] is 0, return [=a promise rejected with=] a {{TypeError}} exception. + 1. Return ! [$ReadableStreamBYOBReaderRead$]([=this=], |view|). +
- A reader's lock cannot be released while it still has a pending read request, i.e., if a promise returned by the - reader's {{ReadableStreamBYOBReader/read()}} method has not yet been settled. Attempting to do so will throw - a TypeError and leave the reader locked to the stream. -
- - - 1. If ! IsReadableStreamBYOBReader(*this*) is *false*, throw a *TypeError* exception. - 1. If *this*.[[ownerReadableStream]] is *undefined*, return. - 1. If *this*.[[readIntoRequests]] is not empty, throw a *TypeError* exception. - 1. Perform ! ReadableStreamReaderGenericRelease(*this*). - - -

Readable stream reader abstract operations

- -

IsReadableStreamDefaultReader ( -x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have a [[readRequests]] internal slot, return *false*. - 1. Return *true*. - - -

IsReadableStreamBYOBReader ( -x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have a [[readIntoRequests]] internal slot, return *false*. - 1. Return *true*. - - -

ReadableStreamReaderGenericCancel ( reader, reason )

- - - 1. Let _stream_ be _reader_.[[ownerReadableStream]]. - 1. Assert: _stream_ is not *undefined*. - 1. Return ! ReadableStreamCancel(_stream_, _reason_). - - -

ReadableStreamReaderGenericInitialize ( reader, stream )

- - - 1. Set _reader_.[[forAuthorCode]] to *true*. - 1. Set _reader_.[[ownerReadableStream]] to _stream_. - 1. Set _stream_.[[reader]] to _reader_. - 1. If _stream_.[[state]] is `"readable"`, - 1. Set _reader_.[[closedPromise]] to a new promise. - 1. Otherwise, if _stream_.[[state]] is `"closed"`, - 1. Set _reader_.[[closedPromise]] to a promise resolved with *undefined*. - 1. Otherwise, - 1. Assert: _stream_.[[state]] is `"errored"`. - 1. Set _reader_.[[closedPromise]] to a promise rejected with _stream_.[[storedError]]. - 1. Set _reader_.[[closedPromise]].[[PromiseIsHandled]] to *true*. - - -

ReadableStreamReaderGenericRelease ( reader )

- - - 1. Assert: _reader_.[[ownerReadableStream]] is not *undefined*. - 1. Assert: _reader_.[[ownerReadableStream]].[[reader]] is _reader_. - 1. If _reader_.[[ownerReadableStream]].[[state]] is `"readable"`, reject _reader_.[[closedPromise]] with a *TypeError* - exception. - 1. Otherwise, set _reader_.[[closedPromise]] to a promise rejected with a *TypeError* exception. - 1. Set _reader_.[[closedPromise]].[[PromiseIsHandled]] to *true*. - 1. Set _reader_.[[ownerReadableStream]].[[reader]] to *undefined*. - 1. Set _reader_.[[ownerReadableStream]] to *undefined*. - - -

ReadableStreamBYOBReaderRead -( reader, view )

- - - 1. Let _stream_ be _reader_.[[ownerReadableStream]]. - 1. Assert: _stream_ is not *undefined*. - 1. Set _stream_.[[disturbed]] to *true*. - 1. If _stream_.[[state]] is `"errored"`, return a promise rejected with _stream_.[[storedError]]. - 1. Return ! ReadableByteStreamControllerPullInto(_stream_.[[readableStreamController]], _view_). - - -

ReadableStreamDefaultReaderRead ( reader )

- - - 1. Let _stream_ be _reader_.[[ownerReadableStream]]. - 1. Assert: _stream_ is not *undefined*. - 1. Set _stream_.[[disturbed]] to *true*. - 1. If _stream_.[[state]] is `"closed"`, return a promise resolved with ! - ReadableStreamCreateReadResult(*undefined*, *true*, _reader_.[[forAuthorCode]]). - 1. If _stream_.[[state]] is `"errored"`, return a promise rejected with _stream_.[[storedError]]. - 1. Assert: _stream_.[[state]] is `"readable"`. - 1. Return ! _stream_.[[readableStreamController]].[[PullSteps]](). - - -

Class -ReadableStreamDefaultController

- -The {{ReadableStreamDefaultController}} class has methods that allow control of a {{ReadableStream}}'s state and -internal queue. When constructing a {{ReadableStream}} that is not a readable byte stream, the -underlying source is given a corresponding {{ReadableStreamDefaultController}} instance to manipulate. - -

Class definition

+
+ The releaseLock() method steps are: -
+ 1. If [=this=].\[[ownerReadableStream]] is undefined, return. + 1. If [=this=].\[[readIntoRequests]] is not [=list/is empty|empty=], throw a {{TypeError}} + exception. + 1. Perform ! [$ReadableStreamReaderGenericRelease$]([=this=]). +
-This section is non-normative. +

The {{ReadableStreamDefaultController}} class

-If one were to write the {{ReadableStreamDefaultController}} class in something close to the syntax of [[!ECMASCRIPT]], -it would look like +The {{ReadableStreamDefaultController}} class has methods that allow control of a +{{ReadableStream}}'s state and [=internal queue=]. When constructing a {{ReadableStream}} that is +not a [=readable byte stream=], the [=underlying source=] is given a corresponding +{{ReadableStreamDefaultController}} instance to manipulate. -

-  class ReadableStreamDefaultController {
-    constructor() // always throws
+

Interface definition

- get desiredSize() +The Web IDL definition for the {{ReadableStreamDefaultController}} class is given as follows: - close() - enqueue(chunk) - error(e) - } -
+ +[Exposed=(Window,Worker,Worklet)] +interface ReadableStreamDefaultController { + readonly attribute unrestricted double? desiredSize; -</div> + void close(); + void enqueue(optional any chunk); + void error(optional any e); +}; +

Internal slots

-Instances of {{ReadableStreamDefaultController}} are created with the internal slots described in the following table: +Instances of {{ReadableStreamDefaultController}} are created with the internal slots described in +the following table: - - - - - - + - + + + - + - + - + - + - + - + - + - + - + - + +
Internal SlotDescription (non-normative)
\[[cancelAlgorithm]] - A promise-returning algorithm, taking one argument (the cancel reason), which communicates - a requested cancelation to the underlying source -
Internal SlotDescription (non-normative)
\[[closeRequested]] - A boolean flag indicating whether the stream has been closed by its underlying - source, but still has chunks in its internal queue that have not yet been read -
\[[cancelAlgorithm]] + A promise-returning algorithm, taking one argument (the cancel reason), + which communicates a requested cancelation to the [=underlying source=]
\[[controlledReadableStream]] - The {{ReadableStream}} instance controlled -
\[[closeRequested]] + A boolean flag indicating whether the stream has been closed by its + [=underlying source=], but still has [=chunks=] in its internal queue that have not yet been + read
\[[pullAgain]] - A boolean flag set to true if the stream's mechanisms requested a call - to the underlying source's pull algorithm to pull more data, but the pull could not yet be done since a - previous call is still executing -
\[[controlledReadableStream]] + The {{ReadableStream}} instance controlled
\[[pullAlgorithm]] - A promise-returning algorithm that pulls data from the underlying source -
\[[pullAgain]] + A boolean flag set to true if the stream's mechanisms requested a call + to the [=underlying source=]'s pull algorithm to pull more data, but the pull could not yet be + done since a previous call is still executing
\[[pulling]] - A boolean flag set to true while the underlying source's - pull algorithm is executing and the returned promise has not yet fulfilled, used to prevent reentrant calls -
\[[pullAlgorithm]] + A promise-returning algorithm that pulls data from the [=underlying + source=]
\[[queue]] - A List representing the stream's internal queue of chunks -
\[[pulling]] + A boolean flag set to true while the [=underlying source=]'s pull + algorithm is executing and the returned promise has not yet fulfilled, used to prevent reentrant + calls
\[[queueTotalSize]] - The total size of all the chunks stored in \[[queue]] (see [[#queue-with-sizes]]) -
\[[queue]] + A [=list=] representing the stream's internal queue of [=chunks=]
\[[started]] - A boolean flag indicating whether the underlying source has finished starting -
\[[queueTotalSize]] + The total size of all the chunks stored in \[[queue]] (see + [[#queue-with-sizes]])
\[[strategyHWM]] - A number supplied to the constructor as part of the stream's queuing strategy, - indicating the point at which the stream will apply backpressure to its underlying source -
\[[started]] + A boolean flag indicating whether the [=underlying source=] has + finished starting
\[[strategySizeAlgorithm]] - An algorithm to calculate the size of enqueued chunks, as part of the stream's - queuing strategy -
\[[strategyHWM]] + A number supplied to the constructor as part of the stream's [=queuing + strategy=], indicating the point at which the stream will apply [=backpressure=] to its + [=underlying source=] +
\[[strategySizeAlgorithm]] + An algorithm to calculate the size of enqueued [=chunks=], as part of + the stream's [=queuing strategy=]
-

new ReadableStreamDefaultController()

- -
- The ReadableStreamDefaultController constructor cannot be used directly; - {{ReadableStreamDefaultController}} instances are created automatically during {{ReadableStream}} construction. -
+

Methods and properties

+ +
+
desiredSize = controller.{{ReadableStreamDefaultController/desiredSize}} +
+

Returns the [=desired size to fill a stream's internal queue|desired size to fill the + controlled stream's internal queue=]. It can be negative, if the queue is over-full. An + [=underlying source=] ought to use this information to determine when and how to apply + [=backpressure=]. + +

controller.{{ReadableStreamDefaultController/close()|close}}() +
+

Closes the controlled readable stream. [=Consumers=] will still be able to read any + previously-enqueued [=chunks=] from the stream, but once those are read, the stream will become + closed. + +

controller.{{ReadableStreamDefaultController/enqueue()|enqueue}}(chunk) +
+

Enqueues the given [=chunk=] chunk in the controlled readable stream. + +

controller.{{ReadableStreamDefaultController/error()|error}}(e) +
+

Errors the controlled readable stream, making all future interactions with it fail with the + given error e. +

- - 1. Throw a *TypeError*. - +
+ The desiredSize attribute's getter steps are: -

Properties of the {{ReadableStreamDefaultController}} prototype

+ 1. Return ! [$ReadableStreamDefaultControllerGetDesiredSize$]([=this=]). +
-
get -desiredSize
+
+ The close() method steps are: -
- The desiredSize getter returns the desired size - to fill the controlled stream's internal queue. It can be negative, if the queue is over-full. An underlying - source ought to use this information to determine when and how to apply backpressure. + 1. If ! [$ReadableStreamDefaultControllerCanCloseOrEnqueue$]([=this=]) is false, throw a + {{TypeError}} exception. + 1. Perform ! [$ReadableStreamDefaultControllerClose$]([=this=]).
- - 1. If ! IsReadableStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. Return ! ReadableStreamDefaultControllerGetDesiredSize(*this*). - - -
close()
+
+ The enqueue(|chunk|) method steps are: -
- The close method will close the controlled readable stream. Consumers will still be able to read - any previously-enqueued chunks from the stream, but once those are read, the stream will become closed. + 1. If ! [$ReadableStreamDefaultControllerCanCloseOrEnqueue$]([=this=]) is false, throw a + {{TypeError}} exception. + 1. Perform ? [$ReadableStreamDefaultControllerEnqueue$]([=this=], |chunk|).
- - 1. If ! IsReadableStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(*this*) is *false*, throw a *TypeError* exception. - 1. Perform ! ReadableStreamDefaultControllerClose(*this*). - - -
enqueue(chunk)
+
+ The error(|e|) method steps are: -
- The enqueue method will enqueue a given chunk in the controlled readable stream. + 1. Perform ! [$ReadableStreamDefaultControllerError$]([=this=], |e|).
- - 1. If ! IsReadableStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(*this*) is *false*, throw a *TypeError* exception. - 1. Return ? ReadableStreamDefaultControllerEnqueue(*this*, _chunk_). - +

Internal methods

-
error(e)
+The following are internal methods implemented by each {{ReadableStreamDefaultController}} instance. +The readable stream implementation will polymorphically call to either these, or to their +counterparts for BYOB controllers, as discussed in [[#rs-abstract-ops-used-by-controllers]]. -
- The error method will error the readable stream, making all future interactions with it fail with the - given error e. -
- - - 1. If ! IsReadableStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. Perform ! ReadableStreamDefaultControllerError(*this*, _e_). - - -

Readable stream default controller internal methods

- -The following are additional internal methods implemented by each {{ReadableStreamDefaultController}} instance. The -readable stream implementation will polymorphically call to either these or their counterparts for BYOB controllers. - -
\[[CancelSteps]](reason)
- - - 1. Perform ! ResetQueue(*this*). - 1. Let _result_ be the result of performing *this*.[[cancelAlgorithm]], passing _reason_. - 1. Perform ! ReadableStreamDefaultControllerClearAlgorithms(*this*). - 1. Return _result_. - - -
\[[PullSteps]]( )
- - - 1. Let _stream_ be *this*.[[controlledReadableStream]]. - 1. If *this*.[[queue]] is not empty, - 1. Let _chunk_ be ! DequeueValue(*this*). - 1. If *this*.[[closeRequested]] is *true* and *this*.[[queue]] is empty, - 1. Perform ! ReadableStreamDefaultControllerClearAlgorithms(*this*). - 1. Perform ! ReadableStreamClose(_stream_). - 1. Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(*this*). - 1. Return a promise resolved with ! ReadableStreamCreateReadResult(_chunk_, *false*, - _stream_.[[reader]].[[forAuthorCode]]). - 1. Let _pendingPromise_ be ! ReadableStreamAddReadRequest(_stream_). - 1. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(*this*). - 1. Return _pendingPromise_. - - -

Readable stream default controller abstract operations

- -

IsReadableStreamDefaultController ( x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have a [[controlledReadableStream]] internal slot, return *false*. - 1. Return *true*. - - -

ReadableStreamDefaultControllerCallPullIfNeeded ( controller )

- - - 1. Let _shouldPull_ be ! ReadableStreamDefaultControllerShouldCallPull(_controller_). - 1. If _shouldPull_ is *false*, return. - 1. If _controller_.[[pulling]] is *true*, - 1. Set _controller_.[[pullAgain]] to *true*. - 1. Return. - 1. Assert: _controller_.[[pullAgain]] is *false*. - 1. Set _controller_.[[pulling]] to *true*. - 1. Let _pullPromise_ be the result of performing _controller_.[[pullAlgorithm]]. - 1. Upon fulfillment of _pullPromise_, - 1. Set _controller_.[[pulling]] to *false*. - 1. If _controller_.[[pullAgain]] is *true*, - 1. Set _controller_.[[pullAgain]] to *false*. - 1. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(_controller_). - 1. Upon rejection of _pullPromise_ with reason _e_, - 1. Perform ! ReadableStreamDefaultControllerError(_controller_, _e_). - - -

ReadableStreamDefaultControllerShouldCallPull ( controller )

- - - 1. Let _stream_ be _controller_.[[controlledReadableStream]]. - 1. If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(_controller_) is *false*, return *false*. - 1. If _controller_.[[started]] is *false*, return *false*. - 1. If ! IsReadableStreamLocked(_stream_) is *true* and ! ReadableStreamGetNumReadRequests(_stream_) > *0*, return - *true*. - 1. Let _desiredSize_ be ! ReadableStreamDefaultControllerGetDesiredSize(_controller_). - 1. Assert: _desiredSize_ is not *null*. - 1. If _desiredSize_ > *0*, return *true*. - 1. Return *false*. - - -

ReadableStreamDefaultControllerClearAlgorithms ( controller )

- -This abstract operation is called once the stream is closed or errored and the algorithms will not be executed any more. -By removing the algorithm references it permits the underlying source object to be garbage collected even if the -{{ReadableStream}} itself is still referenced. - -

The results of this algorithm are not currently observable, but could become so if JavaScript eventually -adds weak references. But even without that factor, -implementations will likely want to include similar steps.

- - - 1. Set _controller_.[[pullAlgorithm]] to *undefined*. - 1. Set _controller_.[[cancelAlgorithm]] to *undefined*. - 1. Set _controller_.[[strategySizeAlgorithm]] to *undefined*. - - -

ReadableStreamDefaultControllerClose ( controller )

- -This abstract operation can be called by other specifications that wish to close a readable stream, in the same way -a developer-created stream would be closed by its associated controller object. Specifications should not do -this to streams they did not create. - - - 1. If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(_controller_) is *false*, return. - 1. Let _stream_ be _controller_.[[controlledReadableStream]]. - 1. Set _controller_.[[closeRequested]] to *true*. - 1. If _controller_.[[queue]] is empty, - 1. Perform ! ReadableStreamDefaultControllerClearAlgorithms(_controller_). - 1. Perform ! ReadableStreamClose(_stream_). - - -

ReadableStreamDefaultControllerEnqueue ( controller, chunk )

- -This abstract operation can be called by other specifications that wish to enqueue chunks in a readable stream, -in the same way a developer would enqueue chunks using the stream's associated controller object. Specifications should -not do this to streams they did not create. - - - 1. If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(_controller_) is *false*, return. - 1. Let _stream_ be _controller_.[[controlledReadableStream]]. - 1. If ! IsReadableStreamLocked(_stream_) is *true* and ! ReadableStreamGetNumReadRequests(_stream_) > *0*, perform - ! ReadableStreamFulfillReadRequest(_stream_, _chunk_, *false*). - 1. Otherwise, - 1. Let _result_ be the result of performing _controller_.[[strategySizeAlgorithm]], passing in _chunk_, and - interpreting the result as an ECMAScript completion value. - 1. If _result_ is an abrupt completion, - 1. Perform ! ReadableStreamDefaultControllerError(_controller_, _result_.[[Value]]). - 1. Return _result_. - 1. Let _chunkSize_ be _result_.[[Value]]. - 1. Let _enqueueResult_ be EnqueueValueWithSize(_controller_, _chunk_, _chunkSize_). - 1. If _enqueueResult_ is an abrupt completion, - 1. Perform ! ReadableStreamDefaultControllerError(_controller_, _enqueueResult_.[[Value]]). - 1. Return _enqueueResult_. - 1. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(_controller_). - - -

ReadableStreamDefaultControllerError ( controller, e )

- -This abstract operation can be called by other specifications that wish to move a readable stream to an errored state, -in the same way a developer would error a stream using its associated controller object. Specifications should -not do this to streams they did not create. - - - 1. Let _stream_ be _controller_.[[controlledReadableStream]]. - 1. If _stream_.[[state]] is not `"readable"`, return. - 1. Perform ! ResetQueue(_controller_). - 1. Perform ! ReadableStreamDefaultControllerClearAlgorithms(_controller_). - 1. Perform ! ReadableStreamError(_stream_, _e_). - - -

ReadableStreamDefaultControllerGetDesiredSize ( controller )

- -This abstract operation can be called by other specifications that wish to determine the desired size to fill this stream's internal queue, similar to how a developer would consult -the {{ReadableStreamDefaultController/desiredSize}} property of the stream's associated controller object. -Specifications should not use this on streams they did not create. - - - 1. Let _stream_ be _controller_.[[controlledReadableStream]]. - 1. Let _state_ be _stream_.[[state]]. - 1. If _state_ is `"errored"`, return *null*. - 1. If _state_ is `"closed"`, return *0*. - 1. Return _controller_.[[strategyHWM]] − _controller_.[[queueTotalSize]]. - - -

ReadableStreamDefaultControllerHasBackpressure ( controller )

- -This abstract operation is used in the implementation of TransformStream. - - - 1. If ! ReadableStreamDefaultControllerShouldCallPull(_controller_) is *true*, return *false*. - 1. Otherwise, return *true*. - - -

ReadableStreamDefaultControllerCanCloseOrEnqueue ( controller )

- - - 1. Let _state_ be _controller_.[[controlledReadableStream]].[[state]]. - 1. If _controller_.[[closeRequested]] is *false* and _state_ is `"readable"`, return *true*. - 1. Otherwise, return *false*. - +
+ \[[CancelSteps]](|reason|) implements the + [$ReadableStreamController/[[CancelSteps]]$] contract. It performs the following steps: -
- The case where stream.\[[closeRequested]] is false, but stream.\[[state]] is - not "readable", happens when the stream is errored via {{ReadableStreamDefaultController/error(e)}}, or - when it is closed without its controller's close method ever being called: e.g., if the stream was closed - by a call to {{ReadableStream/cancel(reason)}}. -
- -

SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, -pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm )

- - - 1. Assert: _stream_.[[readableStreamController]] is *undefined*. - 1. Set _controller_.[[controlledReadableStream]] to _stream_. - 1. Set _controller_.[[queue]] and _controller_.[[queueTotalSize]] to *undefined*, then perform ! - ResetQueue(_controller_). - 1. Set _controller_.[[started]], _controller_.[[closeRequested]], _controller_.[[pullAgain]], and - _controller_.[[pulling]] to *false*. - 1. Set _controller_.[[strategySizeAlgorithm]] to _sizeAlgorithm_ and _controller_.[[strategyHWM]] to _highWaterMark_. - 1. Set _controller_.[[pullAlgorithm]] to _pullAlgorithm_. - 1. Set _controller_.[[cancelAlgorithm]] to _cancelAlgorithm_. - 1. Set _stream_.[[readableStreamController]] to _controller_. - 1. Let _startResult_ be the result of performing _startAlgorithm_. (This may throw an exception.) - 1. Let _startPromise_ be a promise resolved with _startResult_. - 1. Upon fulfillment of _startPromise_, - 1. Set _controller_.[[started]] to *true*. - 1. Assert: _controller_.[[pulling]] is *false*. - 1. Assert: _controller_.[[pullAgain]] is *false*. - 1. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(_controller_). - 1. Upon rejection of _startPromise_ with reason _r_, - 1. Perform ! ReadableStreamDefaultControllerError(_controller_, _r_). - - -

SetUpReadableStreamDefaultControllerFromUnderlyingSource(stream, underlyingSource, -highWaterMark, sizeAlgorithm )

- - - 1. Assert: _underlyingSource_ is not *undefined*. - 1. Let _controller_ be ObjectCreate(the original value of `ReadableStreamDefaultController`'s `prototype` - property). - 1. Let _startAlgorithm_ be the following steps: - 1. Return ? InvokeOrNoop(_underlyingSource_, `"start"`, « _controller_ »). - 1. Let _pullAlgorithm_ be ? CreateAlgorithmFromUnderlyingMethod(_underlyingSource_, `"pull"`, *0*, « _controller_ »). - 1. Let _cancelAlgorithm_ be ? CreateAlgorithmFromUnderlyingMethod(_underlyingSource_, `"cancel"`, *1*, « »). - 1. Perform ? SetUpReadableStreamDefaultController(_stream_, _controller_, _startAlgorithm_, _pullAlgorithm_, - _cancelAlgorithm_, _highWaterMark_, _sizeAlgorithm_). - - -

Class -ReadableByteStreamController

- -The {{ReadableByteStreamController}} class has methods that allow control of a {{ReadableStream}}'s state and -internal queue. When constructing a {{ReadableStream}}, the underlying byte source is given a -corresponding {{ReadableByteStreamController}} instance to manipulate. - -

Class definition

+ 1. Perform ! [$ResetQueue$]([=this=]). + 1. Let |result| be the result of performing [=this=].\[[cancelAlgorithm]], passing |reason|. + 1. Perform ! [$ReadableStreamDefaultControllerClearAlgorithms$]([=this=]). + 1. Return |result|. +
-
+
+ \[[PullSteps]]() implements the + [$ReadableStreamController/[[PullSteps]]$] contract. It performs the following steps: + + 1. Let |stream| be [=this=].\[[controlledReadableStream]]. + 1. If [=this=].\[[queue]] is not [=list/is empty|empty=], + 1. Let |chunk| be ! [$DequeueValue$]([=this=]). + 1. If [=this=].\[[closeRequested]] is true and [=this=].\[[queue]] [=list/is empty=], + 1. Perform ! [$ReadableStreamDefaultControllerClearAlgorithms$]([=this=]). + 1. Perform ! [$ReadableStreamClose$](|stream|). + 1. Otherwise, perform ! [$ReadableStreamDefaultControllerCallPullIfNeeded$]([=this=]). + 1. Return [=a promise resolved with=] ! [$ReadableStreamCreateReadResult$](|chunk|, false, + |stream|.\[[reader]].\[[forAuthorCode]]). + 1. Let |pendingPromise| be ! [$ReadableStreamAddReadRequest$](|stream|). + 1. Perform ! [$ReadableStreamDefaultControllerCallPullIfNeeded$]([=this=]). + 1. Return |pendingPromise|. +
-This section is non-normative. +

The {{ReadableByteStreamController}} class

-If one were to write the {{ReadableByteStreamController}} class in something close to the syntax of [[!ECMASCRIPT]], it -would look like +The {{ReadableByteStreamController}} class has methods that allow control of a {{ReadableStream}}'s +state and [=internal queue=]. When constructing a {{ReadableStream}} that is a [=readable byte +stream=], the [=underlying source=] is given a corresponding {{ReadableByteStreamController}} +instance to manipulate. -

-  class ReadableByteStreamController {
-    constructor() // always throws
+

Interface definition

- get byobRequest() - get desiredSize() +The Web IDL definition for the {{ReadableByteStreamController}} class is given as follows: - close() - enqueue(chunk) - error(e) - } -
+ +[Exposed=(Window,Worker,Worklet)] +interface ReadableByteStreamController { + readonly attribute ReadableStreamBYOBRequest byobRequest; + readonly attribute unrestricted double? desiredSize; -</div> + void close(); + void enqueue(ArrayBufferView chunk); + void error(optional any e); +}; +

Internal slots

-Instances of {{ReadableByteStreamController}} are created with the internal slots described in the following table: +Instances of {{ReadableByteStreamController}} are created with the internal slots described in the +following table: - - - - - - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - +
Internal SlotDescription (non-normative)
Internal SlotDescription (non-normative)
\[[autoAllocateChunkSize]] - A positive integer, when the automatic buffer allocation feature is enabled. In that case, - this value specifies the size of buffer to allocate. It is undefined otherwise. -
\[[autoAllocateChunkSize]] + A positive integer, when the automatic buffer allocation feature is + enabled. In that case, this value specifies the size of buffer to allocate. It is undefined + otherwise.
\[[byobRequest]] - A {{ReadableStreamBYOBRequest}} instance representing the current BYOB pull request, - or undefined if there are no pending requests -
\[[byobRequest]] + A {{ReadableStreamBYOBRequest}} instance representing the current BYOB + pull request, or undefined if there are no pending requests
\[[cancelAlgorithm]] - A promise-returning algorithm, taking one argument (the cancel reason), which communicates - a requested cancelation to the underlying source -
\[[cancelAlgorithm]] + A promise-returning algorithm, taking one argument (the cancel reason), + which communicates a requested cancelation to the [=underlying byte source=]
\[[closeRequested]] - A boolean flag indicating whether the stream has been closed by its underlying byte - source, but still has chunks in its internal queue that have not yet been read -
\[[closeRequested]] + A boolean flag indicating whether the stream has been closed by its + [=underlying byte source=], but still has [=chunks=] in its internal queue that have not yet been + read
\[[controlledReadableByteStream]] - The {{ReadableStream}} instance controlled -
\[[controlledReadableStream]] + The {{ReadableStream}} instance controlled
\[[pullAgain]] - A boolean flag set to true if the stream's mechanisms requested a call - to the underlying byte source's {{underlying source/pull()}} method to pull more data, but the pull could - not yet be done since a previous call is still executing -
\[[pullAgain]] + A boolean flag set to true if the stream's mechanisms requested a call + to the [=underlying byte source=]'s pull algorithm to pull more data, but the pull could not yet + be done since a previous call is still executing
\[[pullAlgorithm]] - A promise-returning algorithm that pulls data from the underlying source -
\[[pullAlgorithm]] + A promise-returning algorithm that pulls data from the [=underlying + byte source=]
\[[pulling]] - A boolean flag set to true while the underlying byte source's - {{underlying source/pull()}} method is executing and has not yet fulfilled, used to prevent reentrant calls -
\[[pulling]] + A boolean flag set to true while the [=underlying byte source=]'s pull + algorithm is executing and the returned promise has not yet fulfilled, used to prevent reentrant + calls
\[[pendingPullIntos]] - A List of descriptors representing pending BYOB pull requests -
\[[pendingPullIntos]] + A [=list=] of descriptors representing pending BYOB pull requests
\[[queue]] - A List representing the stream's internal queue of chunks -
\[[queue]] + A [=list=] representing the stream's internal queue of [=chunks=]
\[[queueTotalSize]] - The total size (in bytes) of all the chunks stored in \[[queue]] -
\[[queueTotalSize]] + The total size, in bytes, of all the chunks stored in \[[queue]] (see + [[#queue-with-sizes]])
\[[started]] - A boolean flag indicating whether the underlying source has finished starting -
\[[started]] + A boolean flag indicating whether the [=underlying byte source=] has + finished starting
\[[strategyHWM]] - A number supplied to the constructor as part of the stream's queuing strategy, - indicating the point at which the stream will apply backpressure to its underlying byte source -
\[[strategyHWM]] + A number supplied to the constructor as part of the stream's [=queuing + strategy=], indicating the point at which the stream will apply [=backpressure=] to its + [=underlying byte source=]
-

Although {{ReadableByteStreamController}} instances have \[[queue]] and \[[queueTotalSize]] slots, we do not use - most of the abstract operations in [[#queue-with-sizes]] on them, as the way in which we manipulate this queue is - rather different than the others in the spec. Instead, we update the two slots together manually.

+

Although {{ReadableByteStreamController}} instances have \[[queue]] and \[[queueTotalSize]] + slots, we do not use most of the abstract operations in [[#queue-with-sizes]] on them, as the way + in which we manipulate this queue is rather different than the others in the spec. Instead, we + update the two slots together manually. -

This might be cleaned up in a future spec refactoring.

+

This might be cleaned up in a future spec refactoring.

-

new -ReadableByteStreamController()

+

Methods and properties

+ +
+
byobRequest = controller.{{ReadableByteStreamController/byobRequest}} +
+

Returns the current BYOB pull request. + +

desiredSize = controller.{{ReadableByteStreamController/desiredSize}} +
+

Returns the [=desired size to fill a stream's internal queue|desired size to fill the + controlled stream's internal queue=]. It can be negative, if the queue is over-full. An + [=underlying byte source=] ought to use this information to determine when and how to apply + [=backpressure=]. + +

controller.{{ReadableByteStreamController/close()|close}}() +
+

Closes the controlled readable stream. [=Consumers=] will still be able to read any + previously-enqueued [=chunks=] from the stream, but once those are read, the stream will become + closed. + +

controller.{{ReadableByteStreamController/enqueue()|enqueue}}(chunk) +
+

Enqueues the given [=chunk=] chunk in the controlled readable stream. The + chunk has to be an {{ArrayBufferView}} instance, or else a {{TypeError}} will be thrown. + +

controller.{{ReadableByteStreamController/error()|error}}(e) +
+

Errors the controlled readable stream, making all future interactions with it fail with the + given error e. +

-
- The ReadableByteStreamController constructor cannot be used directly; - {{ReadableByteStreamController}} instances are created automatically during {{ReadableStream}} construction. +
+ The byobRequest attribute's getter steps are: + + 1. If [=this=].\[[byobRequest]] is undefined and [=this=].\[[pendingPullIntos]] is not [=list/is + empty|empty=], + 1. Let |firstDescriptor| be [=this=].\[[pendingPullIntos]][0]. + 1. Let |view| be ! [$Construct$]({{%Uint8Array%}}, « |firstDescriptor|.\[[buffer]], + |firstDescriptor|.\[[byteOffset]] + |firstDescriptor|.\[[bytesFilled]], + |firstDescriptor|.\[[byteLength]] − |firstDescriptor|.\[[bytesFilled]] »). + 1. Let |byobRequest| be a [=new=] {{ReadableStreamBYOBRequest}}. + 1. Set |byobRequest|.\[[controller]] to [=this=]. + 1. Set |byobRequest|.\[[view]] to |view|. + 1. Set [=this=].\[[byobRequest]] to |byobRequest|. + 1. Return [=this=].\[[byobRequest]].
- - 1. Throw a *TypeError* exception. - +
+ The desiredSize attribute's getter steps are: -

Properties of the {{ReadableByteStreamController}} prototype

- -
get byobRequest
- -
- The byobRequest getter returns the current BYOB pull request. + 1. Return ! [$ReadableByteStreamControllerGetDesiredSize$]([=this=]).
- - 1. If ! IsReadableByteStreamController(*this*) is *false*, throw a *TypeError* exception. - 1. If *this*.[[byobRequest]] is *undefined* and *this*.[[pendingPullIntos]] is not empty, - 1. Let _firstDescriptor_ be the first element of *this*.[[pendingPullIntos]]. - 1. Let _view_ be ! Construct(%Uint8Array%, « _firstDescriptor_.[[buffer]], - _firstDescriptor_.[[byteOffset]] + _firstDescriptor_.[[bytesFilled]], _firstDescriptor_.[[byteLength]] − - _firstDescriptor_.[[bytesFilled]] »). - 1. Let _byobRequest_ be ObjectCreate(the original value of `ReadableStreamBYOBRequest`'s `prototype` - property). - 1. Perform ! SetUpReadableStreamBYOBRequest(_byobRequest_, *this*, _view_). - 1. Set *this*.[[byobRequest]] to _byobRequest_. - 1. Return *this*.[[byobRequest]]. - - -
get desiredSize
+
+ The close() method + steps are: -
- The desiredSize getter returns the desired size - to fill the controlled stream's internal queue. It can be negative, if the queue is over-full. An underlying - source ought to use this information to determine when and how to apply backpressure. + 1. If [=this=].\[[closeRequested]] is true, throw a {{TypeError}} exception. + 1. If [=this=].\[[controlledReadableStream]].\[[state]] is not "`readable`", throw a {{TypeError}} + exception. + 1. Perform ? [$ReadableByteStreamControllerClose$]([=this=]).
- - 1. If ! IsReadableByteStreamController(*this*) is *false*, throw a *TypeError* exception. - 1. Return ! ReadableByteStreamControllerGetDesiredSize(*this*). - - -
close()
+
+ The enqueue(|chunk|) method steps are: -
- The close method will close the controlled readable stream. Consumers will still be able to read - any previously-enqueued chunks from the stream, but once those are read, the stream will become closed. + 1. If [=this=].\[[closeRequested]] is true, throw a {{TypeError}} exception. + 1. If [=this=].\[[controlledReadableStream]].\[[state]] is not "`readable`", throw a {{TypeError}} + exception. + 1. Return ! [$ReadableByteStreamControllerEnqueue$]([=this=], |chunk|).
- - 1. If ! IsReadableByteStreamController(*this*) is *false*, throw a *TypeError* exception. - 1. If *this*.[[closeRequested]] is *true*, throw a *TypeError* exception. - 1. If *this*.[[controlledReadableByteStream]].[[state]] is not `"readable"`, throw a *TypeError* exception. - 1. Perform ? ReadableByteStreamControllerClose(*this*). - - -
enqueue(chunk)
+
+ The error(|e|) + method steps are: -
- The enqueue method will enqueue a given chunk in the controlled readable stream. + 1. Perform ! [$ReadableByteStreamControllerError$]([=this=], |e|).
- - 1. If ! IsReadableByteStreamController(*this*) is *false*, throw a *TypeError* exception. - 1. If *this*.[[closeRequested]] is *true*, throw a *TypeError* exception. - 1. If *this*.[[controlledReadableByteStream]].[[state]] is not `"readable"`, throw a *TypeError* exception. - 1. If Type(_chunk_) is not Object, throw a *TypeError* exception. - 1. If _chunk_ does not have a [[ViewedArrayBuffer]] internal slot, throw a *TypeError* exception. - 1. If ! IsDetachedBuffer(_chunk_.[[ViewedArrayBuffer]]) is *true*, throw a *TypeError* exception. - 1. Return ! ReadableByteStreamControllerEnqueue(*this*, _chunk_). - +

Internal methods

-
error(e)
+The following are internal methods implemented by each {{ReadableByteStreamController}} instance. +The readable stream implementation will polymorphically call to either these, or to their +counterparts for default controllers, as discussed in [[#rs-abstract-ops-used-by-controllers]]. -
- The error method will error the readable stream, making all future interactions with it fail with the - given error e. -
- - - 1. If ! IsReadableByteStreamController(*this*) is *false*, throw a *TypeError* exception. - 1. Perform ! ReadableByteStreamControllerError(*this*, _e_). - - -

Readable stream BYOB controller internal methods

- -The following are additional internal methods implemented by each {{ReadableByteStreamController}} instance. The -readable stream implementation will polymorphically call to either these or their counterparts for default controllers. - -
\[[CancelSteps]](reason)
- - - 1. If *this*.[[pendingPullIntos]] is not empty, - 1. Let _firstDescriptor_ be the first element of *this*.[[pendingPullIntos]]. - 1. Set _firstDescriptor_.[[bytesFilled]] to *0*. - 1. Perform ! ResetQueue(*this*). - 1. Let _result_ be the result of performing *this*.[[cancelAlgorithm]], passing in _reason_. - 1. Perform ! ReadableByteStreamControllerClearAlgorithms(*this*). - 1. Return _result_. - - -
\[[PullSteps]]( )
- - - 1. Let _stream_ be *this*.[[controlledReadableByteStream]]. - 1. Assert: ! ReadableStreamHasDefaultReader(_stream_) is *true*. - 1. If *this*.[[queueTotalSize]] > *0*, - 1. Assert: ! ReadableStreamGetNumReadRequests(_stream_) is *0*. - 1. Let _entry_ be the first element of *this*.[[queue]]. - 1. Remove _entry_ from *this*.[[queue]], shifting all other elements downward (so that the second becomes the - first, and so on). - 1. Set *this*.[[queueTotalSize]] to *this*.[[queueTotalSize]] − _entry_.[[byteLength]]. - 1. Perform ! ReadableByteStreamControllerHandleQueueDrain(*this*). - 1. Let _view_ be ! Construct(%Uint8Array%, « _entry_.[[buffer]], _entry_.[[byteOffset]], - _entry_.[[byteLength]] »). - 1. Return a promise resolved with ! ReadableStreamCreateReadResult(_view_, *false*, - _stream_.[[reader]].[[forAuthorCode]]). - 1. Let _autoAllocateChunkSize_ be *this*.[[autoAllocateChunkSize]]. - 1. If _autoAllocateChunkSize_ is not *undefined*, - 1. Let _buffer_ be Construct(%ArrayBuffer%, « _autoAllocateChunkSize_ »). - 1. If _buffer_ is an abrupt completion, return a promise rejected with _buffer_.[[Value]]. - 1. Let _pullIntoDescriptor_ be Record {[[buffer]]: _buffer_.[[Value]], [[byteOffset]]: *0*, [[byteLength]]: - _autoAllocateChunkSize_, [[bytesFilled]]: *0*, [[elementSize]]: *1*, [[ctor]]: %Uint8Array%, - [[readerType]]: `"default"`}. - 1. Append _pullIntoDescriptor_ as the last element of *this*.[[pendingPullIntos]]. - 1. Let _promise_ be ! ReadableStreamAddReadRequest(_stream_). - 1. Perform ! ReadableByteStreamControllerCallPullIfNeeded(*this*). - 1. Return _promise_. - - -

Class -ReadableStreamBYOBRequest

- -The {{ReadableStreamBYOBRequest}} class represents a pull into request in a {{ReadableByteStreamController}}. - -

Class definition

+
+ \[[CancelSteps]](|reason|) implements the + [$ReadableStreamController/[[CancelSteps]]$] contract. It performs the following steps: -
+ 1. If [=this=].\[[pendingPullIntos]] is not [=list/is empty|empty=], + 1. Let |firstDescriptor| be [=this=].\[[pendingPullIntos]][0]. + 1. Set |firstDescriptor|.\[[bytesFilled]] to 0. + 1. Perform ! [$ResetQueue$]([=this=]). + 1. Let |result| be the result of performing [=this=].\[[cancelAlgorithm]], passing in |reason|. + 1. Perform ! [$ReadableByteStreamControllerClearAlgorithms$]([=this=]). + 1. Return |result|. +
-This section is non-normative. +
+ \[[PullSteps]]() implements the + [$ReadableStreamController/[[PullSteps]]$] contract. It performs the following steps: + + 1. Let |stream| be [=this=].\[[controlledReadableStream]]. + 1. Assert: ! [$ReadableStreamHasDefaultReader$](|stream|) is true. + 1. If [=this=].\[[queueTotalSize]] > 0, + 1. Assert: ! [$ReadableStreamGetNumReadRequests$](|stream|) is 0. + 1. Let |entry| be [=this=].\[[queue]][0]. + 1. [=list/Remove=] |entry| from [=this=].\[[queue]]. + 1. Set [=this=].\[[queueTotalSize]] to [=this=].\[[queueTotalSize]] − |entry|.\[[byteLength]]. + 1. Perform ! [$ReadableByteStreamControllerHandleQueueDrain$]([=this=]). + 1. Let |view| be ! [$Construct$]({{%Uint8Array%}}, « |entry|.\[[buffer]], |entry|.\[[byteOffset]], + |entry|.\[[byteLength]] »). + 1. Return [=a promise resolved with=] ! [$ReadableStreamCreateReadResult$](|view|, false, + |stream|.\[[reader]].\[[forAuthorCode]]). + 1. Let |autoAllocateChunkSize| be [=this=].\[[autoAllocateChunkSize]]. + 1. If |autoAllocateChunkSize| is not undefined, + 1. Let |buffer| be [$Construct$]({{%ArrayBuffer%}}, « |autoAllocateChunkSize| »). + 1. If |buffer| is an abrupt completion, return [=a promise rejected with=] |buffer|.\[[Value]]. + 1. Let |pullIntoDescriptor| be Record {\[[buffer]]: |buffer|.\[[Value]], \[[byteOffset]]: 0, + \[[byteLength]]: |autoAllocateChunkSize|, \[[bytesFilled]]: 0, \[[elementSize]]: 1, \[[ctor]]: + {{%Uint8Array%}}, \[[readerType]]: "`default`"}. + 1. [=list/Append=] |pullIntoDescriptor| to [=this=].\[[pendingPullIntos]]. + 1. Let |promise| be ! [$ReadableStreamAddReadRequest$](|stream|). + 1. Perform ! [$ReadableByteStreamControllerCallPullIfNeeded$]([=this=]). + 1. Return |promise|. +
-If one were to write the {{ReadableStreamBYOBRequest}} class in something close to the syntax of [[!ECMASCRIPT]], it -would look like +

The {{ReadableStreamBYOBRequest}} class

-

-  class ReadableStreamBYOBRequest {
-    constructor(controller, view)
+The {{ReadableStreamBYOBRequest}} class represents a pull-into request in a
+{{ReadableByteStreamController}}.
 
-    get view()
+

Interface definition

- respond(bytesWritten) - respondWithNewView(view) - } -
+The Web IDL definition for the {{ReadableStreamBYOBRequest}} class is given as follows: -
+ +[Exposed=(Window,Worker,Worklet)] +interface ReadableStreamBYOBRequest { + readonly attribute ArrayBufferView view; + + void respond([EnforceRange] unsigned long long bytesWritten); + void respondWithNewView(ArrayBufferView view); +}; +

Internal slots

-Instances of {{ReadableStreamBYOBRequest}} are created with the internal slots described in the following table: +Instances of {{ReadableStreamBYOBRequest}} are created with the internal slots described in the +following table: - - - - - - - - + - + + + + + +
Internal SlotDescription (non-normative)
\[[associatedReadableByteStreamController]] - The parent {{ReadableByteStreamController}} instance -
\[[view]] - A typed array representing the destination region to which the controller can write - generated data -
Internal SlotDescription (non-normative)
\[[controller]] + The parent {{ReadableByteStreamController}} instance +
\[[view]] + A [=typed array=] representing the destination region to which the + controller can write generated data
-

new -ReadableStreamBYOBRequest()

+

Methods and properties

- - 1. Throw a *TypeError* exception. - +
+
view = byobRequest.{{ReadableStreamBYOBRequest/view}} +
+

Returns the view for writing in to. -

Properties of the {{ReadableStreamBYOBRequest}} prototype

+
byobRequest.{{ReadableStreamBYOBRequest/respond()|respond}}(bytesWritten) +
+

Indicates to the associated [=readable byte stream=] that bytesWritten bytes + were written into {{ReadableStreamBYOBRequest/view}}, causing the result be surfaced to the + [=consumer=]. -

get view
+

After this method is called, {{ReadableStreamBYOBRequest/view}} will be transferred and no longer modifiable. - - 1. If ! IsReadableStreamBYOBRequest(*this*) is *false*, throw a *TypeError* exception. - 1. Return *this*.[[view]]. - +

byobRequest.{{ReadableStreamBYOBRequest/respondWithNewView()|respondWithNewView}}(view) +
+

Indicates to the associated [=readable byte stream=] that instead of writing into + {{ReadableStreamBYOBRequest/view}}, the [=underlying byte source=] is providing a new + {{ArrayBufferView}}, which will be given to the [=consumer=] of the [=readable byte stream=]. -

respond(bytesWritten)
+

After this method is called, view will be transferred and no longer modifiable. +

- - 1. If ! IsReadableStreamBYOBRequest(*this*) is *false*, throw a *TypeError* exception. - 1. If *this*.[[associatedReadableByteStreamController]] is *undefined*, throw a *TypeError* exception. - 1. If ! IsDetachedBuffer(*this*.[[view]].[[ViewedArrayBuffer]]) is *true*, throw a *TypeError* exception. - 1. Return ? ReadableByteStreamControllerRespond(*this*.[[associatedReadableByteStreamController]], _bytesWritten_). - +
+ The view + attribute's getter steps are: -
respondWithNewView(view)
+ 1. Return [=this=].\[[view]]. +
- - 1. If ! IsReadableStreamBYOBRequest(*this*) is *false*, throw a *TypeError* exception. - 1. If *this*.[[associatedReadableByteStreamController]] is *undefined*, throw a *TypeError* exception. - 1. If Type(_view_) is not Object, throw a *TypeError* exception. - 1. If _view_ does not have a [[ViewedArrayBuffer]] internal slot, throw a *TypeError* exception. - 1. If ! IsDetachedBuffer(_view_.[[ViewedArrayBuffer]]) is *true*, throw a *TypeError* exception. - 1. Return ? ReadableByteStreamControllerRespondWithNewView(*this*.[[associatedReadableByteStreamController]], _view_). - +
+ The respond(|bytesWritten|) method steps are: -

Readable stream BYOB controller abstract operations

+ 1. If [=this=].\[[controller]] is undefined, throw a {{TypeError}} exception. + 1. Perform ? [$ReadableByteStreamControllerRespond$]([=this=].\[[controller]], |bytesWritten|). +
-

IsReadableStreamBYOBRequest ( -x )

+
+ The respondWithNewView(|view|) method steps are: - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have an [[associatedReadableByteStreamController]] internal slot, return *false*. - 1. Return *true*. - + 1. If [=this=].\[[controller]] is undefined, throw a {{TypeError}} exception. + 1. Return ? [$ReadableByteStreamControllerRespondWithNewView$]([=this=].\[[controller]], |view|). +
+

Abstract operations

-

IsReadableByteStreamController -( x )

+

Working with readable streams

- - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have an [[controlledReadableByteStream]] internal slot, return *false*. - 1. Return *true*. - +The following abstract operations operate on {{ReadableStream}} instances at a higher level. Some +are even meant to be generally useful by other specifications. -

ReadableByteStreamControllerCallPullIfNeeded ( controller )

+
+ AcquireReadableStreamBYOBReader(|stream|[, |forAuthorCode|]) is meant to be called + from other specifications that wish to acquire a [=BYOB reader=] for a given stream. It performs + the following steps: - - 1. Let _shouldPull_ be ! ReadableByteStreamControllerShouldCallPull(_controller_). - 1. If _shouldPull_ is *false*, return. - 1. If _controller_.[[pulling]] is *true*, - 1. Set _controller_.[[pullAgain]] to *true*. - 1. Return. - 1. Assert: _controller_.[[pullAgain]] is *false*. - 1. Set _controller_.[[pulling]] to *true*. - 1. Let _pullPromise_ be the result of performing _controller_.[[pullAlgorithm]]. - 1. Upon fulfillment of _pullPromise_, - 1. Set _controller_.[[pulling]] to *false*. - 1. If _controller_.[[pullAgain]] is *true*, - 1. Set _controller_.[[pullAgain]] to *false*. - 1. Perform ! ReadableByteStreamControllerCallPullIfNeeded(_controller_). - 1. Upon rejection of _pullPromise_ with reason _e_, - 1. Perform ! ReadableByteStreamControllerError(_controller_, _e_). - - -

ReadableByteStreamControllerClearAlgorithms ( controller )

- -This abstract operation is called once the stream is closed or errored and the algorithms will not be executed any more. -By removing the algorithm references it permits the underlying source object to be garbage collected even if the -{{ReadableStream}} itself is still referenced. - -

The results of this algorithm are not currently observable, but could become so if JavaScript eventually -adds weak references. But even without that factor, -implementations will likely want to include similar steps.

- - - 1. Set _controller_.[[pullAlgorithm]] to *undefined*. - 1. Set _controller_.[[cancelAlgorithm]] to *undefined*. - - -

ReadableByteStreamControllerClearPendingPullIntos ( -controller )

- - - 1. Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(_controller_). - 1. Set _controller_.[[pendingPullIntos]] to a new empty List. - - -

ReadableByteStreamControllerClose ( controller )

- - - 1. Let _stream_ be _controller_.[[controlledReadableByteStream]]. - 1. If _controller_.[[closeRequested]] is *true* or _stream_.[[state]] is not `"readable"`, return. - 1. If _controller_.[[queueTotalSize]] > *0*, - 1. Set _controller_.[[closeRequested]] to *true*. - 1. Return. - 1. If _controller_.[[pendingPullIntos]] is not empty, - 1. Let _firstPendingPullInto_ be the first element of _controller_.[[pendingPullIntos]]. - 1. If _firstPendingPullInto_.[[bytesFilled]] > *0*, - 1. Let _e_ be a new *TypeError* exception. - 1. Perform ! ReadableByteStreamControllerError(_controller_, _e_). - 1. Throw _e_. - 1. Perform ! ReadableByteStreamControllerClearAlgorithms(_controller_). - 1. Perform ! ReadableStreamClose(_stream_). - - -

ReadableByteStreamControllerCommitPullIntoDescriptor -( stream, pullIntoDescriptor )

- - - 1. Assert: _stream_.[[state]] is not `"errored"`. - 1. Let _done_ be *false*. - 1. If _stream_.[[state]] is `"closed"`, - 1. Assert: _pullIntoDescriptor_.[[bytesFilled]] is *0*. - 1. Set _done_ to *true*. - 1. Let _filledView_ be ! ReadableByteStreamControllerConvertPullIntoDescriptor(_pullIntoDescriptor_). - 1. If _pullIntoDescriptor_.[[readerType]] is `"default"`, - 1. Perform ! ReadableStreamFulfillReadRequest(_stream_, _filledView_, _done_). - 1. Otherwise, - 1. Assert: _pullIntoDescriptor_.[[readerType]] is `"byob"`. - 1. Perform ! ReadableStreamFulfillReadIntoRequest(_stream_, _filledView_, _done_). - - -

ReadableByteStreamControllerConvertPullIntoDescriptor ( pullIntoDescriptor )

- - - 1. Let _bytesFilled_ be _pullIntoDescriptor_.[[bytesFilled]]. - 1. Let _elementSize_ be _pullIntoDescriptor_.[[elementSize]]. - 1. Assert: _bytesFilled_ ≤ _pullIntoDescriptor_.[[byteLength]]. - 1. Assert: _bytesFilled_ mod _elementSize_ is *0*. - 1. Return ! Construct(_pullIntoDescriptor_.[[ctor]], « _pullIntoDescriptor_.[[buffer]], - _pullIntoDescriptor_.[[byteOffset]], _bytesFilled_ ÷ _elementSize_ »). - - -

ReadableByteStreamControllerEnqueue ( controller, chunk )

- - - 1. Let _stream_ be _controller_.[[controlledReadableByteStream]]. - 1. If _controller_.[[closeRequested]] is *true* or _stream_.[[state]] is not `"readable"`, return. - 1. Let _buffer_ be _chunk_.[[ViewedArrayBuffer]]. - 1. Let _byteOffset_ be _chunk_.[[ByteOffset]]. - 1. Let _byteLength_ be _chunk_.[[ByteLength]]. - 1. Let _transferredBuffer_ be ! TransferArrayBuffer(_buffer_). - 1. If ! ReadableStreamHasDefaultReader(_stream_) is *true* - 1. If ! ReadableStreamGetNumReadRequests(_stream_) is *0*, - 1. Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(_controller_, _transferredBuffer_, _byteOffset_, - _byteLength_). - 1. Otherwise, - 1. Assert: _controller_.[[queue]] is empty. - 1. Let _transferredView_ be ! Construct(%Uint8Array%, « _transferredBuffer_, _byteOffset_, - _byteLength_ »). - 1. Perform ! ReadableStreamFulfillReadRequest(_stream_, _transferredView_, *false*). - 1. Otherwise, if ! ReadableStreamHasBYOBReader(_stream_) is *true*, - 1. Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(_controller_, _transferredBuffer_, _byteOffset_, - _byteLength_). - 1. Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(_controller_). - 1. Otherwise, - 1. Assert: ! IsReadableStreamLocked(_stream_) is *false*. - 1. Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(_controller_, _transferredBuffer_, _byteOffset_, - _byteLength_). - 1. Perform ! ReadableByteStreamControllerCallPullIfNeeded(_controller_). - - -

ReadableByteStreamControllerEnqueueChunkToQueue ( controller, buffer, -byteOffset, byteLength )

- - - 1. Append Record {[[buffer]]: _buffer_, [[byteOffset]]: _byteOffset_, [[byteLength]]: _byteLength_} as the last - element of _controller_.[[queue]]. - 1. Add _byteLength_ to _controller_.[[queueTotalSize]]. - - -

ReadableByteStreamControllerError ( controller, e )

- - - 1. Let _stream_ be _controller_.[[controlledReadableByteStream]]. - 1. If _stream_.[[state]] is not `"readable"`, return. - 1. Perform ! ReadableByteStreamControllerClearPendingPullIntos(_controller_). - 1. Perform ! ResetQueue(_controller_). - 1. Perform ! ReadableByteStreamControllerClearAlgorithms(_controller_). - 1. Perform ! ReadableStreamError(_stream_, _e_). - - -

ReadableByteStreamControllerFillHeadPullIntoDescriptor ( controller, size, -pullIntoDescriptor )

- - - 1. Assert: either _controller_.[[pendingPullIntos]] is empty, or the first element of - _controller_.[[pendingPullIntos]] is _pullIntoDescriptor_. - 1. Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(_controller_). - 1. Set _pullIntoDescriptor_.[[bytesFilled]] to _pullIntoDescriptor_.[[bytesFilled]] + _size_. - - -

ReadableByteStreamControllerFillPullIntoDescriptorFromQueue ( controller, -pullIntoDescriptor )

- - - 1. Let _elementSize_ be _pullIntoDescriptor_.[[elementSize]]. - 1. Let _currentAlignedBytes_ be _pullIntoDescriptor_.[[bytesFilled]] − (_pullIntoDescriptor_.[[bytesFilled]] mod - _elementSize_). - 1. Let _maxBytesToCopy_ be min(_controller_.[[queueTotalSize]], _pullIntoDescriptor_.[[byteLength]] − - _pullIntoDescriptor_.[[bytesFilled]]). - 1. Let _maxBytesFilled_ be _pullIntoDescriptor_.[[bytesFilled]] + _maxBytesToCopy_. - 1. Let _maxAlignedBytes_ be _maxBytesFilled_ − (_maxBytesFilled_ mod _elementSize_). - 1. Let _totalBytesToCopyRemaining_ be _maxBytesToCopy_. - 1. Let _ready_ be *false*. - 1. If _maxAlignedBytes_ > _currentAlignedBytes_, - 1. Set _totalBytesToCopyRemaining_ to _maxAlignedBytes_ − _pullIntoDescriptor_.[[bytesFilled]]. - 1. Set _ready_ to *true*. - 1. Let _queue_ be _controller_.[[queue]]. - 1. Repeat the following steps while _totalBytesToCopyRemaining_ > *0*, - 1. Let _headOfQueue_ be the first element of _queue_. - 1. Let _bytesToCopy_ be min(_totalBytesToCopyRemaining_, _headOfQueue_.[[byteLength]]). - 1. Let _destStart_ be _pullIntoDescriptor_.[[byteOffset]] + _pullIntoDescriptor_.[[bytesFilled]]. - 1. Perform ! CopyDataBlockBytes(_pullIntoDescriptor_.[[buffer]].[[ArrayBufferData]], _destStart_, - _headOfQueue_.[[buffer]].[[ArrayBufferData]], _headOfQueue_.[[byteOffset]], _bytesToCopy_). - 1. If _headOfQueue_.[[byteLength]] is _bytesToCopy_, - 1. Remove the first element of _queue_, shifting all other elements downward (so that the second becomes the - first, and so on). - 1. Otherwise, - 1. Set _headOfQueue_.[[byteOffset]] to _headOfQueue_.[[byteOffset]] + _bytesToCopy_. - 1. Set _headOfQueue_.[[byteLength]] to _headOfQueue_.[[byteLength]] − _bytesToCopy_. - 1. Set _controller_.[[queueTotalSize]] to _controller_.[[queueTotalSize]] − _bytesToCopy_. - 1. Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(_controller_, _bytesToCopy_, - _pullIntoDescriptor_). - 1. Set _totalBytesToCopyRemaining_ to _totalBytesToCopyRemaining_ − _bytesToCopy_. - 1. If _ready_ is *false*, - 1. Assert: _controller_.[[queueTotalSize]] is *0*. - 1. Assert: _pullIntoDescriptor_.[[bytesFilled]] > *0*. - 1. Assert: _pullIntoDescriptor_.[[bytesFilled]] < _pullIntoDescriptor_.[[elementSize]]. - 1. Return _ready_. - - -

ReadableByteStreamControllerGetDesiredSize ( controller )

- - - 1. Let _stream_ be _controller_.[[controlledReadableByteStream]]. - 1. Let _state_ be _stream_.[[state]]. - 1. If _state_ is `"errored"`, return *null*. - 1. If _state_ is `"closed"`, return *0*. - 1. Return _controller_.[[strategyHWM]] − _controller_.[[queueTotalSize]]. - - -

ReadableByteStreamControllerHandleQueueDrain ( controller )

- - - 1. Assert: _controller_.[[controlledReadableByteStream]].[[state]] is `"readable"`. - 1. If _controller_.[[queueTotalSize]] is *0* and _controller_.[[closeRequested]] is *true*, - 1. Perform ! ReadableByteStreamControllerClearAlgorithms(_controller_). - 1. Perform ! ReadableStreamClose(_controller_.[[controlledReadableByteStream]]). - 1. Otherwise, - 1. Perform ! ReadableByteStreamControllerCallPullIfNeeded(_controller_). - - -

ReadableByteStreamControllerInvalidateBYOBRequest ( -controller )

- - - 1. If _controller_.[[byobRequest]] is *undefined*, return. - 1. Set _controller_.[[byobRequest]].[[associatedReadableByteStreamController]] to *undefined*. - 1. Set _controller_.[[byobRequest]].[[view]] to *undefined*. - 1. Set _controller_.[[byobRequest]] to *undefined*. - - -

ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue ( controller )

- - - 1. Assert: _controller_.[[closeRequested]] is *false*. - 1. Repeat the following steps while _controller_.[[pendingPullIntos]] is not empty, - 1. If _controller_.[[queueTotalSize]] is *0*, return. - 1. Let _pullIntoDescriptor_ be the first element of _controller_.[[pendingPullIntos]]. - 1. If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(_controller_, _pullIntoDescriptor_) is *true*, - 1. Perform ! ReadableByteStreamControllerShiftPendingPullInto(_controller_). - 1. Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(_controller_.[[controlledReadableByteStream]], - _pullIntoDescriptor_). - - -

ReadableByteStreamControllerPullInto ( controller, view )

- - - 1. Let _stream_ be _controller_.[[controlledReadableByteStream]]. - 1. Let _elementSize_ be 1. - 1. Let _ctor_ be %DataView%. - 1. If _view_ has a [[TypedArrayName]] internal slot (i.e., it is not a `DataView`), - 1. Set _elementSize_ to the element size specified in the typed array constructors table for - _view_.[[TypedArrayName]]. - 1. Set _ctor_ to the constructor specified in the typed array constructors table for - _view_.[[TypedArrayName]]. - 1. Let _byteOffset_ be _view_.[[ByteOffset]]. - 1. Let _byteLength_ be _view_.[[ByteLength]]. - 1. Let _buffer_ be ! TransferArrayBuffer(_view_.[[ViewedArrayBuffer]]). - 1. Let _pullIntoDescriptor_ be Record {[[buffer]]: _buffer_, [[byteOffset]]: _byteOffset_, - [[byteLength]]: _byteLength_, [[bytesFilled]]: *0*, [[elementSize]]: _elementSize_, - [[ctor]]: _ctor_, [[readerType]]: `"byob"`}. - 1. If _controller_.[[pendingPullIntos]] is not empty, - 1. Append _pullIntoDescriptor_ as the last element of _controller_.[[pendingPullIntos]]. - 1. Return ! ReadableStreamAddReadIntoRequest(_stream_). - 1. If _stream_.[[state]] is `"closed"`, - 1. Let _emptyView_ be ! Construct(_ctor_, « _pullIntoDescriptor_.[[buffer]], _pullIntoDescriptor_.[[byteOffset]], *0* »). - 1. Return a promise resolved with ! ReadableStreamCreateReadResult(_emptyView_, *true*, - _stream_.[[reader]].[[forAuthorCode]]). - 1. If _controller_.[[queueTotalSize]] > *0*, - 1. If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(_controller_, _pullIntoDescriptor_) is *true*, - 1. Let _filledView_ be ! ReadableByteStreamControllerConvertPullIntoDescriptor(_pullIntoDescriptor_). - 1. Perform ! ReadableByteStreamControllerHandleQueueDrain(_controller_). - 1. Return a promise resolved with ! ReadableStreamCreateReadResult(_filledView_, *false*, - _stream_.[[reader]].[[forAuthorCode]]). - 1. If _controller_.[[closeRequested]] is *true*, - 1. Let _e_ be a *TypeError* exception. - 1. Perform ! ReadableByteStreamControllerError(_controller_, _e_). - 1. Return a promise rejected with _e_. - 1. Append _pullIntoDescriptor_ as the last element of _controller_.[[pendingPullIntos]]. - 1. Let _promise_ be ! ReadableStreamAddReadIntoRequest(_stream_). - 1. Perform ! ReadableByteStreamControllerCallPullIfNeeded(_controller_). - 1. Return _promise_. - - -

ReadableByteStreamControllerRespond ( controller, bytesWritten )

- - - 1. Let _bytesWritten_ be ? ToNumber(_bytesWritten_). - 1. If ! IsFiniteNonNegativeNumber(_bytesWritten_) is *false*, - 1. Throw a *RangeError* exception. - 1. Assert: _controller_.[[pendingPullIntos]] is not empty. - 1. Perform ? ReadableByteStreamControllerRespondInternal(_controller_, _bytesWritten_). - - -

ReadableByteStreamControllerRespondInClosedState ( controller, -firstDescriptor )

- - - 1. Set _firstDescriptor_.[[buffer]] to ! TransferArrayBuffer(_firstDescriptor_.[[buffer]]). - 1. Assert: _firstDescriptor_.[[bytesFilled]] is *0*. - 1. Let _stream_ be _controller_.[[controlledReadableByteStream]]. - 1. If ! ReadableStreamHasBYOBReader(_stream_) is *true*, - 1. Repeat the following steps while ! ReadableStreamGetNumReadIntoRequests(_stream_) > *0*, - 1. Let _pullIntoDescriptor_ be ! ReadableByteStreamControllerShiftPendingPullInto(_controller_). - 1. Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(_stream_, _pullIntoDescriptor_). - - -

ReadableByteStreamControllerRespondInReadableState ( -controller, bytesWritten, pullIntoDescriptor )

- - - 1. If _pullIntoDescriptor_.[[bytesFilled]] + _bytesWritten_ > _pullIntoDescriptor_.[[byteLength]], throw a - *RangeError* exception. - 1. Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(_controller_, _bytesWritten_, - _pullIntoDescriptor_). - 1. If _pullIntoDescriptor_.[[bytesFilled]] < _pullIntoDescriptor_.[[elementSize]], return. - 1. Perform ! ReadableByteStreamControllerShiftPendingPullInto(_controller_). - 1. Let _remainderSize_ be _pullIntoDescriptor_.[[bytesFilled]] mod _pullIntoDescriptor_.[[elementSize]]. - 1. If _remainderSize_ > *0*, - 1. Let _end_ be _pullIntoDescriptor_.[[byteOffset]] + _pullIntoDescriptor_.[[bytesFilled]]. - 1. Let _remainder_ be ? CloneArrayBuffer(_pullIntoDescriptor_.[[buffer]], _end_ − _remainderSize_, _remainderSize_, - %ArrayBuffer%). - 1. Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(_controller_, _remainder_, *0*, - _remainder_.[[ByteLength]]). - 1. Set _pullIntoDescriptor_.[[buffer]] to ! TransferArrayBuffer(_pullIntoDescriptor_.[[buffer]]). - 1. Set _pullIntoDescriptor_.[[bytesFilled]] to _pullIntoDescriptor_.[[bytesFilled]] − _remainderSize_. - 1. Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(_controller_.[[controlledReadableByteStream]], - _pullIntoDescriptor_). - 1. Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(_controller_). - - -

ReadableByteStreamControllerRespondInternal ( controller, bytesWritten )

- - - 1. Let _firstDescriptor_ be the first element of _controller_.[[pendingPullIntos]]. - 1. Let _stream_ be _controller_.[[controlledReadableByteStream]]. - 1. If _stream_.[[state]] is `"closed"`, - 1. If _bytesWritten_ is not *0*, throw a *TypeError* exception. - 1. Perform ! ReadableByteStreamControllerRespondInClosedState(_controller_, _firstDescriptor_). - 1. Otherwise, - 1. Assert: _stream_.[[state]] is `"readable"`. - 1. Perform ? ReadableByteStreamControllerRespondInReadableState(_controller_, _bytesWritten_, _firstDescriptor_). - 1. Perform ! ReadableByteStreamControllerCallPullIfNeeded(_controller_). - - -

ReadableByteStreamControllerRespondWithNewView ( controller, view )

- - - 1. Assert: _controller_.[[pendingPullIntos]] is not empty. - 1. Let _firstDescriptor_ be the first element of _controller_.[[pendingPullIntos]]. - 1. If _firstDescriptor_.[[byteOffset]] + _firstDescriptor_.[[bytesFilled]] is not _view_.[[ByteOffset]], throw a - *RangeError* exception. - 1. If _firstDescriptor_.[[byteLength]] is not _view_.[[ByteLength]], throw a *RangeError* exception. - 1. Set _firstDescriptor_.[[buffer]] to _view_.[[ViewedArrayBuffer]]. - 1. Perform ? ReadableByteStreamControllerRespondInternal(_controller_, _view_.[[ByteLength]]). - - -

ReadableByteStreamControllerShiftPendingPullInto ( controller )

- - - 1. Let _descriptor_ be the first element of _controller_.[[pendingPullIntos]]. - 1. Remove _descriptor_ from _controller_.[[pendingPullIntos]], shifting all other elements downward (so that the - second becomes the first, and so on). - 1. Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(_controller_). - 1. Return _descriptor_. - - -

ReadableByteStreamControllerShouldCallPull ( controller )

- - - 1. Let _stream_ be _controller_.[[controlledReadableByteStream]]. - 1. If _stream_.[[state]] is not `"readable"`, return *false*. - 1. If _controller_.[[closeRequested]] is *true*, return *false*. - 1. If _controller_.[[started]] is *false*, return *false*. - 1. If ! ReadableStreamHasDefaultReader(_stream_) is *true* and ! ReadableStreamGetNumReadRequests(_stream_) > *0*, - return *true*. - 1. If ! ReadableStreamHasBYOBReader(_stream_) is *true* and ! ReadableStreamGetNumReadIntoRequests(_stream_) > *0*, - return *true*. - 1. Let _desiredSize_ be ! ReadableByteStreamControllerGetDesiredSize(_controller_). - 1. Assert: _desiredSize_ is not *null*. - 1. If _desiredSize_ > *0*, return *true*. - 1. Return *false*. - - -

SetUpReadableByteStreamController ( stream, controller, startAlgorithm, -pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize )

- - - 1. Assert: _stream_.[[readableStreamController]] is *undefined*. - 1. If _autoAllocateChunkSize_ is not *undefined*, - 1. Assert: ! IsInteger(_autoAllocateChunkSize_) is *true*. - 1. Assert: _autoAllocateChunkSize_ is positive. - 1. Set _controller_.[[controlledReadableByteStream]] to _stream_. - 1. Set _controller_.[[pullAgain]] and _controller_.[[pulling]] to *false*. - 1. Set _controller_.[[byobRequest]] to *undefined*. - 1. Perform ! ResetQueue(_controller_). - 1. Set _controller_.[[closeRequested]] and _controller_.[[started]] to *false*. - 1. Set _controller_.[[strategyHWM]] to ? ValidateAndNormalizeHighWaterMark(_highWaterMark_). - 1. Set _controller_.[[pullAlgorithm]] to _pullAlgorithm_. - 1. Set _controller_.[[cancelAlgorithm]] to _cancelAlgorithm_. - 1. Set _controller_.[[autoAllocateChunkSize]] to _autoAllocateChunkSize_. - 1. Set _controller_.[[pendingPullIntos]] to a new empty List. - 1. Set _stream_.[[readableStreamController]] to _controller_. - 1. Let _startResult_ be the result of performing _startAlgorithm_. - 1. Let _startPromise_ be a promise resolved with _startResult_. - 1. Upon fulfillment of _startPromise_, - 1. Set _controller_.[[started]] to *true*. - 1. Assert: _controller_.[[pulling]] is *false*. - 1. Assert: _controller_.[[pullAgain]] is *false*. - 1. Perform ! ReadableByteStreamControllerCallPullIfNeeded(_controller_). - 1. Upon rejection of _startPromise_ with reason _r_, - 1. Perform ! ReadableByteStreamControllerError(_controller_, _r_). - - -

SetUpReadableByteStreamControllerFromUnderlyingSource ( stream, -underlyingByteSource, highWaterMark )

- - - 1. Assert: _underlyingByteSource_ is not *undefined*. - 1. Let _controller_ be ObjectCreate(the original value of `ReadableByteStreamController`'s `prototype` - property). - 1. Let _startAlgorithm_ be the following steps: - 1. Return ? InvokeOrNoop(_underlyingByteSource_, `"start"`, « _controller_ »). - 1. Let _pullAlgorithm_ be ? CreateAlgorithmFromUnderlyingMethod(_underlyingByteSource_, `"pull"`, *0*, « _controller_ - »). - 1. Let _cancelAlgorithm_ be ? CreateAlgorithmFromUnderlyingMethod(_underlyingByteSource_, `"cancel"`, *1*, « »). - 1. Let _autoAllocateChunkSize_ be ? GetV(_underlyingByteSource_, `"autoAllocateChunkSize"`). - 1. If _autoAllocateChunkSize_ is not *undefined*, - 1. Set _autoAllocateChunkSize_ to ? ToNumber(_autoAllocateChunkSize_). - 1. If ! IsInteger(_autoAllocateChunkSize_) is *false*, or if _autoAllocateChunkSize_ ≤ *0*, throw a *RangeError* - exception. - 1. Perform ? SetUpReadableByteStreamController(_stream_, _controller_, _startAlgorithm_, _pullAlgorithm_, - _cancelAlgorithm_, _highWaterMark_, _autoAllocateChunkSize_). - - -

SetUpReadableStreamBYOBRequest ( request, controller, view )

- - - 1. Assert: ! IsReadableByteStreamController(_controller_) is *true*. - 1. Assert: Type(_view_) is Object. - 1. Assert: _view_ has a [[ViewedArrayBuffer]] internal slot. - 1. Assert: ! IsDetachedBuffer(_view_.[[ViewedArrayBuffer]]) is *false*. - 1. Set _request_.[[associatedReadableByteStreamController]] to _controller_. - 1. Set _request_.[[view]] to _view_. - + 1. If |forAuthorCode| was not passed, set it to false. + 1. Let |reader| be a [=new=] {{ReadableStreamBYOBReader}}. + 1. Perform ? [$SetUpReadableStreamBYOBReader$](|reader|, |stream|). + 1. Set |reader|.\[[forAuthorCode]] to |forAuthorCode|. + 1. Return |reader|. -

Writable streams

+

Other specifications ought to leave |forAuthorCode| as its default value of + false, unless they are planning to directly expose the resulting { value, done } + object to authors. See the note regarding + ReadableStreamCreateReadResult for more information. +

-

Using writable streams

+
+ AcquireReadableStreamDefaultReader(|stream|[, |forAuthorCode|]) is meant to be called + from other specifications that wish to acquire a [=default reader=] for a given stream. It performs + the following steps: -
- The usual way to write to a writable stream is to simply pipe a readable stream to it. - This ensures that backpressure is respected, so that if the writable stream's underlying sink is not - able to accept data as fast as the readable stream can produce it, the readable stream is informed of this and has a - chance to slow down its data production. + 1. If |forAuthorCode| was not passed, set it to false. + 1. Let |reader| be a [=new=] {{ReadableStreamDefaultReader}}. + 1. Perform [$SetUpReadableStreamDefaultReader$](|reader|, |stream|). + 1. Set |reader|.\[[forAuthorCode]] to |forAuthorCode|. + 1. Return |reader|. -

-    readableStream.pipeTo(writableStream)
-      .then(() => console.log("All data successfully written!"))
-      .catch(e => console.error("Something went wrong!", e));
-  
+

The same usage note for the |forAuthorCode| parameter applies here as it does for + [$AcquireReadableStreamBYOBReader$].

-
- You can also write directly to writable streams by acquiring a writer and using its - {{WritableStreamDefaultWriter/write()}} and {{WritableStreamDefaultWriter/close()}} methods. Since writable streams - queue any incoming writes, and take care internally to forward them to the underlying sink in sequence, you can - indiscriminately write to a writable stream without much ceremony: +
+ CreateReadableStream(|startAlgorithm|, |pullAlgorithm|, |cancelAlgorithm|[, + |highWaterMark|, [, |sizeAlgorithm|]]) is meant to be called from other specifications that + wish to create {{ReadableStream}} instances. The |pullAlgorithm| and |cancelAlgorithm| algorithms + must return promises; if supplied, |sizeAlgorithm| must be an algorithm accepting [=chunk=] + objects and returning a number; and if supplied, |highWaterMark| must be a non-negative, non-NaN + number. + + It performs the following steps: + + 1. If |highWaterMark| was not passed, set it to 1. + 1. If |sizeAlgorithm| was not passed, set it to an algorithm that returns 1. + 1. Assert: ! [$IsNonNegativeNumber$](|highWaterMark|) is true. + 1. Let |stream| be a [=new=] {{ReadableStream}}. + 1. Perform ! [$InitializeReadableStream$](|stream|). + 1. Let |controller| be a [=new=] {{ReadableStreamDefaultController}}. + 1. Perform ? [$SetUpReadableStreamDefaultController$](|stream|, |controller|, |startAlgorithm|, + |pullAlgorithm|, |cancelAlgorithm|, |highWaterMark|, |sizeAlgorithm|). + 1. Return |stream|. + +

This abstract operation will throw an exception if and only if the supplied + |startAlgorithm| throws. +

-

-    function writeArrayToStream(array, writableStream) {
-      const writer = writableStream.getWriter();
-      array.forEach(chunk => writer.write(chunk).catch(() => {}));
+
+ CreateReadableByteStream(|startAlgorithm|, |pullAlgorithm|, |cancelAlgorithm|[, + |highWaterMark|, [, |autoAllocateChunkSize|]]) is meant to be called from other + specifications that wish to create {{ReadableStream}} instances that represent [=readable byte + streams=]. The |pullAlgorithm| and |cancelAlgorithm| algorithms must return promises; if supplied, + |highWaterMark| must be a non-negative, non-NaN number, and, if supplied, |autoAllocateChunkSize| + must be a positive integer. + + It performs the following steps: + + 1. If |highWaterMark| was not passed, set it to 0. + 1. If |autoAllocateChunkSize| was not passed, set it to undefined. + 1. Assert: ! [$IsNonNegativeNumber$](|highWaterMark|) is true. + 1. If |autoAllocateChunkSize| is not undefined, + 1. Assert: ! [$IsInteger$](|autoAllocateChunkSize|) is true. + 1. Assert: |autoAllocateChunkSize| is positive. + 1. Let |stream| be a [=new=] {{ReadableStream}}. + 1. Perform ! [$InitializeReadableStream$](|stream|). + 1. Let |controller| be a [=new=] {{ReadableByteStreamController}}. + 1. Perform ? [$SetUpReadableByteStreamController$](|stream|, |controller|, |startAlgorithm|, + |pullAlgorithm|, |cancelAlgorithm|, |highWaterMark|, |autoAllocateChunkSize|). + 1. Return |stream|. + +

This abstract operation will throw an exception if and only if the supplied + |startAlgorithm| throws. +

- return writer.close(); - } +
+ InitializeReadableStream(|stream|) performs the following + steps: - writeArrayToStream([1, 2, 3, 4, 5], writableStream) - .then(() => console.log("All done!")) - .catch(e => console.error("Error with the stream: " + e)); -
+ 1. Set |stream|.\[[state]] to "`readable`". + 1. Set |stream|.\[[reader]] and |stream|.\[[storedError]] to undefined. + 1. Set |stream|.\[[disturbed]] to false. +
+ +
+ IsReadableStreamDisturbed(|stream|) is meant to be called from other specifications + that wish to query whether or not a readable stream has ever been read from or canceled. It + performs the following steps: - Note how we use .catch(() => {}) to suppress any rejections from the - {{WritableStreamDefaultWriter/write()}} method; we'll be notified of any fatal errors via a rejection of the - {{WritableStreamDefaultWriter/close()}} method, and leaving them un-caught would cause potential - {{unhandledrejection}} events and console warnings. + 1. Assert: |stream| [=implements=] {{ReadableStream}}. + 1. Return |stream|.\[[disturbed]].
-
- In the previous example we only paid attention to the success or failure of the entire stream, by looking at the - promise returned by the writer's {{WritableStreamDefaultWriter/close()}} method. That promise will reject if anything - goes wrong with the stream—initializing it, writing to it, or closing it. And it will fulfill once the stream is - successfully closed. Often this is all you care about. +
+ IsReadableStreamLocked(|stream|) is meant to be called from other specifications + that wish to query whether or not a readable stream is [=locked to a reader=]. - However, if you care about the success of writing a specific chunk, you can use the promise returned by the - writer's {{WritableStreamDefaultWriter/write()}} method: + 1. Assert: |stream| [=implements=] {{ReadableStream}}. + 1. If |stream|.\[[reader]] is undefined, return false. + 1. Return true. +
-

-    writer.write("i am a chunk of data")
-      .then(() => console.log("chunk successfully written!"))
-      .catch(e => console.error(e));
-  
+
+ ReadableStreamTee(|stream|, + |cloneForBranch2|) is meant to be called from other specifications that wish to [=tee a + readable stream|tee=] a given readable stream. + + The second argument, |cloneForBranch2|, governs whether or not the data from the original stream + will be cloned (using HTML's [=serializable objects=] framework) before appearing in the second of + the returned branches. This is useful for scenarios where both branches are to be consumed in such + a way that they might otherwise interfere with each other, such as by [=transferable + objects|transferring=] their [=chunks=]. However, it does introduce a noticeable asymmetry between + the two branches, and limits the possible [=chunks=] to serializable ones. [[!HTML]] + +

In this standard ReadableStreamTee is always called with |cloneForBranch2| set to + false; other specifications pass true. + + It performs the following steps: + + 1. Assert: |stream| [=implements=] {{ReadableStream}}. + 1. Assert: |cloneForBranch2| is a boolean. + 1. Let |reader| be ? [$AcquireReadableStreamDefaultReader$](|stream|). + 1. Let |reading| be false. + 1. Let |canceled1| be false. + 1. Let |canceled2| be false. + 1. Let |reason1| be undefined. + 1. Let |reason2| be undefined. + 1. Let |branch1| be undefined. + 1. Let |branch2| be undefined. + 1. Let |cancelPromise| be [=a new promise=]. + 1. Let |pullAlgorithm| be the following steps: + 1. If |reading| is true, return [=a promise resolved with=] undefined. + 1. Set |reading| to true. + 1. Let |readPromise| be the result of [=reacting=] to ! + [$ReadableStreamDefaultReaderRead$](|reader|) with the following fulfillment steps given the + argument |result|: + 1. Set |reading| to false. + 1. Assert: [$Type$](|result|) is Object. + 1. Let |done| be ! [$Get$](|result|, "`done`"). + 1. Assert: [$Type$](|done|) is Boolean. + 1. If |done| is true, + 1. If |canceled1| is false, + 1. Perform ! [$ReadableStreamDefaultControllerClose$](|branch1|.\[[readableStreamController]]). + 1. If |canceled2| is false, + 1. Perform ! + [$ReadableStreamDefaultControllerClose$](|branch2|.\[[readableStreamController]]). + 1. Return. + 1. Let |value| be ! [$Get$](|result|, "`value`"). + 1. Let |value1| and |value2| be |value|. + 1. If |canceled2| is false and |cloneForBranch2| is true, set |value2| to ? + [$StructuredDeserialize$](? [$StructuredSerialize$](|value2|), [=the current Realm=]). + 1. If |canceled1| is false, perform ? + [$ReadableStreamDefaultControllerEnqueue$](|branch1|.\[[readableStreamController]], + |value1|). + 1. If |canceled2| is false, perform ? + ReadableStreamDefaultControllerEnqueue(|branch2|.\[[readableStreamController]], |value2|). + 1. Set |readPromise|.\[[PromiseIsHandled]] to true. + 1. Return [=a promise resolved with=] undefined. + 1. Let |cancel1Algorithm| be the following steps, taking a |reason| argument: + 1. Set |canceled1| to true. + 1. Set |reason1| to |reason|. + 1. If |canceled2| is true, + 1. Let |compositeReason| be ! [$CreateArrayFromList$](« |reason1|, |reason2| »). + 1. Let |cancelResult| be ! [$ReadableStreamCancel$](|stream|, |compositeReason|). + 1. [=Resolve=] |cancelPromise| with |cancelResult|. + 1. Return |cancelPromise|. + 1. Let |cancel2Algorithm| be the following steps, taking a |reason| argument: + 1. Set |canceled2| to true. + 1. Set |reason2| to |reason|. + 1. If |canceled1| is true, + 1. Let |compositeReason| be ! [$CreateArrayFromList$](« |reason1|, |reason2| »). + 1. Let |cancelResult| be ! [$ReadableStreamCancel$](|stream|, |compositeReason|). + 1. [=Resolve=] |cancelPromise| with |cancelResult|. + 1. Return |cancelPromise|. + 1. Let |startAlgorithm| be an algorithm that returns undefined. + 1. Set |branch1| to ! [$CreateReadableStream$](|startAlgorithm|, |pullAlgorithm|, + |cancel1Algorithm|). + 1. Set |branch2| to ! [$CreateReadableStream$](|startAlgorithm|, |pullAlgorithm|, + |cancel2Algorithm|). + 1. [=Upon rejection=] of |reader|.\[[closedPromise]] with reason |r|, + 1. Perform ! [$ReadableStreamDefaultControllerError$](|branch1|.\[[readableStreamController]], + |r|). + 1. Perform ! [$ReadableStreamDefaultControllerError$](|branch2|.\[[readableStreamController]], + |r|). + 1. Return « |branch1|, |branch2| ». +

- What "success" means is up to a given stream instance (or more precisely, its underlying sink) to decide. For - example, for a file stream it could simply mean that the OS has accepted the write, and not necessarily that the - chunk has been flushed to disk. Some streams might not be able to give such a signal at all, in which case the - returned promise will fulfill immediately. +
+ ReadableStreamPipeTo(|source|, |dest|, |preventClose|, |preventAbort|, |preventCancel|[, + |signal|]) is meant to be called from other specifications that wish to [=piping|pipe=] a + given readable stream to a destination [=writable stream=]. It performs the following steps: + + 1. Assert: |source| [=implements=] {{ReadableStream}}. + 1. Assert: |dest| [=implements=] {{WritableStream}}. + 1. Assert: |preventClose|, |preventAbort|, and |preventCancel| are all booleans. + 1. If |signal| is not given, let |signal| be undefined. + 1. Assert: either |signal| is undefined, or |signal| [=implements=] {{AbortSignal}}. + 1. Assert: ! [$IsReadableStreamLocked$](|source|) is false. + 1. Assert: ! [$IsWritableStreamLocked$](|dest|) is false. + 1. If |source|.\[[readableStreamController]] [=implements=] {{ReadableByteStreamController}}, let + |reader| be either ! [$AcquireReadableStreamBYOBReader$](|source|) or ! + [$AcquireReadableStreamDefaultReader$](|source|), at the user agent's discretion. + 1. Otherwise, let |reader| be ! [$AcquireReadableStreamDefaultReader$](|source|). + 1. Let |writer| be ! [$AcquireWritableStreamDefaultWriter$](|dest|). + 1. Set |source|.\[[disturbed]] to true. + 1. Let |shuttingDown| be false. + 1. Let |promise| be [=a new promise=]. + 1. If |signal| is not undefined, + 1. Let |abortAlgorithm| be the following steps: + 1. Let |error| be a new "{{AbortError}}" {{DOMException}}. + 1. Let |actions| be an empty [=ordered set=]. + 1. If |preventAbort| is false, [=set/append=] the following action to |actions|: + 1. If |dest|.\[[state]] is "`writable`", return ! [$WritableStreamAbort$](|dest|, |error|). + 1. Otherwise, return [=a promise resolved with=] undefined. + 1. If |preventCancel| is false, [=set/append=] the following action action to |actions|: + 1. If |source|.\[[state]] is "`readable`", return ! [$ReadableStreamCancel$](|source|, |error|). + 1. Otherwise, return [=a promise resolved with=] undefined. + 1. [=Shutdown with an action=] consisting of [=getting a promise to wait for all=] of the actions + in |actions|, and with |error|. + 1. If |signal|'s [=AbortSignal/aborted flag=] is set, perform |abortAlgorithm| and return + |promise|. + 1. [=AbortSignal/Add=] |abortAlgorithm| to |signal|. + 1. [=In parallel=] but not really; see #905, using |reader| and + |writer|, read all [=chunks=] from |source| and write them to |dest|. Due to the locking + provided by the reader and writer, the exact manner in which this happens is not observable to + author code, and so there is flexibility in how this is done. The following constraints apply + regardless of the exact algorithm used: + * Public API must not be used: while reading or writing, or performing any of + the operations below, the JavaScript-modifiable reader, writer, and stream APIs (i.e. methods + on the appropriate prototypes) must not be used. Instead, the streams must be manipulated + directly. + * Backpressure must be enforced: + * While [$WritableStreamDefaultWriterGetDesiredSize$](|writer|) is ≤ 0 or is null, the user + agent must not read from |reader|. + * If |reader| is a [=BYOB reader=], [$WritableStreamDefaultWriterGetDesiredSize$](|writer|) + should be used as a basis to determine the size of the chunks read from |reader|. +

It's frequently inefficient to read chunks that are too small or too large. + Other information might be factored in to determine the optimal chunk size. + * Reads or writes should not be delayed for reasons other than these backpressure signals. +

An implementation that waits for each write + to successfully complete before proceeding to the next read/write operation violates this + recommendation. In doing so, such an implementation makes the [=internal queue=] of |dest| + useless, as it ensures |dest| always contains at most one queued [=chunk=]. + * Shutdown must stop activity: if |shuttingDown| becomes true, the user agent + must not initiate further reads from |reader|, and must only perform writes of already-read + [=chunks=], as described below. In particular, the user agent must check the below conditions + before performing any reads or writes, since they might lead to immediate shutdown. + * Error and close states must be propagated: the following conditions must be + applied in order. + 1. Errors must be propagated forward: if |source|.\[[state]] is or becomes + "`errored`", then + 1. If |preventAbort| is false, [=shutdown with an action=] of ! [$WritableStreamAbort$](|dest|, + |source|.\[[storedError]]) and with |source|.\[[storedError]]. + 1. Otherwise, [=shutdown=] with |source|.\[[storedError]]. + 1. Errors must be propagated backward: if |dest|.\[[state]] is or becomes + "`errored`", then + 1. If |preventCancel| is false, [=shutdown with an action=] of ! + [$ReadableStreamCancel$](|source|, |dest|.\[[storedError]]) and with |dest|.\[[storedError]]. + 1. Otherwise, [=shutdown=] with |dest|.\[[storedError]]. + 1. Closing must be propagated forward: if |source|.\[[state]] is or becomes + "`closed`", then + 1. If |preventClose| is false, [=shutdown with an action=] of ! + [$WritableStreamDefaultWriterCloseWithErrorPropagation$](|writer|). + 1. Otherwise, [=shutdown=]. + 1. Closing must be propagated backward: if ! + [$WritableStreamCloseQueuedOrInFlight$](|dest|) is true or |dest|.\[[state]] is "`closed`", + then + 1. Assert: no [=chunks=] have been read or written. + 1. Let |destClosed| be a new {{TypeError}}. + 1. If |preventCancel| is false, [=shutdown with an action=] of ! + [$ReadableStreamCancel$](|source|, |destClosed|) and with |destClosed|. + 1. Otherwise, [=shutdown=] with |destClosed|. + * Shutdown with an action: if any of the + above requirements ask to shutdown with an action |action|, optionally with an error + |originalError|, then: + 1. If |shuttingDown| is true, abort these substeps. + 1. Set |shuttingDown| to true. + 1. If |dest|.\[[state]] is "`writable`" and ! [$WritableStreamCloseQueuedOrInFlight$](|dest|) is + false, + 1. If any [=chunks=] have been read but not yet written, write them to |dest|. + 1. Wait until every [=chunk=] that has been read has been written (i.e. the corresponding + promises have settled). + 1. Let |p| be the result of performing |action|. + 1. [=Upon fulfillment=] of |p|, [=finalize=], passing along |originalError| if it was given. + 1. [=Upon rejection=] of |p| with reason |newError|, [=finalize=] with |newError|. + * Shutdown: if any of the above requirements or steps + ask to shutdown, optionally with an error |error|, then: + 1. If |shuttingDown| is true, abort these substeps. + 1. Set |shuttingDown| to true. + 1. If |dest|.\[[state]] is "`writable`" and ! [$WritableStreamCloseQueuedOrInFlight$](|dest|) is + false, + 1. If any [=chunks=] have been read but not yet written, write them to |dest|. + 1. Wait until every [=chunk=] that has been read has been written (i.e. the corresponding + promises have settled). + 1. [=Finalize=], passing along |error| if it was given. + * Finalize: both forms of shutdown will eventually ask + to finalize, optionally with an error |error|, which means to perform the following steps: + 1. Perform ! [$WritableStreamDefaultWriterRelease$](|writer|). + 1. Perform ! [$ReadableStreamReaderGenericRelease$](|reader|). + 1. If |signal| is not undefined, [=AbortSignal/remove=] |abortAlgorithm| from |signal|. + 1. If |error| was given, [=reject=] |promise| with |error|. + 1. Otherwise, [=resolve=] |promise| with undefined. + 1. Return |promise|.

-
- The {{WritableStreamDefaultWriter/desiredSize}} and {{WritableStreamDefaultWriter/ready}} properties of writable - stream writers allow producers to more precisely respond to flow control signals from the stream, to keep - memory usage below the stream's specified high water mark. The following example writes an infinite sequence of - random bytes to a stream, using {{WritableStreamDefaultWriter/desiredSize}} to determine how many bytes to generate at - a given time, and using {{WritableStreamDefaultWriter/ready}} to wait for the backpressure to subside. +

Various abstract operations performed here include object creation (often of +promises), which usually would require specifying a realm for the created object. However, because +of the locking, none of these objects can be observed by author code. As such, the realm used to +create them does not matter. -


-  async function writeRandomBytesForever(writableStream) {
-    const writer = writableStream.getWriter();
+

Interfacing with controllers

- while (true) { - await writer.ready; +In terms of specification factoring, the way that the {{ReadableStream}} class encapsulates the +behavior of both simple readable streams and [=readable byte streams=] into a single class is by +centralizing most of the potentially-varying logic inside the two controller classes, +{{ReadableStreamDefaultController}} and {{ReadableByteStreamController}}. Those classes define most +of the stateful internal slots and abstract operations for how a stream's [=internal queue=] is +managed and how it interfaces with its [=underlying source=] or [=underlying byte source=]. - const bytes = new Uint8Array(writer.desiredSize); - crypto.getRandomValues(bytes); +Each controller class defines two internal methods, which are called by the {{ReadableStream}} +algorithms: - // Purposefully don't await; awaiting writer.ready is enough. - writer.write(bytes).catch(() => {}); - } - } +
+
\[[CancelSteps]](reason) +
The controller's steps that run in reaction to the stream being [=cancel a readable + stream|canceled=], used to clean up the state stored in the controller and inform the + [=underlying source=]. + +
\[[PullSteps]]() +
The controller's steps that run when a [=default reader=] is read from, used to pull from the + controller any queued [=chunks=], or pull from the [=underlying source=] to get more chunks. +
+ +(These are defined as internal methods, instead of as abstract operations, so that they can be +called polymorphically by the {{ReadableStream}} algorithms, without having to branch on which type +of controller is present.) + +The rest of this section concerns abstract operations that go in the other direction: they are +used by the controller implementations to affect their associated {{ReadableStream}} object. This +translates internal state changes of the controller into developer-facing results visible through +the {{ReadableStream}}'s public API. + +
+ ReadableStreamAddReadIntoRequest(|stream|) + performs the following steps: + + 1. Assert: |stream|.\[[reader]] [=implements=] {{ReadableStreamBYOBReader}}. + 1. Assert: |stream|.\[[state]] is "`readable"` or `"closed`". + 1. Let |promise| be [=a new promise=]. + 1. Let |readIntoRequest| be Record {\[[promise]]: |promise|}. + 1. [=list/Append=] |readIntoRequest| to |stream|.\[[reader]].\[[readIntoRequests]]. + 1. Return |promise|. +
- writeRandomBytesForever(myWritableStream).catch(e => console.error("Something broke", e)); -
+
+ ReadableStreamAddReadRequest(|stream|) + performs the following steps: + + 1. Assert: |stream|.\[[reader]] [=implements=] {{ReadableStreamDefaultReader}}. + 1. Assert: |stream|.\[[state]] is "`readable"`. + 1. Let |promise| be [=a new promise=]. + 1. Let |readIntoRequest| be Record {\[[promise]]: |promise|}. + 1. [=list/Append=] |readIntoRequest| to |stream|.\[[reader]].\[[readRequests]]. + 1. Return |promise|. +
- Note how we don't await the promise returned by {{WritableStreamDefaultWriter/write()}}; this would be - redundant with awaiting the {{WritableStreamDefaultWriter/ready}} promise. Additionally, similar to a previous example, we use the .catch(() => {}) pattern on the - promises returned by {{WritableStreamDefaultWriter/write()}}; in this case we'll be notified about any failures - awaiting the {{WritableStreamDefaultWriter/ready}} promise. +
+ ReadableStreamCancel(|stream|, |reason|) performs the following steps: + + 1. Set |stream|.\[[disturbed]] to true. + 1. If |stream|.\[[state]] is "`closed`", return [=a promise resolved with=] undefined. + 1. If |stream|.\[[state]] is "`errored`", return [=a promise rejected with=] + |stream|.\[[storedError]]. + 1. Perform ! [$ReadableStreamClose$](|stream|). + 1. Let |sourceCancelPromise| be ! + |stream|.\[[readableStreamController]].[$ReadableStreamController/[[CancelSteps]]$](|reason|). + 1. Return the result of [=reacting=] to |sourceCancelPromise| with a fulfillment step that returns + undefined.
-
- To further emphasize how it's a bad idea to await the promise returned by - {{WritableStreamDefaultWriter/write()}}, consider a modification of the above example, where we continue to use the - {{WritableStreamDefaultWriter}} interface directly, but we don't control how many bytes we have to write at a given - time. In that case, the backpressure-respecting code looks the same: +
+ ReadableStreamClose(|stream|) performs the following steps: + + 1. Assert: |stream|.\[[state]] is "`readable`". + 1. Set |stream|.\[[state]] to "`closed`". + 1. Let |reader| be |stream|.\[[reader]]. + 1. If |reader| is undefined, return. + 1. If ! IsReadableStreamDefaultReader(|reader|) is true, + 1. [=list/For each=] |readRequest| of |reader|.\[[readRequests]], + 1. [=Resolve=] |readRequest|.\[[promise]] with ! [$ReadableStreamCreateReadResult$](undefined, + true, |reader|.\[[forAuthorCode]]). + 1. Set |reader|.\[[readRequests]] to an empty [=list=]. + 1. [=Resolve=] |reader|.\[[closedPromise]] with undefined. + +

The case where |stream|.\[[state]] is "`closed`", but |stream|.\[[closeRequested]] + is false, will happen if the stream was closed without its controller's close method ever being + called: i.e., if the stream was closed by a call to {{ReadableStream/cancel(reason)}}. In this + case we allow the controller's close() method to be called and silently do nothing, + since the cancelation was outside the control of the underlying source. +

-

-  async function writeSuppliedBytesForever(writableStream, getBytes) {
-    const writer = writableStream.getWriter();
+
+ ReadableStreamCreateReadResult(|value|, |done|, + |forAuthorCode|) performs the following steps: + + 1. Let |prototype| be null. + 1. If |forAuthorCode| is true, set |prototype| to {{%ObjectPrototype%}}. + 1. Assert: [$Type$](|done|) is Boolean. + 1. Let |obj| be [$OrdinaryObjectCreate$](|prototype|). + 1. Perform [$CreateDataProperty$](|obj|, "`value`", |value|). + 1. Perform [$CreateDataProperty$](|obj|, "`done`", |done|). + 1. Return |obj|. + +
+ When |forAuthorCode| is true, this abstract operation gives the same result as + [$CreateIterResultObject$](|value|, |done|). This provides the expected semantics when the object + is to be returned from the {{ReadableStreamDefaultReader/read()|defaultReader.read()}} or + {{ReadableStreamBYOBReader/read()|byobReader.read()}} methods. - while (true) { - await writer.ready; + However, resolving promises with such objects will unavoidably result in an access to + Object.prototype.then. For internal use, particularly in {{ReadableStream/pipeTo()}} + and in other specifications, it is important that reads not be observable by author code—even if + that author code has tampered with + Object.prototype. For this reason, a false value of |forAuthorCode| results in an + object with a null prototype, keeping promise resolution unobservable. + + The underlying issue here is that reading from streams always uses promises for { value, + done } objects, even in specifications. Although it is conceivable we could rephrase all + of the internal algorithms to not use promises and note use JavaScript objects, and instead only + package up the results into promise-for-{ value, done } when a read() + method is called, this would be a large undertaking, which we have not done. See + whatwg/infra#181 for more background on + this subject. +
+
- const bytes = getBytes(); - writer.write(bytes).catch(() => {}); - } - } -
+
+ ReadableStreamError(|stream|, + |e|) performs the following steps: + + 1. Assert: |stream|.\[[reader]] [=implements=] {{ReadableStreamDefaultReader}}. + 1. Assert: |stream|.\[[state]] is "`readable`". + 1. Set |stream|.\[[state]] to "`errored`". + 1. Set |stream|.\[[storedError]] to |e|. + 1. Let |reader| be |stream|.\[[reader]]. + 1. If |reader| is undefined, return. + 1. If |reader| [=implements=] {{ReadableStreamDefaultReader}}, + 1. [=list/For each=] |readRequest| of |reader|.\[[readRequests]], + 1. [=Reject=] |readRequest|.\[[promise]] with |e|. + 1. Set |reader|.\[[readRequests]] to a new empty [=list=]. + 1. Otherwise, + 1. Assert: |reader| [=implements=] {{ReadableStreamBYOBReader}}. + 1. [=list/For each=] |readIntoRequest| of |reader|.\[[readIntoRequests]], + 1. [=Reject=] |readIntoRequest|.\[[promise]] with |e|. + 1. Set |reader|.\[[readIntoRequests]] to a new empty [=list=]. + 1. [=Reject=] |reader|.\[[closedPromise]] with |e|. + 1. Set |reader|.\[[closedPromise]].\[[PromiseIsHandled]] to true. +
- Unlike the previous example, where—because we were always writing exactly - {{WritableStreamDefaultWriter/desiredSize|writer.desiredSize}} bytes each time—the - {{WritableStreamDefaultWriter/write()}} and {{WritableStreamDefaultWriter/ready}} promises were synchronized, in this - case it's quite possible that the {{WritableStreamDefaultWriter/ready}} promise fulfills before the one returned by - {{WritableStreamDefaultWriter/write()}} does. Remember, the {{WritableStreamDefaultWriter/ready}} promise fulfills - when the desired size becomes positive, which might be - before the write succeeds (especially in cases with a larger high water mark). +
+ ReadableStreamFulfillReadIntoRequest(|stream|, + |chunk|, |done|) performs the following steps: - In other words, awaiting the return value of {{WritableStreamDefaultWriter/write()}} means you never - queue up writes in the stream's internal queue, instead only executing a write after the previous one succeeds, - which can result in low throughput. + 1. Let |reader| be |stream|.\[[reader]]. + 1. Let |readIntoRequest| be |reader|.\[[readIntoRequests]][0]. + 1. [=list/Remove=] |readIntoRequest| from |reader|.\[[readIntoRequests]]. + 1. [=Resolve=] |readIntoRequest|.\[[promise]] with ! [$ReadableStreamCreateReadResult$](|chunk|, + |done|, |reader|.\[[forAuthorCode]]).
-

Class WritableStream

+
+ ReadableStreamFulfillReadRequest(|stream|, |chunk|, + |done|) performs the following steps: -

Class definition

+ 1. Let |reader| be |stream|.\[[reader]]. + 1. Let |readRequest| be |reader|.\[[readRequests]][0]. + 1. [=list/Remove=] |readRequest| from |reader|.\[[readRequests]]. + 1. [=Resolve=] |readRequest|.\[[promise]] with ! [$ReadableStreamCreateReadResult$](|chunk|, + |done|, |reader|.\[[forAuthorCode]]). +
-
+
+ ReadableStreamGetNumReadIntoRequests(|stream|) + performs the following steps: -This section is non-normative. + 1. Return |stream|.\[[reader]].\[[readIntoRequests]]'s [=list/size=]. +
+ +
+ ReadableStreamGetNumReadRequests(|stream|) + performs the following steps: -If one were to write the {{WritableStream}} class in something close to the syntax of [[!ECMASCRIPT]], it would look -like + 1. Return |stream|.\[[reader]].\[[readRequests]]'s [=list/size=]. +
-

-  class WritableStream {
-    constructor(underlyingSink = {}, strategy = {})
+
+ ReadableStreamHasBYOBReader(|stream|) performs the + following steps: - get locked() + 1. Let |reader| be |stream|.\[[reader]]. + 1. If |reader| is undefined, return false. + 1. If |reader| [=implements=] {{ReadableStreamBYOBReader}}, return true. + 1. Return false. +
- abort(reason) - close() - getWriter() - } -
+
+ ReadableStreamHasDefaultReader(|stream|) performs the + following steps: + 1. Let |reader| be |stream|.\[[reader]]. + 1. If |reader| is undefined, return false. + 1. If |reader| [=implements=] {{ReadableStreamDefaultReader}}, return true. + 1. Return false.
-

Internal slots

+

Readers

-Instances of {{WritableStream}} are created with the internal slots described in the following table: +The following abstract operations support the implementation and manipulation of +{{ReadableStreamDefaultReader}} and {{ReadableStreamBYOBReader}} instances. - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Internal SlotDescription (non-normative)
\[[backpressure]] - The backpressure signal set by the controller -
\[[closeRequest]] - The promise returned from the writer {{WritableStreamDefaultWriter/close()}} method -
\[[inFlightWriteRequest]] - A slot set to the promise for the current in-flight write operation while the - underlying sink's write algorithm is executing and has not yet fulfilled, used to prevent reentrant calls -
\[[inFlightCloseRequest]] - A slot set to the promise for the current in-flight close operation while the - underlying sink's close algorithm is executing and has not yet fulfilled, used to prevent the - {{WritableStreamDefaultWriter/abort()}} method from interrupting close -
\[[pendingAbortRequest]] - A Record containing the promise returned from - {{WritableStreamDefaultWriter/abort()}} and the reason passed to - {{WritableStreamDefaultWriter/abort()}} -
\[[state]] - A string containing the stream's current state, used internally; one of - "writable", "closed", "erroring", or "errored" -
\[[storedError]] - A value indicating how the stream failed, to be given as a failure reason or exception - when trying to operate on the stream while in the "errored" state -
\[[writableStreamController]] - A {{WritableStreamDefaultController}} created with the ability to control the state and - queue of this stream; also used for the IsWritableStream brand check -
\[[writer]] - A {{WritableStreamDefaultWriter}} instance, if the stream is locked to a writer, or - undefined if it is not -
\[[writeRequests]] - A List of promises representing the stream's internal queue of write requests not yet - processed by the underlying sink -
+
+ ReadableStreamReaderGenericCancel(|reader|, + |reason|) performs the following steps: -

- The \[[inFlightCloseRequest]] slot and \[[closeRequest]] slot are mutually exclusive. Similarly, no element will be - removed from \[[writeRequests]] while \[[inFlightWriteRequest]] is not undefined. Implementations - can optimize storage for these slots based on these invariants. -

+ 1. Let |stream| be |reader|.\[[ownerReadableStream]]. + 1. Assert: |stream| is not undefined. + 1. Return ! [$ReadableStreamCancel$](|stream|, |reason|). +
-

new -WritableStream(underlyingSink = {}, strategy = {})

+
+ ReadableStreamReaderGenericInitialize(|reader|, + |stream|) performs the following steps: + + 1. Set |reader|.\[[forAuthorCode]] to true. + 1. Set |reader|.\[[ownerReadableStream]] to |stream|. + 1. Set |stream|.\[[reader]] to |reader|. + 1. If |stream|.\[[state]] is "`readable`", + 1. Set |reader|.\[[closedPromise]] to [=a new promise=]. + 1. Otherwise, if |stream|.\[[state]] is "`closed`", + 1. Set |reader|.\[[closedPromise]] to [=a promise resolved with=] undefined. + 1. Otherwise, + 1. Assert: |stream|.\[[state]] is "`errored`". + 1. Set |reader|.\[[closedPromise]] to [=a promise rejected with=] |stream|.\[[storedError]]. + 1. Set |reader|.\[[closedPromise]].\[[PromiseIsHandled]] to true. +
-
- The underlyingSink argument represents the underlying sink, as described in - [[#underlying-sink-api]]. - - The strategy argument represents the stream's queuing strategy, as described in [[#qs-api]]. If it - is not provided, the default behavior will be the same as a {{CountQueuingStrategy}} with a high water mark - of 1. -
- - - 1. Perform ! InitializeWritableStream(_this_). - 1. Let _size_ be ? GetV(_strategy_, `"size"`). - 1. Let _highWaterMark_ be ? GetV(_strategy_, `"highWaterMark"`). - 1. Let _type_ be ? GetV(_underlyingSink_, `"type"`). - 1. If _type_ is not *undefined*, throw a *RangeError* exception.

This is to allow us to add new - potential types in the future, without backward-compatibility concerns.

- 1. Let _sizeAlgorithm_ be ? MakeSizeAlgorithmFromSizeFunction(_size_). - 1. If _highWaterMark_ is *undefined*, let _highWaterMark_ be *1*. - 1. Set _highWaterMark_ to ? ValidateAndNormalizeHighWaterMark(_highWaterMark_). - 1. Perform ? SetUpWritableStreamDefaultControllerFromUnderlyingSink(*this*, _underlyingSink_, _highWaterMark_, - _sizeAlgorithm_). -
- -

Underlying sink API

+
+ ReadableStreamReaderGenericRelease(|reader|) + performs the following steps: + + 1. Assert: |reader|.\[[ownerReadableStream]] is not undefined. + 1. Assert: |reader|.\[[ownerReadableStream]].\[[reader]] is |reader|. + 1. If |reader|.\[[ownerReadableStream]].\[[state]] is "`readable`", [=reject=] + |reader|.\[[closedPromise]] with a {{TypeError}} exception. + 1. Otherwise, set |reader|.\[[closedPromise]] to [=a promise rejected with=] a {{TypeError}} + exception. + 1. Set |reader|.\[[closedPromise]].\[[PromiseIsHandled]] to true. + 1. Set |reader|.\[[ownerReadableStream]].\[[reader]] to undefined. + 1. Set |reader|.\[[ownerReadableStream]] to undefined. +
-
+
+ ReadableStreamBYOBReaderRead(|reader|, |view|) performs + the following steps: + + 1. Let |stream| be |reader|.\[[ownerReadableStream]]. + 1. Assert: |stream| is not undefined. + 1. Set |stream|.\[[disturbed]] to true. + 1. If |stream|.\[[state]] is "`errored`", return [=a promise rejected with=] + |stream|.\[[storedError]]. + 1. Return ! [$ReadableByteStreamControllerPullInto$](|stream|.\[[readableStreamController]], + |view|). +
-This section is non-normative. +
+ ReadableStreamDefaultReaderRead(|reader|) performs + the following steps: + + 1. Let |stream| be |reader|.\[[ownerReadableStream]]. + 1. Assert: |stream| is not undefined. + 1. Set |stream|.\[[disturbed]] to true. + 1. If |stream|.\[[state]] is "`closed`", return [=a promise resolved with=] ! + [$ReadableStreamCreateReadResult$](undefined, true, |reader|.\[[forAuthorCode]]). + 1. If |stream|.\[[state]] is "`errored`", return [=a promise rejected with=] + |stream|.\[[storedError]]. + 1. Assert: |stream|.\[[state]] is "`readable`". + 1. Return ! |stream|.\[[readableStreamController]].[$ReadableStreamController/[[PullSteps]]$](). +
-The {{WritableStream()}} constructor accepts as its first argument a JavaScript object representing the underlying -sink. Such objects can contain any of the following properties: +
+ SetUpReadableStreamBYOBReader(|reader|, |stream|) + performs the following steps: -
-
start(controller)
-
-

A function that is called immediately during creation of the {{WritableStream}}.

+ 1. If ! [$IsReadableStreamLocked$](|stream|) is true, throw a {{TypeError}} exception. + 1. If ! |stream|.\[[readableStreamController]] does not [=implement=] + {{ReadableByteStreamController}}, throw a {{TypeError}} exception. + 1. Perform ! [$ReadableStreamReaderGenericInitialize$](|reader|, |stream|). + 1. Set |reader|.\[[readIntoRequests]] to a new empty [=list=]. +
-

Typically this is used to acquire access to the underlying sink resource being represented.

+
+ SetUpReadableStreamDefaultReader(|reader|, + |stream|) performs the following steps: -

If this setup process is asynchronous, it can return a promise to signal success or failure; a rejected promise - will error the stream. Any thrown exceptions will be re-thrown by the {{WritableStream()}} constructor.

- + 1. If ! [$IsReadableStreamLocked$](|stream|) is true, throw a {{TypeError}} exception. + 1. Perform ! [$ReadableStreamReaderGenericInitialize$](|reader|, |stream|). + 1. Set |reader|.\[[readRequests]] to a new empty [=list=]. +
-
write(chunk, controller)
-
-

A function that is called when a new chunk of data is ready to be written to the underlying sink. - The stream implementation guarantees that this function will be called only after previous writes have succeeded, - and never before {{underlying sink/start()}} has succeeded or after {{underlying sink/close()}} or - {{underlying sink/abort()}} have been called.

- -

This function is used to actually send the data to the resource presented by the underlying sink, for - example by calling a lower-level API.

- -

If the process of writing data is asynchronous, and communicates success or failure signals back to its user, - then this function can return a promise to signal success or failure. This promise return value will be communicated - back to the caller of {{WritableStreamDefaultWriter/write()|writer.write()}}, so they can monitor that individual - write. Throwing an exception is treated the same as returning a rejected promise.

- -

Note that such signals are not always available; compare e.g. [[#example-ws-no-backpressure]] with - [[#example-ws-backpressure]]. In such cases, it's best to not return anything.

- -

The promise potentially returned by this function also governs whether the given chunk counts as written for the - purposes of computed the desired size to fill the stream's - internal queue. That is, during the time it takes the promise to settle, - {{WritableStreamDefaultWriter/desiredSize|writer.desiredSize}} will stay at its previous value, only increasing to - signal the desire for more chunks once the write succeeds.

-
- -
close()
-
-

A function that is called after the producer signals, via - {{WritableStreamDefaultWriter/close()|writer.close()}}, that they are done writing chunks to the stream, and - subsequently all queued-up writes have successfully completed.

+

Default controllers

+ +The following abstract operations support the implementation of the +{{ReadableStreamDefaultController}} class. + +
+ ReadableStreamDefaultControllerCallPullIfNeeded(|controller|) + performs the following steps: + + 1. Let |shouldPull| be ! [$ReadableStreamDefaultControllerShouldCallPull$](|controller|). + 1. If |shouldPull| is false, return. + 1. If |controller|.\[[pulling]] is true, + 1. Set |controller|.\[[pullAgain]] to true. + 1. Return. + 1. Assert: |controller|.\[[pullAgain]] is false. + 1. Set |controller|.\[[pulling]] to true. + 1. Let |pullPromise| be the result of performing |controller|.\[[pullAlgorithm]]. + 1. [=Upon fulfillment=] of |pullPromise|, + 1. Set |controller|.\[[pulling]] to false. + 1. If |controller|.\[[pullAgain]] is true, + 1. Set |controller|.\[[pullAgain]] to false. + 1. Perform ! [$ReadableStreamDefaultControllerCallPullIfNeeded$](|controller|). + 1. [=Upon rejection=] of |pullPromise| with reason |e|, + 1. Perform ! [$ReadableStreamDefaultControllerError$](|controller|, |e|). +
-

This function can perform any actions necessary to finalize or flush writes to the underlying sink, and - release access to any held resources.

+
+ ReadableStreamDefaultControllerShouldCallPull(|controller|) + performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If ! [$ReadableStreamDefaultControllerCanCloseOrEnqueue$](|controller|) is false, return false. + 1. If |controller|.\[[started]] is false, return false. + 1. If ! [$IsReadableStreamLocked$](|stream|) is true and ! + [$ReadableStreamGetNumReadRequests$](|stream|) > 0, return true. + 1. Let |desiredSize| be ! [$ReadableStreamDefaultControllerGetDesiredSize$](|controller|). + 1. Assert: |desiredSize| is not null. + 1. If |desiredSize| > 0, return true. + 1. Return false. +
-

If the shutdown process is asynchronous, the function can return a promise to signal success or failure; the - result will be communicated via the return value of the called - {{WritableStreamDefaultWriter/close()|writer.close()}} method. Additionally, a rejected promise will error the - stream, instead of letting it close successfully. Throwing an exception is treated the same as returning a rejected - promise.

-
+
+ ReadableStreamDefaultControllerClearAlgorithms(|controller|) + is called once the stream is closed or errored and the algorithms will not be executed any more. By + removing the algorithm references it permits the [=underlying source=] object to be garbage + collected even if the {{ReadableStream}} itself is still referenced. -
abort(reason)
-
-

A function that is called after the producer signals, via {{WritableStream/abort()|stream.abort()}} or - {{WritableStreamDefaultWriter/abort()|writer.abort()}}, that they wish to abort - the stream. It takes as its argument the same value as was passed to those methods by the producer.

+

This is observable using weak + references. See tc39/proposal-weakrefs#31 for more + detail. -

Writable streams can additionally be aborted under certain conditions during piping; see the definition - of the {{ReadableStream/pipeTo()}} method for more details.

+ It performs the following steps: -

This function can clean up any held resources, much like {{underlying sink/close()}}, but perhaps with some - custom handling.

+ 1. Set |controller|.\[[pullAlgorithm]] to undefined. + 1. Set |controller|.\[[cancelAlgorithm]] to undefined. + 1. Set |controller|.\[[strategySizeAlgorithm]] to undefined. +
-

If the shutdown process is asynchronous, the function can return a promise to signal success or failure; the - result will be communicated via the return value of the called abort() method. Throwing an exception is - treated the same as returning a rejected promise. Regardless, the stream will be errored with a new {{TypeError}} - indicating that it was aborted.

- - +
+ ReadableStreamDefaultControllerClose(|controller|) can be called by other + specifications that wish to close a readable stream, in the same way a developer-created stream + would be closed by its associated controller object. Specifications should not do this to + streams or controllers they did not create. + + It performs the following steps: + + 1. If ! [$ReadableStreamDefaultControllerCanCloseOrEnqueue$](|controller|) is false, return. + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. Set |controller|.\[[closeRequested]] to true. + 1. If |controller|.\[[queue]] [=list/is empty=], + 1. Perform ! [$ReadableStreamDefaultControllerClearAlgorithms$](|controller|). + 1. Perform ! [$ReadableStreamClose$](|stream|). +
-The controller argument passed to {{underlying sink/start()}} and {{underlying sink/write()}} is an -instance of {{WritableStreamDefaultController}}, and has the ability to error the stream. This is mainly used for -bridging the gap with non-promise-based APIs, as seen for example in [[#example-ws-no-backpressure]]. +
+ ReadableStreamDefaultControllerClose(|controller|, |chunk|) can be called by other + specifications that wish to enqueue [=chunks=] in a readable stream, in the same way a developer + would enqueue chunks using the stream's associated controller object. Specifications should + not do this to streams or controllers they did not create. + + It performs the following steps: + + 1. If ! [$ReadableStreamDefaultControllerCanCloseOrEnqueue$](|controller|) is false, return. + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If ! [$IsReadableStreamLocked$](|stream|) is true and ! + [$ReadableStreamGetNumReadRequests$](|stream|) > 0, perform ! + [$ReadableStreamFulfillReadRequest$](|stream|, |chunk|, false). + 1. Otherwise, + 1. Let |result| be the result of performing |controller|.\[[strategySizeAlgorithm]], passing in + |chunk|, and interpreting the result as a [=completion record=]. + 1. If |result| is an abrupt completion, + 1. Perform ! [$ReadableStreamDefaultControllerError$](|controller|, |result|.\[[Value]]). + 1. Return |result|. + 1. Let |chunkSize| be |result|.\[[Value]]. + 1. Let |enqueueResult| be [$EnqueueValueWithSize$](|controller|, |chunk|, |chunkSize|). + 1. If |enqueueResult| is an abrupt completion, + 1. Perform ! [$ReadableStreamDefaultControllerError$](|controller|, |enqueueResult|.\[[Value]]). + 1. Return |enqueueResult|. + 1. Perform ! [$ReadableStreamDefaultControllerCallPullIfNeeded$](|controller|). +
+
+ ReadableStreamDefaultControllerError(|controller|, |e|) can be called by other + specifications that wish to move a readable stream to an errored state, in the same way a + developer would error a stream using its associated controller object. Specifications should + not do this to streams or controllers they did not create. + + It performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If |stream|.\[[state]] is not "`readable`", return. + 1. Perform ! [$ResetQueue$](|controller|). + 1. Perform ! [$ReadableStreamDefaultControllerClearAlgorithms$](|controller|). + 1. Perform ! [$ReadableStreamError$](|stream|, |e|).
-

Properties of the {{WritableStream}} prototype

+
+ ReadableStreamDefaultControllerGetDesiredSize(|controller|) can be called by other + specifications that wish to determine the [=desired size to fill a stream's internal queue|desired + size to fill this stream's internal queue=], similar to how a developer would consult the + {{ReadableStreamDefaultController/desiredSize}} property of the stream's associated controller + object. Specifications should not use this on streams or controllers they did not create. + + It performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. Let |state| be |stream|.\[[state]]. + 1. If |state| is "`errored`", return null. + 1. If |state| is "`closed`", return 0. + 1. Return |controller|.\[[strategyHWM]] − |controller|.\[[queueTotalSize]]. +
-
get locked
+
+ ReadableStreamDefaultControllerHasBackpressure(|controller|) + is used in the implementation of {{TransformStream}}. It performs the following steps: -
- The locked getter returns whether or not the writable stream is locked to a writer. + 1. If ! [$ReadableStreamDefaultControllerShouldCallPull$](|controller|) is true, return false. + 1. Otherwise, return true.
- - 1. If ! IsWritableStream(*this*) is *false*, throw a *TypeError* exception. - 1. Return ! IsWritableStreamLocked(*this*). - +
+ ReadableStreamDefaultControllerCanCloseOrEnqueue(|controller|) + performs the following steps: + + 1. Let |state| be |controller|.\[[controlledReadableStream]].\[[state]]. + 1. If |controller|.\[[closeRequested]] is false and |state| is "`readable`", return true. + 1. Otherwise, return false. + +

The case where |controller|.\[[closeRequested]] is false, but |state| is not + "`readable`", happens when the stream is errored via + {{ReadableStreamDefaultController/error(e)|controller.error()}}, or when it is closed without its + controller's {{ReadableStreamDefaultController/close()|controller.close()}} method ever being + called: e.g., if the stream was closed by a call to + {{ReadableStream/cancel(reason)|stream.cancel()}}. +

-
abort(reason)
+
+ SetUpReadableStreamDefaultController(|stream|, + |controller|, |startAlgorithm|, |pullAlgorithm|, |cancelAlgorithm|, |highWaterMark|, + |sizeAlgorithm|) performs the following steps: + + 1. Assert: |stream|.\[[readableStreamController]] is undefined. + 1. Set |controller|.\[[controlledReadableStream]] to |stream|. + 1. Set |controller|.\[[queue]] and |controller|.\[[queueTotalSize]] to undefined, then perform ! + [$ResetQueue$](|controller|). + 1. Set |controller|.\[[started]], |controller|.\[[closeRequested]], |controller|.\[[pullAgain]], and + |controller|.\[[pulling]] to false. + 1. Set |controller|.\[[strategySizeAlgorithm]] to |sizeAlgorithm| and |controller|.\[[strategyHWM]] + to |highWaterMark|. + 1. Set |controller|.\[[pullAlgorithm]] to |pullAlgorithm|. + 1. Set |controller|.\[[cancelAlgorithm]] to |cancelAlgorithm|. + 1. Set |stream|.\[[readableStreamController]] to |controller|. + 1. Let |startResult| be the result of performing |startAlgorithm|. (This might throw an exception.) + 1. Let |startPromise| be [=a promise resolved with=] |startResult|. + 1. [=Upon fulfillment=] of |startPromise|, + 1. Set |controller|.\[[started]] to true. + 1. Assert: |controller|.\[[pulling]] is false. + 1. Assert: |controller|.\[[pullAgain]] is false. + 1. Perform ! [$ReadableStreamDefaultControllerCallPullIfNeeded$](|controller|). + 1. [=Upon rejection=] of |startPromise| with reason |r|, + 1. Perform ! [$ReadableStreamDefaultControllerError$](|controller|, |r|). +
-
- The abort method aborts the stream, signaling that the producer can - no longer successfully write to the stream and it is to be immediately moved to an errored state, with any queued-up - writes discarded. This will also execute any abort mechanism of the underlying sink. +
+ SetUpReadableStreamDefaultControllerFromUnderlyingSource(|stream|, + |controller|, |underlyingSource|, |underlyingSourceDict|, |highWaterMark|, |sizeAlgorithm|) + performs the following steps: + + 1. Let |controller| be a [=new=] {{ReadableStreamDefaultController}}. + 1. Let |startAlgorithm| be an algorithm that returns undefined. + 1. Let |pullAlgorithm| be an algorithm that returns [=a promise resolved with=] undefined. + 1. Let |cancelAlgorithm| be an algorithm that returns [=a promise resolved with=] undefined. + 1. If |underlyingSourceDict|["{{UnderlyingSource/start}}"] [=map/exists=], then set + |startAlgorithm| to an algorithm which returns the result of [=invoking=] + |underlyingSourceDict|["{{UnderlyingSource/start}}"] with argument list + « |controller| » and [=callback this value=] |underlyingSource|. + 1. If |underlyingSourceDict|["{{UnderlyingSource/pull}}"] [=map/exists=], then set + |pullAlgorithm| to an algorithm which returns the result of [=invoking=] + |underlyingSourceDict|["{{UnderlyingSource/pull}}"] with argument list + « |controller| » and [=callback this value=] |underlyingSource|. + 1. If |underlyingSourceDict|["{{UnderlyingSource/cancel}}"] [=map/exists=], then set + |cancelAlgorithm| to an algorithm which takes an argument |reason| and returns the result of + [=invoking=] |underlyingSourceDict|["{{UnderlyingSource/cancel}}"] with argument list + « |reason| » and [=callback this value=] |underlyingSource|. + 1. Perform ? [$SetUpReadableStreamDefaultController$](|stream|, |controller|, |startAlgorithm|, + |pullAlgorithm|, |cancelAlgorithm|, |highWaterMark|, |sizeAlgorithm|).
- - 1. If ! IsWritableStream(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If ! IsWritableStreamLocked(*this*) is *true*, return a promise rejected with a *TypeError* exception. - 1. Return ! WritableStreamAbort(*this*, _reason_). - +

Byte stream controllers

+ +
+ ReadableByteStreamControllerCallPullIfNeeded(|controller|) + performs the following steps: + + 1. Let |shouldPull| be ! [$ReadableByteStreamControllerShouldCallPull$](|controller|). + 1. If |shouldPull| is false, return. + 1. If |controller|.\[[pulling]] is true, + 1. Set |controller|.\[[pullAgain]] to true. + 1. Return. + 1. Assert: |controller|.\[[pullAgain]] is false. + 1. Set |controller|.\[[pulling]] to true. + 1. Let |pullPromise| be the result of performing |controller|.\[[pullAlgorithm]]. + 1. [=Upon fulfillment=] of |pullPromise|, + 1. Set |controller|.\[[pulling]] to false. + 1. If |controller|.\[[pullAgain]] is true, + 1. Set |controller|.\[[pullAgain]] to false. + 1. Perform ! [$ReadableByteStreamControllerCallPullIfNeeded$](|controller|). + 1. [=Upon rejection=] of |pullPromise| with reason |e|, + 1. Perform ! [$ReadableByteStreamControllerError$](|controller|, |e|). +
-
close()
+
+ ReadableByteStreamControllerClearAlgorithms(|controller|) + is called once the stream is closed or errored and the algorithms will not be executed any more. By + removing the algorithm references it permits the [=underlying byte source=] object to be garbage + collected even if the {{ReadableStream}} itself is still referenced. -
- The close method closes the stream. The underlying sink will finish processing any - previously-written chunks, before invoking its close behavior. During this time any further attempts to write - will fail (without erroring the stream). +

This is observable using weak + references. See tc39/proposal-weakrefs#31 for more + detail. - The method returns a promise that is fulfilled with undefined if all remaining chunks are - successfully written and the stream successfully closes, or rejects if an error is encountered during this process. + It performs the following steps: + + 1. Set |controller|.\[[pullAlgorithm]] to undefined. + 1. Set |controller|.\[[cancelAlgorithm]] to undefined.

- - 1. If ! IsWritableStream(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If ! IsWritableStreamLocked(*this*) is *true*, return a promise rejected with a *TypeError* exception. - 1. If ! WritableStreamCloseQueuedOrInFlight(*this*) is *true*, return a promise rejected with a *TypeError* - exception. - 1. Return ! WritableStreamClose(*this*). - +
+ ReadableByteStreamControllerClearPendingPullIntos(|controller|) + performs the following steps: -
getWriter()
+ 1. Perform ! [$ReadableByteStreamControllerInvalidateBYOBRequest$](|controller|). + 1. Set |controller|.\[[pendingPullIntos]] to a new empty [=list=]. +
-
- The getWriter method creates a writer (an instance of {{WritableStreamDefaultWriter}}) and locks the stream to the new writer. While the stream is locked, no other writer can be - acquired until this one is released. - - This functionality is especially useful for creating abstractions that desire the ability to write to a stream without - interruption or interleaving. By getting a writer for the stream, you can ensure nobody else can write at the same - time, which would cause the resulting written data to be unpredictable and probably useless. -
- - - 1. If ! IsWritableStream(*this*) is *false*, throw a *TypeError* exception. - 1. Return ? AcquireWritableStreamDefaultWriter(*this*). - - -

General writable stream abstract operations

- -The following abstract operations, unlike most in this specification, are meant to be generally useful by other -specifications, instead of just being part of the implementation of this spec's classes. - -

AcquireWritableStreamDefaultWriter ( stream )

- - - 1. Return ? Construct(`WritableStreamDefaultWriter`, « _stream_ »). - - -

CreateWritableStream ( startAlgorithm, -writeAlgorithm, closeAlgorithm, abortAlgorithm [, highWaterMark [, -sizeAlgorithm ] ] )

- -This abstract operation is meant to be called from other specifications that wish to create {{WritableStream}} -instances. The writeAlgorithm, closeAlgorithm and abortAlgorithm algorithms must return -promises; if supplied, sizeAlgorithm must be an algorithm accepting chunk objects and returning a -number; and if supplied, highWaterMark must be a non-negative, non-NaN number. - -

CreateWritableStream throws an exception if and only if the supplied startAlgorithm -throws.

- - - 1. If _highWaterMark_ was not passed, set it to *1*. - 1. If _sizeAlgorithm_ was not passed, set it to an algorithm that returns *1*. - 1. Assert: ! IsNonNegativeNumber(_highWaterMark_) is *true*. - 1. Let _stream_ be ObjectCreate(the original value of `WritableStream`'s `prototype` property). - 1. Perform ! InitializeWritableStream(_stream_). - 1. Let _controller_ be ObjectCreate(the original value of `WritableStreamDefaultController`'s `prototype` - property). - 1. Perform ? SetUpWritableStreamDefaultController(_stream_, _controller_, _startAlgorithm_, _writeAlgorithm_, - _closeAlgorithm_, _abortAlgorithm_, _highWaterMark_, _sizeAlgorithm_). - 1. Return _stream_. - - -

InitializeWritableStream ( stream -)

- - - 1. Set _stream_.[[state]] to `"writable"`. - 1. Set _stream_.[[storedError]], _stream_.[[writer]], _stream_.[[writableStreamController]], - _stream_.[[inFlightWriteRequest]], _stream_.[[closeRequest]], _stream_.[[inFlightCloseRequest]] and - _stream_.[[pendingAbortRequest]] to *undefined*. - 1. Set _stream_.[[writeRequests]] to a new empty List. - 1. Set _stream_.[[backpressure]] to *false*. - - -

IsWritableStream ( x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have a [[writableStreamController]] internal slot, return *false*. - 1. Return *true*. - - -

IsWritableStreamLocked ( stream -)

- -This abstract operation is meant to be called from other specifications that may wish to query whether or not a -writable stream is locked to a writer. - - - 1. Assert: ! IsWritableStream(_stream_) is *true*. - 1. If _stream_.[[writer]] is *undefined*, return *false*. - 1. Return *true*. - - -

WritableStreamAbort ( stream, -reason )

- - - 1. Let _state_ be _stream_.[[state]]. - 1. If _state_ is `"closed"` or `"errored"`, return a promise resolved with *undefined*. - 1. If _stream_.[[pendingAbortRequest]] is not *undefined*, return _stream_.[[pendingAbortRequest]].[[promise]]. - 1. Assert: _state_ is `"writable"` or `"erroring"`. - 1. Let _wasAlreadyErroring_ be *false*. - 1. If _state_ is `"erroring"`, - 1. Set _wasAlreadyErroring_ to *true*. - 1. Set _reason_ to *undefined*. - 1. Let _promise_ be a new promise. - 1. Set _stream_.[[pendingAbortRequest]] to Record {[[promise]]: _promise_, [[reason]]: _reason_, - [[wasAlreadyErroring]]: _wasAlreadyErroring_}. - 1. If _wasAlreadyErroring_ is *false*, perform ! WritableStreamStartErroring(_stream_, _reason_). - 1. Return _promise_. - - -

WritableStreamClose ( stream )

- - - 1. Let _state_ be _stream_.[[state]]. - 1. If _state_ is `"closed"` or `"errored"`, return a promise rejected with a *TypeError* exception. - 1. Assert: _state_ is `"writable"` or `"erroring"`. - 1. Assert: ! WritableStreamCloseQueuedOrInFlight(_stream_) is *false*. - 1. Let _promise_ be a new promise. - 1. Set _stream_.[[closeRequest]] to _promise_. - 1. Let _writer_ be _stream_.[[writer]]. - 1. If _writer_ is not *undefined*, and _stream_.[[backpressure]] is *true*, and _state_ is `"writable"`, - resolve _writer_.[[readyPromise]] with *undefined*. - 1. Perform ! WritableStreamDefaultControllerClose(_stream_.[[writableStreamController]]). - 1. Return _promise_. - - -

Writable stream abstract operations used by controllers

- -To allow future flexibility to add different writable stream behaviors (similar to the distinction between default -readable streams and readable byte streams), much of the internal state of a writable stream is -encapsulated by the {{WritableStreamDefaultController}} class. - -The abstract operations in this section are interfaces that are used by the controller implementation to affect its -associated {{WritableStream}} object, translating the controller's internal state changes into developer-facing results -visible through the {{WritableStream}}'s public API. - -

WritableStreamAddWriteRequest ( -stream )

- - - 1. Assert: ! IsWritableStreamLocked(_stream_) is *true*. - 1. Assert: _stream_.[[state]] is `"writable"`. - 1. Let _promise_ be a new promise. - 1. Append _promise_ as the last element of _stream_.[[writeRequests]]. - 1. Return _promise_. - - -

WritableStreamDealWithRejection ( stream, error )

- - - 1. Let _state_ be _stream_.[[state]]. - 1. If _state_ is `"writable"`, - 1. Perform ! WritableStreamStartErroring(_stream_, _error_). - 1. Return. - 1. Assert: _state_ is `"erroring"`. - 1. Perform ! WritableStreamFinishErroring(_stream_). - - -

WritableStreamStartErroring ( -stream, reason )

- - - 1. Assert: _stream_.[[storedError]] is *undefined*. - 1. Assert: _stream_.[[state]] is `"writable"`. - 1. Let _controller_ be _stream_.[[writableStreamController]]. - 1. Assert: _controller_ is not *undefined*. - 1. Set _stream_.[[state]] to `"erroring"`. - 1. Set _stream_.[[storedError]] to _reason_. - 1. Let _writer_ be _stream_.[[writer]]. - 1. If _writer_ is not *undefined*, perform ! - WritableStreamDefaultWriterEnsureReadyPromiseRejected(_writer_, _reason_). - 1. If ! WritableStreamHasOperationMarkedInFlight(_stream_) is *false* and _controller_.[[started]] is *true*, perform - ! WritableStreamFinishErroring(_stream_). - - -

WritableStreamFinishErroring -( stream )

- - - 1. Assert: _stream_.[[state]] is `"erroring"`. - 1. Assert: ! WritableStreamHasOperationMarkedInFlight(_stream_) is *false*. - 1. Set _stream_.[[state]] to `"errored"`. - 1. Perform ! _stream_.[[writableStreamController]].[[ErrorSteps]](). - 1. Let _storedError_ be _stream_.[[storedError]]. - 1. Repeat for each _writeRequest_ that is an element of _stream_.[[writeRequests]], - 1. Reject _writeRequest_ with _storedError_. - 1. Set _stream_.[[writeRequests]] to an empty List. - 1. If _stream_.[[pendingAbortRequest]] is *undefined*, - 1. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(_stream_). - 1. Return. - 1. Let _abortRequest_ be _stream_.[[pendingAbortRequest]]. - 1. Set _stream_.[[pendingAbortRequest]] to *undefined*. - 1. If _abortRequest_.[[wasAlreadyErroring]] is *true*, - 1. Reject _abortRequest_.[[promise]] with _storedError_. - 1. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(_stream_). - 1. Return. - 1. Let _promise_ be ! stream.[[writableStreamController]].[[AbortSteps]](_abortRequest_.[[reason]]). - 1. Upon fulfillment of _promise_, - 1. Resolve _abortRequest_.[[promise]] with *undefined*. - 1. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(_stream_). - 1. Upon rejection of _promise_ with reason _reason_, - 1. Reject _abortRequest_.[[promise]] with _reason_. - 1. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(_stream_). - - -

WritableStreamFinishInFlightWrite ( stream )

- - - 1. Assert: _stream_.[[inFlightWriteRequest]] is not *undefined*. - 1. Resolve _stream_.[[inFlightWriteRequest]] with *undefined*. - 1. Set _stream_.[[inFlightWriteRequest]] to *undefined*. - - -

WritableStreamFinishInFlightWriteWithError ( stream, error )

- - - 1. Assert: _stream_.[[inFlightWriteRequest]] is not *undefined*. - 1. Reject _stream_.[[inFlightWriteRequest]] with _error_. - 1. Set _stream_.[[inFlightWriteRequest]] to *undefined*. - 1. Assert: _stream_.[[state]] is `"writable"` or `"erroring"`. - 1. Perform ! WritableStreamDealWithRejection(_stream_, _error_). - - -

WritableStreamFinishInFlightClose ( stream )

- - - 1. Assert: _stream_.[[inFlightCloseRequest]] is not *undefined*. - 1. Resolve _stream_.[[inFlightCloseRequest]] with *undefined*. - 1. Set _stream_.[[inFlightCloseRequest]] to *undefined*. - 1. Let _state_ be _stream_.[[state]]. - 1. Assert: _stream_.[[state]] is `"writable"` or `"erroring"`. - 1. If _state_ is `"erroring"`, - 1. Set _stream_.[[storedError]] to *undefined*. - 1. If _stream_.[[pendingAbortRequest]] is not *undefined*, - 1. Resolve _stream_.[[pendingAbortRequest]].[[promise]] with *undefined*. - 1. Set _stream_.[[pendingAbortRequest]] to *undefined*. - 1. Set _stream_.[[state]] to `"closed"`. - 1. Let _writer_ be _stream_.[[writer]]. - 1. If _writer_ is not *undefined*, resolve _writer_.[[closedPromise]] with *undefined*. - 1. Assert: _stream_.[[pendingAbortRequest]] is *undefined*. - 1. Assert: _stream_.[[storedError]] is *undefined*. - - -

WritableStreamFinishInFlightCloseWithError ( stream, error )

- - - 1. Assert: _stream_.[[inFlightCloseRequest]] is not *undefined*. - 1. Reject _stream_.[[inFlightCloseRequest]] with _error_. - 1. Set _stream_.[[inFlightCloseRequest]] to *undefined*. - 1. Assert: _stream_.[[state]] is `"writable"` or `"erroring"`. - 1. If _stream_.[[pendingAbortRequest]] is not *undefined*, - 1. Reject _stream_.[[pendingAbortRequest]].[[promise]] with _error_. - 1. Set _stream_.[[pendingAbortRequest]] to *undefined*. - 1. Perform ! WritableStreamDealWithRejection(_stream_, _error_). - - -

-WritableStreamCloseQueuedOrInFlight ( stream )

- - - 1. If _stream_.[[closeRequest]] is *undefined* and _stream_.[[inFlightCloseRequest]] is *undefined*, return *false*. - 1. Return *true*. - - -

WritableStreamHasOperationMarkedInFlight ( stream )

- - - 1. If _stream_.[[inFlightWriteRequest]] is *undefined* and _controller_.[[inFlightCloseRequest]] is *undefined*, return *false*. - 1. Return *true*. - - -

WritableStreamMarkCloseRequestInFlight ( stream )

- - - 1. Assert: _stream_.[[inFlightCloseRequest]] is *undefined*. - 1. Assert: _stream_.[[closeRequest]] is not *undefined*. - 1. Set _stream_.[[inFlightCloseRequest]] to _stream_.[[closeRequest]]. - 1. Set _stream_.[[closeRequest]] to *undefined*. - - -

WritableStreamMarkFirstWriteRequestInFlight ( stream )

- - - 1. Assert: _stream_.[[inFlightWriteRequest]] is *undefined*. - 1. Assert: _stream_.[[writeRequests]] is not empty. - 1. Let _writeRequest_ be the first element of _stream_.[[writeRequests]]. - 1. Remove _writeRequest_ from _stream_.[[writeRequests]], shifting all other elements downward (so that the second - becomes the first, and so on). - 1. Set _stream_.[[inFlightWriteRequest]] to _writeRequest_. - - -

WritableStreamRejectCloseAndClosedPromiseIfNeeded ( -stream )

- - - 1. Assert: _stream_.[[state]] is `"errored"`. - 1. If _stream_.[[closeRequest]] is not *undefined*, - 1. Assert: _stream_.[[inFlightCloseRequest]] is *undefined*. - 1. Reject _stream_.[[closeRequest]] with _stream_.[[storedError]]. - 1. Set _stream_.[[closeRequest]] to *undefined*. - 1. Let _writer_ be _stream_.[[writer]]. - 1. If _writer_ is not *undefined*, - 1. Reject _writer_.[[closedPromise]] with _stream_.[[storedError]]. - 1. Set _writer_.[[closedPromise]].[[PromiseIsHandled]] to *true*. - - -

WritableStreamUpdateBackpressure ( stream, backpressure )

- - - 1. Assert: _stream_.[[state]] is `"writable"`. - 1. Assert: ! WritableStreamCloseQueuedOrInFlight(_stream_) is *false*. - 1. Let _writer_ be _stream_.[[writer]]. - 1. If _writer_ is not *undefined* and _backpressure_ is not _stream_.[[backpressure]], - 1. If _backpressure_ is *true*, set _writer_.[[readyPromise]] to a new promise. - 1. Otherwise, - 1. Assert: _backpressure_ is *false*. - 1. Resolve _writer_.[[readyPromise]] with *undefined*. - 1. Set _stream_.[[backpressure]] to _backpressure_. - - -

Class -WritableStreamDefaultWriter

- -The {{WritableStreamDefaultWriter}} class represents a writable stream writer designed to be vended by a -{{WritableStream}} instance. - -

Class definition

+
+ ReadableByteStreamControllerClose(|controller|) + performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If |controller|.\[[closeRequested]] is true or |stream|.\[[state]] is not "`readable`", return. + 1. If |controller|.\[[queueTotalSize]] > 0, + 1. Set |controller|.\[[closeRequested]] to true. + 1. Return. + 1. If |controller|.\[[pendingPullIntos]] is not empty, + 1. Let |firstPendingPullInto| be |controller|.\[[pendingPullIntos]][0]. + 1. If |firstPendingPullInto|.\[[bytesFilled]] > 0, + 1. Let |e| be a new {{TypeError}} exception. + 1. Perform ! [$ReadableByteStreamControllerError$](|controller|, |e|). + 1. Throw |e|. + 1. Perform ! [$ReadableByteStreamControllerClearAlgorithms$](|controller|). + 1. Perform ! [$ReadableStreamClose$](|stream|). +
-
+
+ ReadableByteStreamControllerCommitPullIntoDescriptor(|stream|, + |pullIntoDescriptor|) performs the following steps: + + 1. Assert: |stream|.\[[state]] is not "`errored`". + 1. Let |done| be false. + 1. If |stream|.\[[state]] is "`closed`", + 1. Assert: |pullIntoDescriptor|.\[[bytesFilled]] is 0. + 1. Set |done| to true. + 1. Let |filledView| be ! + [$ReadableByteStreamControllerConvertPullIntoDescriptor$](|pullIntoDescriptor|). + 1. If |pullIntoDescriptor|.\[[readerType]] is "`default`", + 1. Perform ! [$ReadableStreamFulfillReadRequest$](|stream|, |filledView|, |done|). + 1. Otherwise, + 1. Assert: |pullIntoDescriptor|.\[[readerType]] is "`byob`". + 1. Perform ! [$ReadableStreamFulfillReadIntoRequest$](|stream|, |filledView|, |done|). +
-This section is non-normative. +
+ ReadableByteStreamControllerConvertPullIntoDescriptor(|pullIntoDescriptor|) + performs the following steps: + + 1. Let |bytesFilled| be |pullIntoDescriptor|.\[[bytesFilled]]. + 1. Let |elementSize| be |pullIntoDescriptor|.\[[elementSize]]. + 1. Assert: |bytesFilled| ≤ |pullIntoDescriptor|.\[[byteLength]]. + 1. Assert: |bytesFilled| mod |elementSize| is 0. + 1. Return ! [$Construct$](|pullIntoDescriptor|.\[[ctor]], « |pullIntoDescriptor|.\[[buffer]], + |pullIntoDescriptor|.\[[byteOffset]], |bytesFilled| ÷ |elementSize| »). +
-If one were to write the {{WritableStreamDefaultWriter}} class in something close to the syntax of [[!ECMASCRIPT]], it -would look like +
+ ReadableByteStreamControllerEnqueue(|controller|, + |chunk|) performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If |controller|.\[[closeRequested]] is true or |stream|.\[[state]] is not "`readable`", return. + 1. Let |buffer| be |chunk|.\[[ViewedArrayBuffer]]. + 1. Let |byteOffset| be |chunk|.\[[ByteOffset]]. + 1. Let |byteLength| be |chunk|.\[[ByteLength]]. + 1. Let |transferredBuffer| be ! [$TransferArrayBuffer$](|buffer|). + 1. If ! [$ReadableStreamHasDefaultReader$](|stream|) is true + 1. If ! [$ReadableStreamGetNumReadRequests$](|stream|) is 0, + 1. Perform ! [$ReadableByteStreamControllerEnqueueChunkToQueue$](|controller|, + |transferredBuffer|, |byteOffset|, |byteLength|). + 1. Otherwise, + 1. Assert: |controller|.\[[queue]] [=list/is empty=]. + 1. Let |transferredView| be ! [$Construct$]({{%Uint8Array%}}, « |transferredBuffer|, + |byteOffset|, |byteLength| »). + 1. Perform ! [$ReadableStreamFulfillReadRequest$](|stream|, |transferredView|, false). + 1. Otherwise, if ! [$ReadableStreamHasBYOBReader$](|stream|) is true, + 1. Perform ! [$ReadableByteStreamControllerEnqueueChunkToQueue$](|controller|, + |transferredBuffer|, |byteOffset|, |byteLength|). + 1. Perform ! [$ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue$](|controller|). + 1. Otherwise, + 1. Assert: ! [$IsReadableStreamLocked$](|stream|) is false. + 1. Perform ! [$ReadableByteStreamControllerEnqueueChunkToQueue$](|controller|, + |transferredBuffer|, |byteOffset|, |byteLength|). + 1. Perform ! [$ReadableByteStreamControllerCallPullIfNeeded$](|controller|). +
-

-  class WritableStreamDefaultWriter {
-    constructor(stream)
+
+ ReadableByteStreamControllerEnqueueChunkToQueue(|controller|, + |buffer|, |byteOffset|, |byteLength|) performs the following steps: - get closed() - get desiredSize() - get ready() + 1. [=list/Append=] Record {\[[buffer]]: |buffer|, \[[byteOffset]]: |byteOffset|, \[[byteLength]]: + |byteLength|} to |controller|.\[[queue]]. + 1. Set |controller|.\[[queueTotalSize]] to |controller|.\[[queueTotalSize]] + |byteLength|. +
- abort(reason) - close() - releaseLock() - write(chunk) - } -
+
+ ReadableByteStreamControllerError(|controller|, + |e|) performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If |stream|.\[[state]] is not "`readable`", return. + 1. Perform ! [$ReadableByteStreamControllerClearPendingPullIntos$](|controller|). + 1. Perform ! [$ResetQueue$](|controller|). + 1. Perform ! [$ReadableByteStreamControllerClearAlgorithms$](|controller|). + 1. Perform ! [$ReadableStreamError$](|stream|, |e|). +
+ +
+ ReadableByteStreamControllerFillHeadPullIntoDescriptor(|controller|, + |size|, |pullIntoDescriptor|) performs the following steps: + 1. Assert: either |controller|.\[[pendingPullIntos]] [=list/is empty=], or + |controller|.\[[pendingPullIntos]][0] is |pullIntoDescriptor|. + 1. Perform ! [$ReadableByteStreamControllerInvalidateBYOBRequest$](|controller|). + 1. Set |pullIntoDescriptor|.\[[bytesFilled]] to |pullIntoDescriptor|.\[[bytesFilled]] + |size|.
-

Internal slots

+
+ ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(|controller|, + |pullIntoDescriptor|) performs the following steps: + + 1. Let |elementSize| be |pullIntoDescriptor|.\[[elementSize]]. + 1. Let |currentAlignedBytes| be |pullIntoDescriptor|.\[[bytesFilled]] − + (|pullIntoDescriptor|.\[[bytesFilled]] mod |elementSize|). + 1. Let |maxBytesToCopy| be min(|controller|.\[[queueTotalSize]], + |pullIntoDescriptor|.\[[byteLength]] − |pullIntoDescriptor|.\[[bytesFilled]]). + 1. Let |maxBytesFilled| be |pullIntoDescriptor|.\[[bytesFilled]] + |maxBytesToCopy|. + 1. Let |maxAlignedBytes| be |maxBytesFilled| − (|maxBytesFilled| mod |elementSize|). + 1. Let |totalBytesToCopyRemaining| be |maxBytesToCopy|. + 1. Let |ready| be false. + 1. If |maxAlignedBytes| > |currentAlignedBytes|, + 1. Set |totalBytesToCopyRemaining| to |maxAlignedBytes| − |pullIntoDescriptor|.\[[bytesFilled]]. + 1. Set |ready| to true. + 1. Let |queue| be |controller|.\[[queue]]. + 1. [=While=] |totalBytesToCopyRemaining| > 0, + 1. Let |headOfQueue| be |queue|[0]. + 1. Let |bytesToCopy| be min(|totalBytesToCopyRemaining|, |headOfQueue|.\[[byteLength]]). + 1. Let |destStart| be |pullIntoDescriptor|.\[[byteOffset]] + + |pullIntoDescriptor|.\[[bytesFilled]]. + 1. Perform ! [$CopyDataBlockBytes$](|pullIntoDescriptor|.\[[buffer]].\[[ArrayBufferData]], + |destStart|, |headOfQueue|.\[[buffer]].\[[ArrayBufferData]], |headOfQueue|.\[[byteOffset]], + |bytesToCopy|). + 1. If |headOfQueue|.\[[byteLength]] is |bytesToCopy|, + 1. [=list/Remove=] |queue|[0]. + 1. Otherwise, + 1. Set |headOfQueue|.\[[byteOffset]] to |headOfQueue|.\[[byteOffset]] + |bytesToCopy|. + 1. Set |headOfQueue|.\[[byteLength]] to |headOfQueue|.\[[byteLength]] − |bytesToCopy|. + 1. Set |controller|.\[[queueTotalSize]] to |controller|.\[[queueTotalSize]] − |bytesToCopy|. + 1. Perform ! [$ReadableByteStreamControllerFillHeadPullIntoDescriptor$](|controller|, + |bytesToCopy|, |pullIntoDescriptor|). + 1. Set |totalBytesToCopyRemaining| to |totalBytesToCopyRemaining| − |bytesToCopy|. + 1. If |ready| is false, + 1. Assert: |controller|.\[[queueTotalSize]] is 0. + 1. Assert: |pullIntoDescriptor|.\[[bytesFilled]] > 0. + 1. Assert: |pullIntoDescriptor|.\[[bytesFilled]] < |pullIntoDescriptor|.\[[elementSize]]. + 1. Return |ready|. +
+ +
+ ReadableByteStreamControllerGetDesiredSize(|controller|) + performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. Let |state| be |stream|.\[[state]]. + 1. If |state| is "`errored`", return null. + 1. If |state| is "`closed`", return 0. + 1. Return |controller|.\[[strategyHWM]] − |controller|.\[[queueTotalSize]]. +
+ +
+ ReadableByteStreamControllerHandleQueueDrain(|controller|) + performs the following steps: + + 1. Assert: |controller|.\[[controlledReadableStream]].\[[state]] is "`readable`". + 1. If |controller|.\[[queueTotalSize]] is 0 and |controller|.\[[closeRequested]] is true, + 1. Perform ! [$ReadableByteStreamControllerClearAlgorithms$](|controller|). + 1. Perform ! [$ReadableStreamClose$](|controller|.\[[controlledReadableStream]]). + 1. Otherwise, + 1. Perform ! [$ReadableByteStreamControllerCallPullIfNeeded$](|controller|). +
+ +
+ ReadableByteStreamControllerInvalidateBYOBRequest(|controller|) + performs the following steps: + + 1. If |controller|.\[[byobRequest]] is undefined, return. + 1. Set |controller|.\[[byobRequest]].\[[controller]] to undefined. + 1. Set |controller|.\[[byobRequest]].\[[view]] to undefined. + 1. Set |controller|.\[[byobRequest]] to undefined. +
+ +
+ ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(|controller|) + performs the following steps: + + 1. Assert: |controller|.\[[closeRequested]] is false. + 1. [=While=] |controller|.\[[pendingPullIntos]] is not [=list/is empty|empty=], + 1. If |controller|.\[[queueTotalSize]] is 0, return. + 1. Let |pullIntoDescriptor| be |controller|.\[[pendingPullIntos]][0]. + 1. If ! [$ReadableByteStreamControllerFillPullIntoDescriptorFromQueue$](|controller|, + |pullIntoDescriptor|) is true, + 1. Perform ! [$ReadableByteStreamControllerShiftPendingPullInto$](|controller|). + 1. Perform ! + [$ReadableByteStreamControllerCommitPullIntoDescriptor$](|controller|.\[[controlledReadableStream]], + |pullIntoDescriptor|). +
+ +
+ ReadableByteStreamControllerPullInto(|controller|, + |view|) performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. Let |elementSize| be 1. + 1. Let |ctor| be {{%DataView%}}. + 1. If |view| has a \[[TypedArrayName]] internal slot (i.e., it is not a {{DataView}}), + 1. Set |elementSize| to the element size specified in [=the typed array constructors table=] for + |view|.\[[TypedArrayName]]. + 1. Set |ctor| to the constructor specified in [=the typed array constructors table=] for + |view|.\[[TypedArrayName]]. + 1. Let |byteOffset| be |view|.\[[ByteOffset]]. + 1. Let |byteLength| be |view|.\[[ByteLength]]. + 1. Let |buffer| be ! [$TransferArrayBuffer$](|view|.\[[ViewedArrayBuffer]]). + 1. Let |pullIntoDescriptor| be Record {\[[buffer]]: |buffer|, \[[byteOffset]]: |byteOffset|, + \[[byteLength]]: |byteLength|, \[[bytesFilled]]: 0, \[[elementSize]]: |elementSize|, + \[[ctor]]: |ctor|, \[[readerType]]: "`byob`"}. + 1. If |controller|.\[[pendingPullIntos]] is not empty, + 1. [=list/Append=] |pullIntoDescriptor| to |controller|.\[[pendingPullIntos]]. + 1. Return ! [$ReadableStreamAddReadIntoRequest$](|stream|). + 1. If |stream|.\[[state]] is "`closed`", + 1. Let |emptyView| be ! [$Construct$](|ctor|, « |pullIntoDescriptor|.\[[buffer]], + |pullIntoDescriptor|.\[[byteOffset]], 0 »). + 1. Return [=a promise resolved with=] ! [$ReadableStreamCreateReadResult$](|emptyView|, true, + |stream|.\[[reader]].\[[forAuthorCode]]). + 1. If |controller|.\[[queueTotalSize]] > 0, + 1. If ! [$ReadableByteStreamControllerFillPullIntoDescriptorFromQueue$](|controller|, + |pullIntoDescriptor|) is true, + 1. Let |filledView| be ! + [$ReadableByteStreamControllerConvertPullIntoDescriptor$](|pullIntoDescriptor|). + 1. Perform ! [$ReadableByteStreamControllerHandleQueueDrain$](|controller|). + 1. Return [=a promise resolved with=] ! [$ReadableStreamCreateReadResult$](|filledView|, false, + |stream|.\[[reader]].\[[forAuthorCode]]). + 1. If |controller|.\[[closeRequested]] is true, + 1. Let |e| be a {{TypeError}} exception. + 1. Perform ! [$ReadableByteStreamControllerError$](|controller|, |e|). + 1. Return [=a promise rejected with=] |e|. + 1. [=list/Append=] |pullIntoDescriptor| to |controller|.\[[pendingPullIntos]]. + 1. Let |promise| be ! [$ReadableStreamAddReadIntoRequest$](|stream|). + 1. Perform ! [$ReadableByteStreamControllerCallPullIfNeeded$](|controller|). + 1. Return |promise|. +
+ +
+ ReadableByteStreamControllerRespond(|controller|, + |bytesWritten|) performs the following steps: + + 1. Assert: |controller|.\[[pendingPullIntos]] is not empty. + 1. Perform ? [$ReadableByteStreamControllerRespondInternal$](|controller|, |bytesWritten|). +
+ +
+ ReadableByteStreamControllerRespondInClosedState(|controller|, + |firstDescriptor|) performs the following steps: + + 1. Set |firstDescriptor|.\[[buffer]] to ! [$TransferArrayBuffer$](|firstDescriptor|.\[[buffer]]). + 1. Assert: |firstDescriptor|.\[[bytesFilled]] is 0. + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If ! [$ReadableStreamHasBYOBReader$](|stream|) is true, + 1. [=While=] ! [$ReadableStreamGetNumReadIntoRequests$](|stream|) > 0, + 1. Let |pullIntoDescriptor| be ! + [$ReadableByteStreamControllerShiftPendingPullInto$](|controller|). + 1. Perform ! [$ReadableByteStreamControllerCommitPullIntoDescriptor$](|stream|, + |pullIntoDescriptor|). +
+ +
+ ReadableByteStreamControllerRespondInReadableState(|controller|, + |bytesWritten|, |pullIntoDescriptor|) performs the following steps: + + 1. If |pullIntoDescriptor|.\[[bytesFilled]] + |bytesWritten| > + |pullIntoDescriptor|.\[[byteLength]], throw a {{RangeError}} exception. + 1. Perform ! [$ReadableByteStreamControllerFillHeadPullIntoDescriptor$](|controller|, + |bytesWritten|, |pullIntoDescriptor|). + 1. If |pullIntoDescriptor|.\[[bytesFilled]] < |pullIntoDescriptor|.\[[elementSize]], return. + 1. Perform ! [$ReadableByteStreamControllerShiftPendingPullInto$](|controller|). + 1. Let |remainderSize| be |pullIntoDescriptor|.\[[bytesFilled]] mod + |pullIntoDescriptor|.\[[elementSize]]. + 1. If |remainderSize| > 0, + 1. Let |end| be |pullIntoDescriptor|.\[[byteOffset]] + |pullIntoDescriptor|.\[[bytesFilled]]. + 1. Let |remainder| be ? [$CloneArrayBuffer$](|pullIntoDescriptor|.\[[buffer]], |end| − + |remainderSize|, |remainderSize|, {{%ArrayBuffer%}}). + 1. Perform ! [$ReadableByteStreamControllerEnqueueChunkToQueue$](|controller|, |remainder|, 0, + |remainder|.\[[ByteLength]]). + 1. Set |pullIntoDescriptor|.\[[buffer]] to ! [$TransferArrayBuffer$](|pullIntoDescriptor|.\[[buffer]]). + 1. Set |pullIntoDescriptor|.\[[bytesFilled]] to |pullIntoDescriptor|.\[[bytesFilled]] − |remainderSize|. + 1. Perform ! + [$ReadableByteStreamControllerCommitPullIntoDescriptor$](|controller|.\[[controlledReadableStream]], + |pullIntoDescriptor|). + 1. Perform ! [$ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue$](|controller|). +
+ +
+ ReadableByteStreamControllerRespondInternal(|controller|, + |bytesWritten|) performs the following steps: + + 1. Let |firstDescriptor| be |controller|.\[[pendingPullIntos]][0]. + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If |stream|.\[[state]] is "`closed`", + 1. If |bytesWritten| is not 0, throw a {{TypeError}} exception. + 1. Perform ! [$ReadableByteStreamControllerRespondInClosedState$](|controller|, + |firstDescriptor|). + 1. Otherwise, + 1. Assert: |stream|.\[[state]] is "`readable`". + 1. Perform ? [$ReadableByteStreamControllerRespondInReadableState$](|controller|, |bytesWritten|, + |firstDescriptor|). + 1. Perform ! [$ReadableByteStreamControllerCallPullIfNeeded$](|controller|). +
-Instances of {{WritableStreamDefaultWriter}} are created with the internal slots described in the following table: +
+ ReadableByteStreamControllerRespondWithNewView(|controller|, + |view|) performs the following steps: + + 1. Assert: |controller|.\[[pendingPullIntos]] is not [=list/is empty|empty=]. + 1. Let |firstDescriptor| be |controller|.\[[pendingPullIntos]][0]. + 1. If |firstDescriptor|.\[[byteOffset]] + |firstDescriptor|.\[[bytesFilled]] is not + |view|.\[[ByteOffset]], throw a {{RangeError}} exception. + 1. If |firstDescriptor|.\[[byteLength]] is not |view|.\[[ByteLength]], throw a {{RangeError}} + exception. + 1. Set |firstDescriptor|.\[[buffer]] to |view|.\[[ViewedArrayBuffer]]. + 1. Perform ? [$ReadableByteStreamControllerRespondInternal$](|controller|, |view|.\[[ByteLength]]). +
+ +
+ ReadableByteStreamControllerShiftPendingPullInto(|controller|) + performs the following steps: + + 1. Let |descriptor| be |controller|.\[[pendingPullIntos]][0]. + 1. [=list/Remove=] |descriptor| from |controller|.\[[pendingPullIntos]]. + 1. Perform ! [$ReadableByteStreamControllerInvalidateBYOBRequest$](|controller|). + 1. Return |descriptor|. +
+ +
+ ReadableByteStreamControllerShouldCallPull(|controller|) + performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledReadableStream]]. + 1. If |stream|.\[[state]] is not "`readable`", return false. + 1. If |controller|.\[[closeRequested]] is true, return false. + 1. If |controller|.\[[started]] is false, return false. + 1. If ! [$ReadableStreamHasDefaultReader$](|stream|) is true and ! + [$ReadableStreamGetNumReadRequests$](|stream|) > 0, return true. + 1. If ! [$ReadableStreamHasBYOBReader$](|stream|) is true and ! + [$ReadableStreamGetNumReadIntoRequests$](|stream|) > 0, return true. + 1. Let |desiredSize| be ! [$ReadableByteStreamControllerGetDesiredSize$](|controller|). + 1. Assert: |desiredSize| is not null. + 1. If |desiredSize| > 0, return true. + 1. Return false. +
+ +
+ SetUpReadableByteStreamController(|stream|, + |controller|, |startAlgorithm|, |pullAlgorithm|, |cancelAlgorithm|, |highWaterMark|, + |autoAllocateChunkSize|) performs the following steps: + + 1. Assert: |stream|.\[[readableStreamController]] is undefined. + 1. If |autoAllocateChunkSize| is not undefined, + 1. Assert: ! IsInteger(|autoAllocateChunkSize|) is true. + 1. Assert: |autoAllocateChunkSize| is positive. + 1. Set |controller|.\[[controlledReadableStream]] to |stream|. + 1. Set |controller|.\[[pullAgain]] and |controller|.\[[pulling]] to false. + 1. Set |controller|.\[[byobRequest]] to undefined. + 1. Perform ! [$ResetQueue$](|controller|). + 1. Set |controller|.\[[closeRequested]] and |controller|.\[[started]] to false. + 1. Set |controller|.\[[strategyHWM]] to |highWaterMark|. + 1. Set |controller|.\[[pullAlgorithm]] to |pullAlgorithm|. + 1. Set |controller|.\[[cancelAlgorithm]] to |cancelAlgorithm|. + 1. Set |controller|.\[[autoAllocateChunkSize]] to |autoAllocateChunkSize|. + 1. Set |controller|.\[[pendingPullIntos]] to a new empty [=list=]. + 1. Set |stream|.\[[readableStreamController]] to |controller|. + 1. Let |startResult| be the result of performing |startAlgorithm|. + 1. Let |startPromise| be [=a promise resolved with=] |startResult|. + 1. [=Upon fulfillment=] of |startPromise|, + 1. Set |controller|.\[[started]] to true. + 1. Assert: |controller|.\[[pulling]] is false. + 1. Assert: |controller|.\[[pullAgain]] is false. + 1. Perform ! [$ReadableByteStreamControllerCallPullIfNeeded$](|controller|). + 1. [=Upon rejection=] of |startPromise| with reason |r|, + 1. Perform ! [$ReadableByteStreamControllerError$](|controller|, |r|). +
+ +
+ SetUpReadableByteStreamControllerFromUnderlyingSource(|stream|, + |underlyingSource|, |underlyingSourceDict|, |highWaterMark|) performs the following steps: + + 1. Let |controller| be a [=new=] {{ReadableByteStreamController}}. + 1. Let |startAlgorithm| be an algorithm that returns undefined. + 1. Let |pullAlgorithm| be an algorithm that returns [=a promise resolved with=] undefined. + 1. Let |cancelAlgorithm| be an algorithm that returns [=a promise resolved with=] undefined. + 1. If |underlyingSourceDict|["{{UnderlyingSource/start}}"] [=map/exists=], then set + |startAlgorithm| to an algorithm which returns the result of [=invoking=] + |underlyingSourceDict|["{{UnderlyingSource/start}}"] with argument list + « |controller| » and [=callback this value=] |underlyingSource|. + 1. If |underlyingSourceDict|["{{UnderlyingSource/pull}}"] [=map/exists=], then set + |pullAlgorithm| to an algorithm which returns the result of [=invoking=] + |underlyingSourceDict|["{{UnderlyingSource/pull}}"] with argument list + « |controller| » and [=callback this value=] |underlyingSource|. + 1. If |underlyingSourceDict|["{{UnderlyingSource/cancel}}"] [=map/exists=], then set + |cancelAlgorithm| to an algorithm which takes an argument |reason| and returns the result of + [=invoking=] |underlyingSourceDict|["{{UnderlyingSource/cancel}}"] with argument list + « |reason| » and [=callback this value=] |underlyingSource|. + 1. Let |autoAllocateChunkSize| be + |underlyingSourceDict|["{{UnderlyingSource/autoAllocateChunkSize}}"], if it [=map/exists=], or + undefined otherwise. + 1. Perform ? [$SetUpReadableByteStreamController$](|stream|, |controller|, |startAlgorithm|, + |pullAlgorithm|, |cancelAlgorithm|, |highWaterMark|, |autoAllocateChunkSize|). +
+

Writable streams

+ +

Using writable streams

+ +
+ The usual way to write to a writable stream is to simply [=piping|pipe=] a [=readable stream=] to + it. This ensures that [=backpressure=] is respected, so that if the writable stream's [=underlying + sink=] is not able to accept data as fast as the readable stream can produce it, the readable + stream is informed of this and has a chance to slow down its data production. + + + readableStream.pipeTo(writableStream) + .then(() => console.log("All data successfully written!")) + .catch(e => console.error("Something went wrong!", e)); + +
+ +
+ You can also write directly to writable streams by acquiring a [=writer=] and using its + {{WritableStreamDefaultWriter/write()}} and {{WritableStreamDefaultWriter/close()}} methods. Since + writable streams queue any incoming writes, and take care internally to forward them to the + [=underlying sink=] in sequence, you can indiscriminately write to a writable stream without much + ceremony: + + + function writeArrayToStream(array, writableStream) { + const writer = writableStream.getWriter(); + array.forEach(chunk => writer.write(chunk).catch(() => {})); + + return writer.close(); + } + + writeArrayToStream([1, 2, 3, 4, 5], writableStream) + .then(() => console.log("All done!")) + .catch(e => console.error("Error with the stream: " + e)); + + + Note how we use .catch(() => {}) to suppress any rejections from the + {{WritableStreamDefaultWriter/write()}} method; we'll be notified of any fatal errors via a + rejection of the {{WritableStreamDefaultWriter/close()}} method, and leaving them un-caught would + cause potential {{unhandledrejection}} events and console warnings. +
+ +
+ In the previous example we only paid attention to the success or failure of the entire stream, by + looking at the promise returned by the writer's {{WritableStreamDefaultWriter/close()}} method. + That promise will reject if anything goes wrong with the stream—initializing it, writing to it, or + closing it. And it will fulfill once the stream is successfully closed. Often this is all you care + about. + + However, if you care about the success of writing a specific [=chunk=], you can use the promise + returned by the writer's {{WritableStreamDefaultWriter/write()}} method: + + + writer.write("i am a chunk of data") + .then(() => console.log("chunk successfully written!")) + .catch(e => console.error(e)); + + + What "success" means is up to a given stream instance (or more precisely, its [=underlying sink=]) + to decide. For example, for a file stream it could simply mean that the OS has accepted the write, + and not necessarily that the chunk has been flushed to disk. Some streams might not be able to + give such a signal at all, in which case the returned promise will fulfill immediately. +
+ +
+ The {{WritableStreamDefaultWriter/desiredSize}} and {{WritableStreamDefaultWriter/ready}} + properties of writable stream writers allow [=producers=] to more precisely respond to flow + control signals from the stream, to keep memory usage below the stream's specified [=high water + mark=]. The following example writes an infinite sequence of random bytes to a stream, using + {{WritableStreamDefaultWriter/desiredSize}} to determine how many bytes to generate at a given + time, and using {{WritableStreamDefaultWriter/ready}} to wait for the [=backpressure=] to subside. + + + async function writeRandomBytesForever(writableStream) { + const writer = writableStream.getWriter(); + + while (true) { + await writer.ready; + + const bytes = new Uint8Array(writer.desiredSize); + crypto.getRandomValues(bytes); + + // Purposefully don't await; awaiting writer.ready is enough. + writer.write(bytes).catch(() => {}); + } + } + + writeRandomBytesForever(myWritableStream).catch(e => console.error("Something broke", e)); + + + Note how we don't await the promise returned by + {{WritableStreamDefaultWriter/write()}}; this would be redundant with awaiting the + {{WritableStreamDefaultWriter/ready}} promise. Additionally, similar to a previous example, we use the .catch(() => + {}) pattern on the promises returned by {{WritableStreamDefaultWriter/write()}}; in this + case we'll be notified about any failures + awaiting the {{WritableStreamDefaultWriter/ready}} promise. +
+ +
+ To further emphasize how it's a bad idea to await the promise returned by + {{WritableStreamDefaultWriter/write()}}, consider a modification of the above example, where we + continue to use the {{WritableStreamDefaultWriter}} interface directly, but we don't control how + many bytes we have to write at a given time. In that case, the [=backpressure=]-respecting code + looks the same: + + + async function writeSuppliedBytesForever(writableStream, getBytes) { + const writer = writableStream.getWriter(); + + while (true) { + await writer.ready; + + const bytes = getBytes(); + writer.write(bytes).catch(() => {}); + } + } + + + Unlike the previous example, where—because we were always writing exactly + {{WritableStreamDefaultWriter/desiredSize|writer.desiredSize}} bytes each time—the + {{WritableStreamDefaultWriter/write()}} and {{WritableStreamDefaultWriter/ready}} promises were + synchronized, in this case it's quite possible that the {{WritableStreamDefaultWriter/ready}} + promise fulfills before the one returned by {{WritableStreamDefaultWriter/write()}} does. + Remember, the {{WritableStreamDefaultWriter/ready}} promise fulfills when the [=desired size to + fill a stream's internal queue|desired size=] becomes positive, which might be before the write + succeeds (especially in cases with a larger [=high water mark=]). + + In other words, awaiting the return value of {{WritableStreamDefaultWriter/write()}} + means you never queue up writes in the stream's [=internal queue=], instead only executing a write + after the previous one succeeds, which can result in low throughput. +
+ +

The {{WritableStream}} class

+ +The {{WritableStream}} represents a [=writable stream=]. + +

Interface definition

+ +The Web IDL definition for the {{WritableStream}} class is given as follows: + + +[Exposed=(Window,Worker,Worklet)] +interface WritableStream { + constructor(object underlyingSink, optional QueuingStrategy strategy = {}); + + readonly attribute boolean locked; + + Promise<void> abort(optional any reason); + Promise<void> close(); + WritableStreamDefaultWriter getWriter(); +}; + + +

Internal slots

+ +Instances of {{WritableStream}} are created with the internal slots described in the following +table: - - - - - - - - + - + - + + + + + + + + + +
Internal SlotDescription (non-normative)
\[[closedPromise]] - A promise returned by the writer's {{WritableStreamDefaultWriter/closed}} getter -
\[[ownerWritableStream]] - A {{WritableStream}} instance that owns this writer -
Internal Slot + Description (non-normative) +
\[[readyPromise]] - A promise returned by the writer's {{WritableStreamDefaultWriter/ready}} getter -
\[[backpressure]] + A boolean indicating the backpressure signal set by the controller +
\[[closeRequest]] + The promise returned from the writer's + {{WritableStreamDefaultWriter/close()}} method +
\[[inFlightWriteRequest]] + A slot set to the promise for the current in-flight write operation + while the [=underlying sink=]'s write algorithm is executing and has not yet fulfilled, used to + prevent reentrant calls +
\[[inFlightCloseRequest]] + A slot set to the promise for the current in-flight close operation + while the [=underlying sink=]'s close algorithm is executing and has not yet fulfilled, used to + prevent the {{WritableStreamDefaultWriter/abort()}} method from interrupting close +
\[[pendingAbortRequest]] + A Record containing the promise returned from + {{WritableStreamDefaultWriter/abort()}} and the reason passed to + {{WritableStreamDefaultWriter/abort()}} +
\[[state]] + A string containing the stream's current state, used internally; one of + "`writable`", "`closed`", "`erroring`", or "`errored`" +
\[[storedError]] + A value indicating how the stream failed, to be given as a failure + reason or exception when trying to operate on the stream while in the "`errored`" state +
\[[writableStreamController]] + A {{WritableStreamDefaultController}} created with the ability to + control the state and queue of this stream +
\[[writer]] + A {{WritableStreamDefaultWriter}} instance, if the stream is [=locked to + a writer=], or undefined if it is not +
\[[writeRequests]] + A [=list=] of promises representing the stream's internal queue of write + requests not yet processed by the [=underlying sink=]
-

new WritableStreamDefaultWriter(stream)

+

The \[[inFlightCloseRequest]] slot and \[[closeRequest]] slot are mutually +exclusive. Similarly, no element will be removed from \[[writeRequests]] while +\[[inFlightWriteRequest]] is not undefined. Implementations can optimize storage for these slots +based on these invariants. -

- The WritableStreamDefaultWriter constructor is generally not meant to be used directly; instead, a - stream's {{WritableStream/getWriter()}} method ought to be used. -
- - - 1. If ! IsWritableStream(_stream_) is *false*, throw a *TypeError* exception. - 1. If ! IsWritableStreamLocked(_stream_) is *true*, throw a *TypeError* exception. - 1. Set *this*.[[ownerWritableStream]] to _stream_. - 1. Set _stream_.[[writer]] to *this*. - 1. Let _state_ be _stream_.[[state]]. - 1. If _state_ is `"writable"`, - 1. If ! WritableStreamCloseQueuedOrInFlight(_stream_) is *false* and _stream_.[[backpressure]] is *true*, - set *this*.[[readyPromise]] to a new promise. - 1. Otherwise, set *this*.[[readyPromise]] to a promise resolved with *undefined*. - 1. Set *this*.[[closedPromise]] to a new promise. - 1. Otherwise, if _state_ is `"erroring"`, - 1. Set *this*.[[readyPromise]] to a promise rejected with _stream_.[[storedError]]. - 1. Set *this*.[[readyPromise]].[[PromiseIsHandled]] to *true*. - 1. Set *this*.[[closedPromise]] to a new promise. - 1. Otherwise, if _state_ is `"closed"`, - 1. Set *this*.[[readyPromise]] to a promise resolved with *undefined*. - 1. Set *this*.[[closedPromise]] to a promise resolved with *undefined*. - 1. Otherwise, - 1. Assert: _state_ is `"errored"`. - 1. Let _storedError_ be _stream_.[[storedError]]. - 1. Set *this*.[[readyPromise]] to a promise rejected with _storedError_. - 1. Set *this*.[[readyPromise]].[[PromiseIsHandled]] to *true*. - 1. Set *this*.[[closedPromise]] to a promise rejected with _storedError_. - 1. Set *this*.[[closedPromise]].[[PromiseIsHandled]] to *true*. - +

The underlying sink API

-

Properties of the {{WritableStreamDefaultWriter}} prototype

+The {{WritableStream()}} constructor accepts as its first argument a JavaScript object representing +the [=underlying sink=]. Such objects can contain any of the following properties: -
get closed
+ +dictionary UnderlyingSink { + WritableStreamStartCallback start; + WritableStreamWriteCallback write; + WritableStreamCloseCallback close; + WritableStreamAbortCallback abort; + any type; +}; -<div class="note"> - The <code>closed</code> getter returns a promise that will be fulfilled when the stream becomes closed, or rejected if - the stream ever errors or the writer's lock is <a lt="release a write lock">released</a> before the stream finishes - closing. -</div> +callback WritableStreamStartCallback = Promise<void> (WritableStreamDefaultController controller); +callback WritableStreamWriteCallback = Promise<void> (WritableStreamDefaultController controller, optional any chunk); +callback WritableStreamCloseCallback = Promise<void> (); +callback WritableStreamAbortCallback = Promise<void> (optional any reason); + - - 1. If ! IsWritableStreamDefaultWriter(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. Return *this*.[[closedPromise]]. - +
+
start(controller)
+
+

A function that is called immediately during creation of the {{WritableStream}}. -

get desiredSize
+

Typically this is used to acquire access to the [=underlying sink=] resource being + represented. -

- The desiredSize getter returns the desired size to - fill the stream's internal queue. It can be negative, if the queue is over-full. A producer can use this - information to determine the right amount of data to write. +

If this setup process is asynchronous, it can return a promise to signal success or failure; a + rejected promise will error the stream. Any thrown exceptions will be re-thrown by the + {{WritableStream()}} constructor. - It will be null if the stream cannot be successfully written to (due to either being errored, or - having an abort queued up). It will return zero if the stream is closed. The getter will throw an exception if invoked - when the writer's lock is released. -

+
write(chunk, + controller)
+
+

A function that is called when a new [=chunk=] of data is ready to be written to the + [=underlying sink=]. The stream implementation guarantees that this function will be called only + after previous writes have succeeded, and never before {{UnderlyingSink/start|start()}} has + succeeded or after {{UnderlyingSink/close|close()}} or {{UnderlyingSink/abort|abort()}} have + been called. + +

This function is used to actually send the data to the resource presented by the [=underlying + sink=], for example by calling a lower-level API. + +

If the process of writing data is asynchronous, and communicates success or failure signals + back to its user, then this function can return a promise to signal success or failure. This + promise return value will be communicated back to the caller of + {{WritableStreamDefaultWriter/write()|writer.write()}}, so they can monitor that individual + write. Throwing an exception is treated the same as returning a rejected promise. + +

Note that such signals are not always available; compare e.g. [[#example-ws-no-backpressure]] + with [[#example-ws-backpressure]]. In such cases, it's best to not return anything. + +

The promise potentially returned by this function also governs whether the given chunk counts + as written for the purposes of computed the [=desired size to fill a stream's internal + queue|desired size to fill the stream's internal queue=]. That is, during the time it takes the + promise to settle, {{WritableStreamDefaultWriter/desiredSize|writer.desiredSize}} will stay at + its previous value, only increasing to signal the desire for more chunks once the write + succeeds. + +

close()
+
+

A function that is called after the [=producer=] signals, via + {{WritableStreamDefaultWriter/close()|writer.close()}}, that they are done writing [=chunks=] to + the stream, and subsequently all queued-up writes have successfully completed. - - 1. If ! IsWritableStreamDefaultWriter(*this*) is *false*, throw a *TypeError* exception. - 1. If *this*.[[ownerWritableStream]] is *undefined*, throw a *TypeError* exception. - 1. Return ! WritableStreamDefaultWriterGetDesiredSize(*this*). - +

This function can perform any actions necessary to finalize or flush writes to the + [=underlying sink=], and release access to any held resources. -

get ready
+

If the shutdown process is asynchronous, the function can return a promise to signal success + or failure; the result will be communicated via the return value of the called + {{WritableStreamDefaultWriter/close()|writer.close()}} method. Additionally, a rejected promise + will error the stream, instead of letting it close successfully. Throwing an exception is + treated the same as returning a rejected promise. -

- The ready getter returns a promise that will be fulfilled when the desired size to fill the stream's internal queue transitions from non-positive to positive, - signaling that it is no longer applying backpressure. Once the desired size to fill the stream's internal queue dips back to zero or below, the getter will return a new - promise that stays pending until the next transition. +
close(reason)
+
+

A function that is called after the [=producer=] signals, via + {{WritableStream/abort()|stream.abort()}} or + {{WritableStreamDefaultWriter/abort()|writer.abort()}}, that they wish to [=abort a writable + stream|abort=] the stream. It takes as its argument the same value as was passed to those + methods by the producer. + +

Writable streams can additionally be aborted under certain conditions during [=piping=]; see + the definition of the {{ReadableStream/pipeTo()}} method for more details. + +

This function can clean up any held resources, much like {{UnderlyingSink/close|close()}}, + but perhaps with some custom handling. + +

If the shutdown process is asynchronous, the function can return a promise to signal success + or failure; the result will be communicated via the return value of the called + {{WritableStreamDefaultWriter/abort()|writer.abort()}} method. Throwing an exception is treated + the same as returning a rejected promise. Regardless, the stream will be errored with a new + {{TypeError}} indicating that it was aborted. + +

type
+
+

This property is reserved for future use, so any attempts to supply a value will throw an + exception. +

- If the stream becomes errored or aborted, or the writer's lock is released, the - returned promise will become rejected. +The controller argument passed to {{UnderlyingSink/start|start()}} and +{{UnderlyingSink/write|write()}} is an instance of {{WritableStreamDefaultController}}, and has the +ability to error the stream. This is mainly used for bridging the gap with non-promise-based APIs, +as seen for example in [[#example-ws-no-backpressure]]. + +

Constructor, methods, and properties

+ +
+
stream = new {{WritableStream/constructor(underlyingSink, strategy)|WritableStream}}(underlyingSink[, strategy) +
+

Creates a new {{WritableStream}} wrapping the provided [=underlying sink=]. See + [[#underlying-sink-api]] for more details on the underlyingSink argument. + +

The |strategy| argument represents the stream's [=queuing strategy=], as described in + [[#qs-api]]. If it is not provided, the default behavior will be the same as a + {{CountQueuingStrategy}} with a [=high water mark=] of 1. + +

isLocked = stream.{{WritableStream/locked}} +
+

Returns whether or not the writable stream is [=locked to a writer=]. + +

await stream.{{WritableStream/abort(reason)|abort}}([ reason ]) +
+

[=abort a writable stream|Aborts=] the stream, signaling that the producer can no longer + successfully write to the stream and it is to be immediately moved to an errored state, with any + queued-up writes discarded. This will also execute any abort mechanism of the [=underlying + sink=]. + +

The returned promise will fulfill if the stream shuts down successfully, or reject if the + underlying sink signaled that there was an error doing so. Additionally, it will reject with a + {{TypeError}} (without attempting to cancel the stream) if the stream is currently [=locked to a + writer|locked=]. + +

await stream.{{WritableStream/close()|close}}() +
+

Closes the stream. The [=underlying sink=] will finish processing any previously-written + [=chunks=], before invoking its close behavior. During this time any further attempts to write + will fail (without erroring the stream). + +

The method returns a promise that will fulfill if all remaining [=chunks=] are successfully + written and the stream successfully closes, or rejects if an error is encountered during this + process. Additionally, it will reject with a {{TypeError}} (without attempting to cancel the + stream) if the stream is currently [=locked to a writer|locked=]. + +

writer = stream.{{WritableStream/getWriter()|getWriter}}() +
+

Creates a [=writer=] (an instance of {{WritableStreamDefaultWriter}}) and [=locked to a + writer|locks=] the stream to the new writer. While the stream is locked, no other writer can be + acquired until this one is [=release a write lock|released=]. + +

This functionality is especially useful for creating abstractions that desire the ability to + write to a stream without interruption or interleaving. By getting a writer for the stream, you + can ensure nobody else can write at the same time, which would cause the resulting written data + to be unpredictable and probably useless. +

+ +
+ The WritableStream(|underlyingSink|, + |strategy|) constructor steps are: + + 1. Let |underlyingSinkDict| be |underlyingSinkDict|, [=converted to an IDL value=] of type + {{UnderlyingSink}}. +

We cannot declare the |underlyingSink| argument as having the {{UnderlyingSink}} + type directly, because doing so would lose the reference to the original object. We need to + retain the object so we can [=invoke=] the various methods on it. + 1. If |underlyingSinkDict|["{{UnderlyingSink/type}}"] [=map/exists=], throw a {{RangeError}} + exception. +

This is to allow us to add new potential types in the future, without + backward-compatibility concerns. + 1. Perform ! [$InitializeWritableStream$]([=this=]). + 1. Let |sizeAlgorithm| be ? [$ExtractSizeAlgorithm$](|strategy|). + 1. Let |highWaterMark| be ? [$ExtractHighWaterMark$](|strategy|, 1). + 1. Perform ? [$SetUpWritableStreamDefaultControllerFromUnderlyingSink$]([=this=], |underlyingSink|, + |underlyingSinkDict|, |highWaterMark|, |sizeAlgorithm|).

- - 1. If ! IsWritableStreamDefaultWriter(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. Return *this*.[[readyPromise]]. - +
+ The locked attribute's getter steps are: -
abort(reason)
+ 1. Return ! [$IsWritableStreamLocked$]([=this=]). +
-
- If the writer is active, the abort method behaves the same as that for the - associated stream. (Otherwise, it returns a rejected promise.) +
+ The abort(|reason|) method steps are: + + 1. If ! [$IsWritableStreamLocked$]([=this=]) is true, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. Return ! [$WritableStreamAbort$]([=this=], |reason|).
- - 1. If ! IsWritableStreamDefaultWriter(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If *this*.[[ownerWritableStream]] is *undefined*, return a promise rejected with a *TypeError* exception. - 1. Return ! WritableStreamDefaultWriterAbort(*this*, _reason_). - +
+ The close() method steps are: -
close()
+ 1. If ! [$IsWritableStreamLocked$]([=this=]) is true, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. If ! [$WritableStreamCloseQueuedOrInFlight$]([=this=]) is true, return [=a promise rejected + with=] a {{TypeError}} exception. + 1. Return ! [$WritableStreamClose$]([=this=]). +
-
- If the writer is active, the close method behaves the same as that for the - associated stream. (Otherwise, it returns a rejected promise.) +
+ The getWriter() method steps are: + + 1. Return ? [$AcquireWritableStreamDefaultWriter$]([=this=]).
- - 1. If ! IsWritableStreamDefaultWriter(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. Let _stream_ be *this*.[[ownerWritableStream]]. - 1. If _stream_ is *undefined*, return a promise rejected with a *TypeError* exception. - 1. If ! WritableStreamCloseQueuedOrInFlight(_stream_) is *true*, return a promise rejected with a *TypeError* - exception. - 1. Return ! WritableStreamDefaultWriterClose(*this*). - +

The {{WritableStreamDefaultWriter}} class

-
releaseLock()
+The {{WritableStreamDefaultWriter}} class represents a [=writable stream writer=] designed to be +vended by a {{WritableStream}} instance. -
- The releaseLock method releases the writer's lock on the corresponding - stream. After the lock is released, the writer is no longer active. If the associated - stream is errored when the lock is released, the writer will appear errored in the same way from now on; otherwise, +

Interface definition

+ +The Web IDL definition for the {{WritableStreamDefaultWriter}} class is given as follows: + + +[Exposed=(Window,Worker,Worklet)] +interface WritableStreamDefaultWriter { + constructor(WritableStream stream); + + readonly attribute Promise<void> closed; + readonly attribute unrestricted double? desiredSize; + readonly attribute Promise<void> ready; + + Promise<void> abort(optional any reason); + Promise<void> close(); + void releaseLock(); + Promise<void> write(optional any chunk); +}; + + +

Internal slots

+ +Instances of {{WritableStreamDefaultWriter}} are created with the internal slots described in the +following table: + + + + + + + + +
Internal Slot + Description (non-normative) +
\[[closedPromise]] + A promise returned by the writer's + {{WritableStreamDefaultWriter/closed}} getter +
\[[ownerWritableStream]] + A {{WritableStream}} instance that owns this reader +
\[[readyPromise]] + A promise returned by the writer's + {{WritableStreamDefaultWriter/ready}} getter +
+ +

Constructor, methods, and properties

+ +
+
writer = new {{WritableStreamDefaultWriter(stream)|WritableStreamDefaultWriter}}(|stream|) +
+

This is equivalent to calling |stream|.{{WritableStream/getWriter()}}. + +

await writer.{{WritableStreamDefaultWriter/closed}} +
+

Returns a promise that will be fulfilled when the stream becomes closed, or rejected if the + stream ever errors or the writer's lock is [=release a write lock|released=] before the stream + finishes closing. + +

desiredSize = writer.{{WritableStreamDefaultWriter/desiredSize}} +
+

Returns the [=desired size to fill a stream's internal queue|desired size to fill the stream's + internal queue=]. It can be negative, if the queue is over-full. A [=producer=] can use this + information to determine the right amount of data to write. + +

It will be null if the stream cannot be successfully written to (due to either being errored, + or having an abort queued up). It will return zero if the stream is closed. And the getter will + throw an exception if invoked when the writer's lock is [=release a write lock|released=]. + +

await writer.{{WritableStreamDefaultWriter/ready}} +
+

Returns a promise that will be fulfilled when the [=desired size to fill a stream's internal + queue|desired size to fill the stream's internal queue=] transitions from non-positive to + positive, signaling that it is no longer applying [=backpressure=]. Once the [=desired size to + fill a stream's internal queue|desired size=] dips back to zero or below, the getter will return + a new promise that stays pending until the next transition. + +

If the stream becomes errored or aborted, or the writer's lock is [=release a write + lock|released=], the returned promise will become rejected. + +

await writer.{{WritableStreamDefaultWriter/abort(reason)|abort}}([ reason ]) +
+

If the reader is [=active writer|active=], behaves the same as + |stream|.{{WritableStream/abort(reason)|abort}}(reason). + +

await writer.{{WritableStreamDefaultWriter/close()|close}}() +
+

If the reader is [=active writer|active=], behaves the same as + |stream|.{{WritableStream/close()|close}}(). + +

writer.{{WritableStreamDefaultWriter/releaseLock()|releaseLock}}() +
+

[=release a write lock|Releases the writer's lock=] on the corresponding stream. After the lock + is released, the writer is no longer [=active writer|active=]. If the associated stream is errored + when the lock is released, the writer will appear errored in the same way from now on; otherwise, the writer will appear closed. - Note that the lock can still be released even if some ongoing writes have not yet finished (i.e. even if the promises - returned from previous calls to {{WritableStreamDefaultWriter/write()}} have not yet settled). It's not necessary to - hold the lock on the writer for the duration of the write; the lock instead simply prevents other producers - from writing in an interleaved manner. +

Note that the lock can still be released even if some ongoing writes have not yet finished + (i.e. even if the promises returned from previous calls to + {{WritableStreamDefaultWriter/write()}} have not yet settled). It's not necessary to hold the + lock on the writer for the duration of the write; the lock instead simply prevents other + [=producers=] from writing in an interleaved manner. + +

await writer.{{WritableStreamDefaultWriter/write(chunk)|write}}(chunk) +
+

Writes the given [=chunk=] to the writable stream, by waiting until any previous writes have + finished successfully, and then sending the [=chunk=] to the [=underlying sink=]'s + {{UnderlyingSink/write|write()}} method. It will return a promise that fulfills with undefined + upon a successful write, or rejects if the write fails or stream becomes errored before the + writing process is initiated. + +

Note that what "success" means is up to the [=underlying sink=]; it might indicate simply that + the [=chunk=] has been accepted, and not necessarily that it is safely saved to its ultimate + destination. +

+ +
+ The WritableStreamDefaultWriter(|stream|) constructor steps + are: + + 1. Perform ? [$SetUpWritableStreamDefaultWriter$]([=this=], |stream|).
- - 1. If ! IsWritableStreamDefaultWriter(*this*) is *false*, throw a *TypeError* exception. - 1. Let _stream_ be *this*.[[ownerWritableStream]]. - 1. If _stream_ is *undefined*, return. - 1. Assert: _stream_.[[writer]] is not *undefined*. - 1. Perform ! WritableStreamDefaultWriterRelease(*this*). - +
+ The closed + getter steps are: -
write(chunk)
+ 1. Return [=this=].\[[closedPromise]]. +
-
- The write method writes the given chunk to the writable stream, by waiting until any previous - writes have finished successfully, and then sending the chunk to the underlying sink's {{underlying - sink/write()}} method. It will return a promise that fulfills with undefined upon a successful - write, or rejects if the write fails or stream becomes errored before the writing process is initiated. - - Note that what "success" means is up to the underlying sink; it might indicate simply that the chunk has - been accepted, and not necessarily that it is safely saved to its ultimate destination. -
- - - 1. If ! IsWritableStreamDefaultWriter(*this*) is *false*, return a promise rejected with a *TypeError* exception. - 1. If *this*.[[ownerWritableStream]] is *undefined*, return a promise rejected with a *TypeError* exception. - 1. Return ! WritableStreamDefaultWriterWrite(*this*, _chunk_). - - -

Writable stream writer abstract operations

- -

IsWritableStreamDefaultWriter ( -x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have an [[ownerWritableStream]] internal slot, return *false*. - 1. Return *true*. - - -

WritableStreamDefaultWriterAbort ( writer, reason )

- - - 1. Let _stream_ be _writer_.[[ownerWritableStream]]. - 1. Assert: _stream_ is not *undefined*. - 1. Return ! WritableStreamAbort(_stream_, _reason_). - - -

WritableStreamDefaultWriterClose ( writer )

- - - 1. Let _stream_ be _writer_.[[ownerWritableStream]]. - 1. Assert: _stream_ is not *undefined*. - 1. Return ! WritableStreamClose(_stream_). - - -

WritableStreamDefaultWriterCloseWithErrorPropagation ( writer )

- -

This abstract operation helps implement the error propagation semantics of -{{ReadableStream/pipeTo()}}.

- - - 1. Let _stream_ be _writer_.[[ownerWritableStream]]. - 1. Assert: _stream_ is not *undefined*. - 1. Let _state_ be _stream_.[[state]]. - 1. If ! WritableStreamCloseQueuedOrInFlight(_stream_) is *true* or _state_ is `"closed"`, return - a promise resolved with *undefined*. - 1. If _state_ is `"errored"`, return a promise rejected with _stream_.[[storedError]]. - 1. Assert: _state_ is `"writable"` or `"erroring"`. - 1. Return ! WritableStreamDefaultWriterClose(_writer_). - - -

WritableStreamDefaultWriterEnsureClosedPromiseRejected( writer, error )

- - - 1. If _writer_.[[closedPromise]].[[PromiseState]] is `"pending"`, reject _writer_.[[closedPromise]] with - _error_. - 1. Otherwise, set _writer_.[[closedPromise]] to a promise rejected with _error_. - 1. Set _writer_.[[closedPromise]].[[PromiseIsHandled]] to *true*. - - -

WritableStreamDefaultWriterEnsureReadyPromiseRejected( writer, error )

- - - 1. If _writer_.[[readyPromise]].[[PromiseState]] is `"pending"`, reject _writer_.[[readyPromise]] with _error_. - 1. Otherwise, set _writer_.[[readyPromise]] to a promise rejected with _error_. - 1. Set _writer_.[[readyPromise]].[[PromiseIsHandled]] to *true*. - - -

WritableStreamDefaultWriterGetDesiredSize ( writer )

- - - 1. Let _stream_ be _writer_.[[ownerWritableStream]]. - 1. Let _state_ be _stream_.[[state]]. - 1. If _state_ is `"errored"` or `"erroring"`, return *null*. - 1. If _state_ is `"closed"`, return *0*. - 1. Return ! WritableStreamDefaultControllerGetDesiredSize(_stream_.[[writableStreamController]]). - - -

WritableStreamDefaultWriterRelease ( writer )

- - - 1. Let _stream_ be _writer_.[[ownerWritableStream]]. - 1. Assert: _stream_ is not *undefined*. - 1. Assert: _stream_.[[writer]] is _writer_. - 1. Let _releasedError_ be a new *TypeError*. - 1. Perform ! WritableStreamDefaultWriterEnsureReadyPromiseRejected(_writer_, _releasedError_). - 1. Perform ! WritableStreamDefaultWriterEnsureClosedPromiseRejected(_writer_, _releasedError_). - 1. Set _stream_.[[writer]] to *undefined*. - 1. Set _writer_.[[ownerWritableStream]] to *undefined*. - - -

WritableStreamDefaultWriterWrite ( writer, chunk )

- - - 1. Let _stream_ be _writer_.[[ownerWritableStream]]. - 1. Assert: _stream_ is not *undefined*. - 1. Let _controller_ be _stream_.[[writableStreamController]]. - 1. Let _chunkSize_ be ! WritableStreamDefaultControllerGetChunkSize(_controller_, _chunk_). - 1. If _stream_ is not equal to _writer_.[[ownerWritableStream]], return a promise rejected with a *TypeError* - exception. - 1. Let _state_ be _stream_.[[state]]. - 1. If _state_ is `"errored"`, return a promise rejected with _stream_.[[storedError]]. - 1. If ! WritableStreamCloseQueuedOrInFlight(_stream_) is *true* or _state_ is `"closed"`, return - a promise rejected with a *TypeError* exception indicating that the stream is closing or closed. - 1. If _state_ is `"erroring"`, return a promise rejected with _stream_.[[storedError]]. - 1. Assert: _state_ is `"writable"`. - 1. Let _promise_ be ! WritableStreamAddWriteRequest(_stream_). - 1. Perform ! WritableStreamDefaultControllerWrite(_controller_, _chunk_, _chunkSize_). - 1. Return _promise_. - - -

Class -WritableStreamDefaultController

- -The {{WritableStreamDefaultController}} class has methods that allow control of a {{WritableStream}}'s state. When -constructing a {{WritableStream}}, the underlying sink is given a corresponding -{{WritableStreamDefaultController}} instance to manipulate. - -

Class definition

+
+ The desiredSize getter steps are: -
+ 1. If [=this=].\[[ownerWritableStream]] is undefined, throw a {{TypeError}} exception. + 1. Return ! [$WritableStreamDefaultWriterGetDesiredSize$]([=this=]). +
-This section is non-normative. +
+ The ready getter + steps are: + + 1. Return [=this=].\[[readyPromise]]. +
-If one were to write the {{WritableStreamDefaultController}} class in something close to the syntax of [[!ECMASCRIPT]], -it would look like +
+ The abort(|reason|) + method steps are: -

-  class WritableStreamDefaultController {
-    constructor() // always throws
+ 1. If [=this=].\[[ownerWritableStream]] is undefined, return [=a promise rejected with=] a
+    {{TypeError}} exception.
+ 1. Return ! [$WritableStreamDefaultWriterAbort$]([=this=], |reason|).
+
- error(e) - } -
+
+ The close() method + steps are: + 1. Let |stream| be [=this=].\[[ownerWritableStream]]. + 1. If |stream| is undefined, return [=a promise rejected with=] a {{TypeError}} exception. + 1. If ! [$WritableStreamCloseQueuedOrInFlight$](|stream|) is true, return [=a promise rejected + with=] a {{TypeError}} exception. + 1. Return ! [$WritableStreamDefaultWriterClose$]([=this=]).
+
+ The releaseLock() method steps are: + + 1. Let |stream| be [=this=].\[[ownerWritableStream]]. + 1. If |stream| is undefined, return. + 1. Assert: |stream|.\[[writer]] is not undefined. + 1. Perform ! [$WritableStreamDefaultWriterRelease$]([=this=]). +
+ +
+ The write(|chunk|) + method steps are: + + 1. If [=this=].\[[ownerWritableStream]] is undefined, return [=a promise rejected with=] a + {{TypeError}} exception. + 1. Return ! [$WritableStreamDefaultWriterWrite$]([=this=], |chunk|). +
+ +

The {{WritableStreamDefaultController}} class

+ +The {{WritableStreamDefaultController}} class has methods that allow control of a +{{WritableStream}}'s state. When constructing a {{WritableStream}}, the [=underlying sink=] is +given a corresponding {{WritableStreamDefaultController}} instance to manipulate. + +

Interface definition

+ +The Web IDL definition for the {{WritableStreamDefaultController}} class is given as follows: + + +[Exposed=(Window,Worker,Worklet)] +interface WritableStreamDefaultController { + void error(optional any e); +}; + +

Internal slots

-Instances of {{WritableStreamDefaultController}} are created with the internal slots described in the following table: +Instances of {{WritableStreamDefaultController}} are created with the internal slots described in +the following table: - - - - - - + - + + + - + - + - + - + - + - + - + - + +
Internal SlotDescription (non-normative)
\[[abortAlgorithm]] - A promise-returning algorithm, taking one argument (the abort reason), which communicates - a requested abort to the underlying sink -
Internal SlotDescription (non-normative)
\[[closeAlgorithm]] - A promise-returning algorithm which communicates a requested close to the underlying - sink -
\[[abortAlgorithm]] + A promise-returning algorithm, taking one argument (the abort reason), + which communicates a requested abort to the [=underlying sink=]
\[[controlledWritableStream]] - The {{WritableStream}} instance controlled -
\[[closeAlgorithm]] + A promise-returning algorithm which communicates a requested close to + the [=underlying sink=]
\[[queue]] - A List representing the stream's internal queue of chunks -
\[[controlledWritableStream]] + The {{WritableStream}} instance controlled
\[[queueTotalSize]] - The total size of all the chunks stored in \[[queue]] (see [[#queue-with-sizes]]) -
\[[queue]] + A [=list=] representing the stream's internal queue of [=chunks=]
\[[started]] - A boolean flag indicating whether the underlying sink has finished starting -
\[[queueTotalSize]] + The total size of all the chunks stored in \[[queue]] (see + [[#queue-with-sizes]])
\[[strategyHWM]] - A number supplied by the creator of the stream as part of the stream's queuing - strategy, indicating the point at which the stream will apply backpressure to its underlying - sink -
\[[started]] + A boolean flag indicating whether the [=underlying sink=] has finished + starting
\[[strategySizeAlgorithm]] - An algorithm to calculate the size of enqueued chunks, as part of the stream’s - queuing strategy -
\[[strategyHWM]] + A number supplied by the creator of the stream as part of the stream's + [=queuing strategy=], indicating the point at which the stream will apply [=backpressure=] to its + [=underlying sink=]
\[[writeAlgorithm]] - A promise-returning algorithm, taking one argument (the chunk to write), which - writes data to the underlying sink -
\[[strategySizeAlgorithm]] + An algorithm to calculate the size of enqueued [=chunks=], as part of + the stream's [=queuing strategy=] +
\[[writeAlgorithm]] + A promise-returning algorithm, taking one argument (the [=chunk=] to + write), which writes data to the [=underlying sink=]
-

new WritableStreamDefaultController()

+

Methods

-
- The WritableStreamDefaultController constructor cannot be used directly; - {{WritableStreamDefaultController}} instances are created automatically during {{WritableStream}} construction. +
+
controller.{{WritableStreamDefaultController/error()|error}}(e) +
+

Closes the controlled writable stream, making all future interactions with it fail with the + given error e. + +

This method is rarely used, since usually it suffices to return a rejected promise from one of + the [=underlying sink=]'s methods. However, it can be useful for suddenly shutting down a stream + in response to an event outside the normal lifecycle of interactions with the [=underlying + sink=]. +

+ +
+ The error(|e|) method steps are: + + 1. Let |state| be [=this=].\[[controlledWritableStream]].\[[state]]. + 1. If |state| is not "`writable`", return. + 1. Perform ! [$WritableStreamDefaultControllerError$]([=this=], |e|).
- - 1. Throw a *TypeError* exception. - +

Internal methods

-

Properties of the {{WritableStreamDefaultController}} prototype

+The following are internal methods implemented by each {{WritableStreamDefaultController}} instance. +The writable stream implementation will call into these. -
error(e)
+

The reason these are in method form, instead of as abstract operations, is to make +it clear that the writable stream implementation is decoupled from the controller implementation, +and could in the future be expanded with other controllers, as long as those controllers +implemented such internal methods. A similar scenario is seen for readable streams (see +[[#rs-abstract-ops-used-by-controllers]]), where there actually are multiple controller types and +as such the counterpart internal methods are used polymorphically. -

- The error method will error the writable stream, making all future interactions with it fail with the - given error e. - - This method is rarely used, since usually it suffices to return a rejected promise from one of the underlying - sink's methods. However, it can be useful for suddenly shutting down a stream in response to an event outside the - normal lifecycle of interactions with the underlying sink. -
- - - 1. If ! IsWritableStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. Let _state_ be *this*.[[controlledWritableStream]].[[state]]. - 1. If _state_ is not `"writable"`, return. - 1. Perform ! WritableStreamDefaultControllerError(*this*, _e_). - - -

Writable stream default controller internal methods

- -The following are additional internal methods implemented by each {{WritableStreamDefaultController}} instance. The -writable stream implementation will call into these. - -

The reason these are in method form, instead of as abstract operations, is to make it clear that the -writable stream implementation is decoupled from the controller implementation, and could in the future be expanded with -other controllers, as long as those controllers implemented such internal methods. A similar scenario is seen for -readable streams, where there actually are multiple controller types and as such the counterpart internal methods are -used polymorphically. - -

\[[AbortSteps]]( -reason )
- - - 1. Let _result_ be the result of performing *this*.[[abortAlgorithm]], passing _reason_. - 1. Perform ! WritableStreamDefaultControllerClearAlgorithms(*this*). - 1. Return _result_. - - -
\[[ErrorSteps]]()
- - - 1. Perform ! ResetQueue(*this*). - - -

Writable stream default controller abstract operations

- -

IsWritableStreamDefaultController ( x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have an [[controlledWritableStream]] internal slot, return *false*. - 1. Return *true*. - - -

SetUpWritableStreamDefaultController ( stream, controller, startAlgorithm, -writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm )

- - - 1. Assert: ! IsWritableStream(_stream_) is *true*. - 1. Assert: _stream_.[[writableStreamController]] is *undefined*. - 1. Set _controller_.[[controlledWritableStream]] to _stream_. - 1. Set _stream_.[[writableStreamController]] to _controller_. - 1. Perform ! ResetQueue(_controller_). - 1. Set _controller_.[[started]] to *false*. - 1. Set _controller_.[[strategySizeAlgorithm]] to _sizeAlgorithm_. - 1. Set _controller_.[[strategyHWM]] to _highWaterMark_. - 1. Set _controller_.[[writeAlgorithm]] to _writeAlgorithm_. - 1. Set _controller_.[[closeAlgorithm]] to _closeAlgorithm_. - 1. Set _controller_.[[abortAlgorithm]] to _abortAlgorithm_. - 1. Let _backpressure_ be ! WritableStreamDefaultControllerGetBackpressure(_controller_). - 1. Perform ! WritableStreamUpdateBackpressure(_stream_, _backpressure_). - 1. Let _startResult_ be the result of performing _startAlgorithm_. (This may throw an exception.) - 1. Let _startPromise_ be a promise resolved with _startResult_. - 1. Upon fulfillment of _startPromise_, - 1. Assert: _stream_.[[state]] is `"writable"` or `"erroring"`. - 1. Set _controller_.[[started]] to *true*. - 1. Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(_controller_). - 1. Upon rejection of _startPromise_ with reason _r_, - 1. Assert: _stream_.[[state]] is `"writable"` or `"erroring"`. - 1. Set _controller_.[[started]] to *true*. - 1. Perform ! WritableStreamDealWithRejection(_stream_, _r_). - - -

SetUpWritableStreamDefaultControllerFromUnderlyingSink ( stream, underlyingSink, -highWaterMark, sizeAlgorithm )

- - - 1. Assert: _underlyingSink_ is not *undefined*. - 1. Let _controller_ be ObjectCreate(the original value of `WritableStreamDefaultController`'s `prototype` - property). - 1. Let _startAlgorithm_ be the following steps: - 1. Return ? InvokeOrNoop(_underlyingSink_, `"start"`, « _controller_ »). - 1. Let _writeAlgorithm_ be ? CreateAlgorithmFromUnderlyingMethod(_underlyingSink_, `"write"`, *1*, « _controller_ »). - 1. Let _closeAlgorithm_ be ? CreateAlgorithmFromUnderlyingMethod(_underlyingSink_, `"close"`, *0*, « »). - 1. Let _abortAlgorithm_ be ? CreateAlgorithmFromUnderlyingMethod(_underlyingSink_, `"abort"`, *1*, « »). - 1. Perform ? SetUpWritableStreamDefaultController(_stream_, _controller_, _startAlgorithm_, _writeAlgorithm_, - _closeAlgorithm_, _abortAlgorithm_, _highWaterMark_, _sizeAlgorithm_). - - -

WritableStreamDefaultControllerClearAlgorithms ( controller )

- -This abstract operation is called once the stream is closed or errored and the algorithms will not be executed any more. -By removing the algorithm references it permits the underlying sink object to be garbage collected even if the -{{WritableStream}} itself is still referenced. - -

The results of this algorithm are not currently observable, but could become so if JavaScript eventually -adds weak references. But even without that factor, -implementations will likely want to include similar steps.

- -

This operation will be performed multiple times in some edge cases. After the first time it will do -nothing.

- - - 1. Set _controller_.[[writeAlgorithm]] to *undefined*. - 1. Set _controller_.[[closeAlgorithm]] to *undefined*. - 1. Set _controller_.[[abortAlgorithm]] to *undefined*. - 1. Set _controller_.[[strategySizeAlgorithm]] to *undefined*. - - -

WritableStreamDefaultControllerClose ( controller )

- - - 1. Perform ! EnqueueValueWithSize(_controller_, `"close"`, *0*). - 1. Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(_controller_). - - -

WritableStreamDefaultControllerGetChunkSize ( controller, chunk )

- - - 1. Let _returnValue_ be the result of performing _controller_.[[strategySizeAlgorithm]], passing in _chunk_, and - interpreting the result as an ECMAScript completion value. - 1. If _returnValue_ is an abrupt completion, - 1. Perform ! WritableStreamDefaultControllerErrorIfNeeded(_controller_, _returnValue_.[[Value]]). - 1. Return 1. - 1. Return _returnValue_.[[Value]]. - - -

WritableStreamDefaultControllerGetDesiredSize ( controller )

- - - 1. Return _controller_.[[strategyHWM]] − _controller_.[[queueTotalSize]]. - - -

WritableStreamDefaultControllerWrite ( controller, chunk, chunkSize -)

- - - 1. Let _writeRecord_ be Record {[[chunk]]: _chunk_}. - 1. Let _enqueueResult_ be EnqueueValueWithSize(_controller_, _writeRecord_, _chunkSize_). - 1. If _enqueueResult_ is an abrupt completion, - 1. Perform ! WritableStreamDefaultControllerErrorIfNeeded(_controller_, _enqueueResult_.[[Value]]). - 1. Return. - 1. Let _stream_ be _controller_.[[controlledWritableStream]]. - 1. If ! WritableStreamCloseQueuedOrInFlight(_stream_) is *false* and _stream_.[[state]] is `"writable"`, - 1. Let _backpressure_ be ! WritableStreamDefaultControllerGetBackpressure(_controller_). - 1. Perform ! WritableStreamUpdateBackpressure(_stream_, _backpressure_). - 1. Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(_controller_). - - -

WritableStreamDefaultControllerAdvanceQueueIfNeeded ( -controller )

- - - 1. Let _stream_ be _controller_.[[controlledWritableStream]]. - 1. If _controller_.[[started]] is *false*, return. - 1. If _stream_.[[inFlightWriteRequest]] is not *undefined*, return. - 1. Let _state_ be _stream_.[[state]]. - 1. Assert: _state_ is not `"closed"` or `"errored"`. - 1. If _state_ is `"erroring"`, - 1. Perform ! WritableStreamFinishErroring(_stream_). - 1. Return. - 1. If _controller_.[[queue]] is empty, return. - 1. Let _writeRecord_ be ! PeekQueueValue(_controller_). - 1. If _writeRecord_ is `"close"`, perform ! WritableStreamDefaultControllerProcessClose(_controller_). - 1. Otherwise, perform ! WritableStreamDefaultControllerProcessWrite(_controller_, _writeRecord_.[[chunk]]). - - -

WritableStreamDefaultControllerErrorIfNeeded ( controller, error )

- - - 1. If _controller_.[[controlledWritableStream]].[[state]] is `"writable"`, perform ! - WritableStreamDefaultControllerError(_controller_, _error_). - - -

WritableStreamDefaultControllerProcessClose ( controller )

- - - 1. Let _stream_ be _controller_.[[controlledWritableStream]]. - 1. Perform ! WritableStreamMarkCloseRequestInFlight(_stream_). - 1. Perform ! DequeueValue(_controller_). - 1. Assert: _controller_.[[queue]] is empty. - 1. Let _sinkClosePromise_ be the result of performing _controller_.[[closeAlgorithm]]. - 1. Perform ! WritableStreamDefaultControllerClearAlgorithms(_controller_). - 1. Upon fulfillment of _sinkClosePromise_, - 1. Perform ! WritableStreamFinishInFlightClose(_stream_). - 1. Upon rejection of _sinkClosePromise_ with reason _reason_, - 1. Perform ! WritableStreamFinishInFlightCloseWithError(_stream_, _reason_). - - -

WritableStreamDefaultControllerProcessWrite ( controller, chunk )

- - - 1. Let _stream_ be _controller_.[[controlledWritableStream]]. - 1. Perform ! WritableStreamMarkFirstWriteRequestInFlight(_stream_). - 1. Let _sinkWritePromise_ be the result of performing _controller_.[[writeAlgorithm]], passing in _chunk_. - 1. Upon fulfillment of _sinkWritePromise_, - 1. Perform ! WritableStreamFinishInFlightWrite(_stream_). - 1. Let _state_ be _stream_.[[state]]. - 1. Assert: _state_ is `"writable"` or `"erroring"`. - 1. Perform ! DequeueValue(_controller_). - 1. If ! WritableStreamCloseQueuedOrInFlight(_stream_) is *false* and _state_ is `"writable"`, - 1. Let _backpressure_ be ! WritableStreamDefaultControllerGetBackpressure(_controller_). - 1. Perform ! WritableStreamUpdateBackpressure(_stream_, _backpressure_). - 1. Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(_controller_). - 1. Upon rejection of _sinkWritePromise_ with _reason_, - 1. If _stream_.[[state]] is `"writable"`, perform ! WritableStreamDefaultControllerClearAlgorithms(_controller_). - 1. Perform ! WritableStreamFinishInFlightWriteWithError(_stream_, _reason_). - - -

WritableStreamDefaultControllerGetBackpressure ( controller )

- - - 1. Let _desiredSize_ be ! WritableStreamDefaultControllerGetDesiredSize(_controller_). - 1. Return _desiredSize_ ≤ *0*. - - -

WritableStreamDefaultControllerError ( controller, error )

- - - 1. Let _stream_ be _controller_.[[controlledWritableStream]]. - 1. Assert: _stream_.[[state]] is `"writable"`. - 1. Perform ! WritableStreamDefaultControllerClearAlgorithms(_controller_). - 1. Perform ! WritableStreamStartErroring(_stream_, _error_). - +
+ \[[AbortSteps]](|reason|) implements the + [$WritableStreamController/[[AbortSteps]]$] contract. It performs the following steps: + + 1. Let |result| be the result of performing [=this=].\[[abortAlgorithm]], passing |reason|. + 1. Perform ! [$WritableStreamDefaultControllerClearAlgorithms$]([=this=]). + 1. Return |result|. +
+ +
+ \[[ErrorSteps]]() implements the + [$WritableStreamController/[[ErrorSteps]]$] contract. It performs the following steps: + + 1. Perform ! ResetQueue([=this=]). +
+ +

Abstract operations

+ +

Working with writable streams

+ +The following abstract operations operate on {{WritableStream}} instances at a higher level. Some +are even meant to be generally useful by other specifications. + +
+ AcquireWritableStreamDefaultWriter(|stream|) is meant to be called from other + specifications that wish to acquire a [=writer=] for a given writable stream. It performs the + following steps: + + 1. Let |writer| be a [=new=] {{WritableStreamDefaultWriter}}. + 1. Perform ? [$SetUpWritableStreamDefaultWriter$](|writer|, |stream|). + 1. Return |writer|. +
+ +
+ CreateWritableStream(|startAlgorithm|, |writeAlgorithm|, |closeAlgorithm|, + |abortAlgorithm|[, |highWaterMark|[, |sizeAlgorithm|]]) is meant to be called from other + specifications that wish to create {{WritableStream}} instances. The |writeAlgorithm|, + |closeAlgorithm|, and |abortAlgorithm| algorithms must return promises; if supplied, + |sizeAlgorithm| must be an algorithm accepting [=chunk=] objects and returning a number; and if + supplied, |highWaterMark| must be a non-negative, non-NaN number. + + It performs the following steps: + + 1. If |highWaterMark| was not passed, set it to 1. + 1. If |sizeAlgorithm| was not passed, set it to an algorithm that returns 1. + 1. Assert: ! [$IsNonNegativeNumber$](|highWaterMark|) is true. + 1. Let |stream| be a [=new=] {{WritableStream}}. + 1. Perform ! [$InitializeWritableStream$](|stream|). + 1. Let |controller| be a [=new=] {{WritableStreamDefaultController}}. + 1. Perform ? [$SetUpWritableStreamDefaultController$](|stream|, |controller|, |startAlgorithm|, + |writeAlgorithm|, |closeAlgorithm|, |abortAlgorithm|, |highWaterMark|, |sizeAlgorithm|). + 1. Return |stream|. + +

This abstract operation will throw an exception if and only if the supplied + |startAlgorithm| throws. +

+ +
+ InitializeWritableStream(|stream|) performs the following + steps: + + 1. Set |stream|.\[[state]] to "`writable`". + 1. Set |stream|.\[[storedError]], |stream|.\[[writer]], |stream|.\[[writableStreamController]], + |stream|.\[[inFlightWriteRequest]], |stream|.\[[closeRequest]], + |stream|.\[[inFlightCloseRequest]] and |stream|.\[[pendingAbortRequest]] to undefined. + 1. Set |stream|.\[[writeRequests]] to a new empty [=list=]. + 1. Set |stream|.\[[backpressure]] to false. +
+ +
+ IsWritableStreamLocked(|stream|) is meant to be called from + other specifications that wish to query whether or not a writable stream is [=locked to a writer=]. + It performs the following steps: + + 1. If |stream|.\[[writer]] is undefined, return false. + 1. Return true. +
+ +
+ SetUpWritableStreamDefaultWriter(|writer|, + |stream|) performs the following steps: + + 1. If ! [$IsWritableStreamLocked$](|stream|) is true, throw a {{TypeError}} exception. + 1. Set |writer|.\[[ownerWritableStream]] to |stream|. + 1. Set |stream|.\[[writer]] to |writer|. + 1. Let |state| be |stream|.\[[state]]. + 1. If |state| is "`writable`", + 1. If ! [$WritableStreamCloseQueuedOrInFlight$](|stream|) is false and |stream|.\[[backpressure]] + is true, set |writer|.\[[readyPromise]] to [=a new promise=]. + 1. Otherwise, set |writer|.\[[readyPromise]] to [=a promise resolved with=] undefined. + 1. Set |writer|.\[[closedPromise]] to [=a new promise=]. + 1. Otherwise, if |state| is "`erroring`", + 1. Set |writer|.\[[readyPromise]] to [=a promise rejected with=] |stream|.\[[storedError]]. + 1. Set |writer|.\[[readyPromise]].\[[PromiseIsHandled]] to true. + 1. Set |writer|.\[[closedPromise]] to [=a new promise=]. + 1. Otherwise, if |state| is "`closed`", + 1. Set |writer|.\[[readyPromise]] to [=a promise resolved with=] undefined. + 1. Set |writer|.\[[closedPromise]] to [=a promise resolved with=] undefined. + 1. Otherwise, + 1. Assert: |state| is "`errored`". + 1. Let |storedError| be |stream|.\[[storedError]]. + 1. Set |writer|.\[[readyPromise]] to [=a promise rejected with=] |storedError|. + 1. Set |writer|.\[[readyPromise]].\[[PromiseIsHandled]] to true. + 1. Set |writer|.\[[closedPromise]] to [=a promise rejected with=] |storedError|. + 1. Set |writer|.\[[closedPromise]].\[[PromiseIsHandled]] to true. +
+ +
+ WritableStreamAbort(|stream|, |reason|) performs the following + steps: + + 1. Let |state| be |stream|.\[[state]]. + 1. If |state| is "`closed"` or `"errored`", return [=a promise resolved with=] undefined. + 1. If |stream|.\[[pendingAbortRequest]] is not undefined, return + |stream|.\[[pendingAbortRequest]].\[[promise]]. + 1. Assert: |state| is "`writable"` or `"erroring`". + 1. Let |wasAlreadyErroring| be false. + 1. If |state| is "`erroring`", + 1. Set |wasAlreadyErroring| to true. + 1. Set |reason| to undefined. + 1. Let |promise| be [=a new promise=]. + 1. Set |stream|.\[[pendingAbortRequest]] to Record {\[[promise]]: |promise|, \[[reason]]: |reason|, + \[[wasAlreadyErroring]]: |wasAlreadyErroring|}. + 1. If |wasAlreadyErroring| is false, perform ! [$WritableStreamStartErroring$](|stream|, |reason|). + 1. Return |promise|. +
+ +
+ WritableStreamClose(|stream|) performs the following steps: + + 1. Let |state| be |stream|.\[[state]]. + 1. If |state| is "`closed"` or `"errored`", return [=a promise rejected with=] a {{TypeError}} + exception. + 1. Assert: |state| is "`writable"` or `"erroring`". + 1. Assert: ! [$WritableStreamCloseQueuedOrInFlight$](|stream|) is false. + 1. Let |promise| be [=a new promise=]. + 1. Set |stream|.\[[closeRequest]] to |promise|. + 1. Let |writer| be |stream|.\[[writer]]. + 1. If |writer| is not undefined, and |stream|.\[[backpressure]] is true, and |state| is + "`writable`", [=resolve=] |writer|.\[[readyPromise]] with undefined. + 1. Perform ! [$WritableStreamDefaultControllerClose$](|stream|.\[[writableStreamController]]). + 1. Return |promise|. +
+ +

Interfacing with controllers

+ +To allow future flexibility to add different writable stream behaviors (similar to the distinction +between default readable streams and [=readable byte streams=]), much of the internal state of a +[=writable stream=] is encapsulated by the {{WritableStreamDefaultController}} class. + +Each controller class defines two internal methods, which are called by the {{WritableStream}} +algorithms: + +
+
\[[AbortSteps]](reason) +
The controller's steps that run in reaction to the stream being [=abort a writable + stream|aborted=], used to clean up the state stored in the controller and inform the + [=underlying sink=]. + +
\[[ErrorSteps]]() +
The controller's steps that run in reaction to the stream being errored, used to clean up the + state stored in the controller. +
+ +(These are defined as internal methods, instead of as abstract operations, so that they can be +called polymorphically by the {{WritableStream}} algorithms, without having to branch on which type +of controller is present. This is a bit theoretical for now, given that only +{{WritableStreamDefaultController}} exists so far.) + +The rest of this section concerns abstract operations that go in the other direction: they are used +by the controller implementation to affect its associated {{WritableStream}} object. This +translates internal state changes of the controllerinto developer-facing results visible through +the {{WritableStream}}'s public API. + +
+ WritableStreamAddWriteRequest(|stream|) performs the + following steps: + + 1. Assert: ! [$IsWritableStreamLocked$](|stream|) is true. + 1. Assert: |stream|.\[[state]] is "`writable`". + 1. Let |promise| be [=a new promise=]. + 1. [=list/Append=] |promise| to |stream|.\[[writeRequests]]. + 1. Return |promise|. +
+ +
+ WritableStreamCloseQueuedOrInFlight(|stream|) + performs the following steps: + + 1. If |stream|.\[[closeRequest]] is undefined and |stream|.\[[inFlightCloseRequest]] is undefined, + return false. + 1. Return true. +
+ +
+ WritableStreamDealWithRejection(|stream|, |error|) + performs the following steps: + + 1. Let |state| be |stream|.\[[state]]. + 1. If |state| is "`writable`", + 1. Perform ! [$WritableStreamStartErroring$](|stream|, |error|). + 1. Return. + 1. Assert: |state| is "`erroring`". + 1. Perform ! [$WritableStreamFinishErroring$](|stream|). +
+ +
+ WritableStreamFinishErroring(|stream|, |reason|) + performs the following steps: + + 1. Assert: |stream|.\[[state]] is "`erroring`". + 1. Assert: ! [$WritableStreamHasOperationMarkedInFlight$](|stream|) is false. + 1. Set |stream|.\[[state]] to "`errored`". + 1. Perform ! |stream|.\[[writableStreamController]].\[[ErrorSteps]](). + 1. Let |storedError| be |stream|.\[[storedError]]. + 1. [=list/For each=] |writeRequest| of |stream|.\[[writeRequests]]: + 1. [=Reject=] |writeRequest| with |storedError|. + 1. Set |stream|.\[[writeRequests]] to an empty [=list=]. + 1. If |stream|.\[[pendingAbortRequest]] is undefined, + 1. Perform ! [$WritableStreamRejectCloseAndClosedPromiseIfNeeded$](|stream|). + 1. Return. + 1. Let |abortRequest| be |stream|.\[[pendingAbortRequest]]. + 1. Set |stream|.\[[pendingAbortRequest]] to undefined. + 1. If |abortRequest|.\[[wasAlreadyErroring]] is true, + 1. [=Reject=] |abortRequest|.\[[promise]] with |storedError|. + 1. Perform ! [$WritableStreamRejectCloseAndClosedPromiseIfNeeded$](|stream|). + 1. Return. + 1. Let |promise| be ! + stream.\[[writableStreamController]].\[[AbortSteps]](|abortRequest|.\[[reason]]). + 1. [=Upon fulfillment=] of |promise|, + 1. [=Resolve=] |abortRequest|.\[[promise]] with undefined. + 1. Perform ! [$WritableStreamRejectCloseAndClosedPromiseIfNeeded$](|stream|). + 1. [=Upon rejection=] of |promise| with reason |reason|, + 1. [=Reject=] |abortRequest|.\[[promise]] with |reason|. + 1. Perform ! [$WritableStreamRejectCloseAndClosedPromiseIfNeeded$](|stream|). +
+ +
+ WritableStreamFinishInFlightClose(|stream|) + performs the following steps: + + 1. Assert: |stream|.\[[inFlightCloseRequest]] is not undefined. + 1. [=Resolve=] |stream|.\[[inFlightCloseRequest]] with undefined. + 1. Set |stream|.\[[inFlightCloseRequest]] to undefined. + 1. Let |state| be |stream|.\[[state]]. + 1. Assert: |stream|.\[[state]] is "`writable"` or `"erroring`". + 1. If |state| is "`erroring`", + 1. Set |stream|.\[[storedError]] to undefined. + 1. If |stream|.\[[pendingAbortRequest]] is not undefined, + 1. [=Resolve=] |stream|.\[[pendingAbortRequest]].\[[promise]] with undefined. + 1. Set |stream|.\[[pendingAbortRequest]] to undefined. + 1. Set |stream|.\[[state]] to "`closed`". + 1. Let |writer| be |stream|.\[[writer]]. + 1. If |writer| is not undefined, [=resolve=] |writer|.\[[closedPromise]] with undefined. + 1. Assert: |stream|.\[[pendingAbortRequest]] is undefined. + 1. Assert: |stream|.\[[storedError]] is undefined. +
+ +
+ WritableStreamFinishInFlightCloseWithError(|stream|, + |error|) performs the following steps: + + 1. Assert: |stream|.\[[inFlightCloseRequest]] is not undefined. + 1. [=Reject=] |stream|.\[[inFlightCloseRequest]] with |error|. + 1. Set |stream|.\[[inFlightCloseRequest]] to undefined. + 1. Assert: |stream|.\[[state]] is "`writable"` or `"erroring`". + 1. If |stream|.\[[pendingAbortRequest]] is not undefined, + 1. [=Reject=] |stream|.\[[pendingAbortRequest]].\[[promise]] with |error|. + 1. Set |stream|.\[[pendingAbortRequest]] to undefined. + 1. Perform ! WritableStreamDealWithRejection(|stream|, |error|). +
+ +
+ WritableStreamFinishInFlightWrite(|stream|) + performs the following steps: + + 1. Assert: |stream|.\[[inFlightWriteRequest]] is not undefined. + 1. [=Resolve=] |stream|.\[[inFlightWriteRequest]] with undefined. + 1. Set |stream|.\[[inFlightWriteRequest]] to undefined. +
+ +
+ WritableStreamFinishInFlightWriteWithError(|stream|, + |error|) performs the following steps: + + 1. Assert: |stream|.\[[inFlightWriteRequest]] is not undefined. + 1. [=Reject=] |stream|.\[[inFlightWriteRequest]] with |error|. + 1. Set |stream|.\[[inFlightWriteRequest]] to undefined. + 1. Assert: |stream|.\[[state]] is "`writable"` or `"erroring`". + 1. Perform ! [$WritableStreamDealWithRejection$](|stream|, |error|). +
+ +
+ WritableStreamHasOperationMarkedInFlight(|stream|) + performs the following steps: + + 1. If |stream|.\[[inFlightWriteRequest]] is undefined and + |stream|.\[[writableStreamController]].\[[inFlightCloseRequest]] is undefined, return false. + 1. Return true. +
+ +
+ WritableStreamMarkCloseRequestInFlight(|stream|) + performs the following steps: + + 1. Assert: |stream|.\[[inFlightCloseRequest]] is undefined. + 1. Assert: |stream|.\[[closeRequest]] is not undefined. + 1. Set |stream|.\[[inFlightCloseRequest]] to |stream|.\[[closeRequest]]. + 1. Set |stream|.\[[closeRequest]] to undefined. +
+ +
+ WritableStreamMarkFirstWriteRequestInFlight(|stream|) + performs the following steps: + + 1. Assert: |stream|.\[[inFlightWriteRequest]] is undefined. + 1. Assert: |stream|.\[[writeRequests]] is not empty. + 1. Let |writeRequest| be |stream|.\[[writeRequests]][0]. + 1. [=list/Remove=] |writeRequest| from |stream|.\[[writeRequests]]. + 1. Set |stream|.\[[inFlightWriteRequest]] to |writeRequest|. +
+ +
+ WritableStreamRejectCloseAndClosedPromiseIfNeeded(|stream|) + performs the following steps: + + 1. Assert: |stream|.\[[state]] is "`errored`". + 1. If |stream|.\[[closeRequest]] is not undefined, + 1. Assert: |stream|.\[[inFlightCloseRequest]] is undefined. + 1. [=Reject=] |stream|.\[[closeRequest]] with |stream|.\[[storedError]]. + 1. Set |stream|.\[[closeRequest]] to undefined. + 1. Let |writer| be |stream|.\[[writer]]. + 1. If |writer| is not undefined, + 1. [=Reject=] |writer|.\[[closedPromise]] with |stream|.\[[storedError]]. + 1. Set |writer|.\[[closedPromise]].\[[PromiseIsHandled]] to true. +
+ +
+ WritableStreamStartErroring(|stream|, |reason|) + performs the following steps: + + 1. Assert: |stream|.\[[storedError]] is undefined. + 1. Assert: |stream|.\[[state]] is "`writable`". + 1. Let |controller| be |stream|.\[[writableStreamController]]. + 1. Assert: |controller| is not undefined. + 1. Set |stream|.\[[state]] to "`erroring`". + 1. Set |stream|.\[[storedError]] to |reason|. + 1. Let |writer| be |stream|.\[[writer]]. + 1. If |writer| is not undefined, perform ! + [$WritableStreamDefaultWriterEnsureReadyPromiseRejected$](|writer|, |reason|). + 1. If ! [$WritableStreamHasOperationMarkedInFlight$](|stream|) is false and + |controller|.\[[started]] is true, perform ! [$WritableStreamFinishErroring$](|stream|). +
+ +
+ WritableStreamUpdateBackpressure(|stream|, + |backpressure|) performs the following steps: + + 1. Assert: |stream|.\[[state]] is "`writable`". + 1. Assert: ! [$WritableStreamCloseQueuedOrInFlight$](|stream|) is false. + 1. Let |writer| be |stream|.\[[writer]]. + 1. If |writer| is not undefined and |backpressure| is not |stream|.\[[backpressure]], + 1. If |backpressure| is true, set |writer|.\[[readyPromise]] to [=a new promise=]. + 1. Otherwise, + 1. Assert: |backpressure| is false. + 1. [=Resolve=] |writer|.\[[readyPromise]] with undefined. + 1. Set |stream|.\[[backpressure]] to |backpressure|. +
+ +

Writers

+ +The following abstract operations support the implementation and manipualtion of +{{WritableStreamDefaultWriter}} instances. + +
+ WritableStreamDefaultWriterAbort(|writer|, + |reason|) performs the following steps: + + 1. Let |stream| be |writer|.\[[ownerWritableStream]]. + 1. Assert: |stream| is not undefined. + 1. Return ! [$WritableStreamAbort$](|stream|, |reason|). +
+ +
+ WritableStreamDefaultWriterClose(|writer|) performs + the following steps: + + 1. Let |stream| be |writer|.\[[ownerWritableStream]]. + 1. Assert: |stream| is not undefined. + 1. Return ! [$WritableStreamClose$](|stream|). +
+ +
+ WritableStreamDefaultWriterCloseWithErrorPropagation(|writer|) + performs the following steps: + + 1. Let |stream| be |writer|.\[[ownerWritableStream]]. + 1. Assert: |stream| is not undefined. + 1. Let |state| be |stream|.\[[state]]. + 1. If ! [$WritableStreamCloseQueuedOrInFlight$](|stream|) is true or |state| is "`closed`", return + [=a promise resolved with=] undefined. + 1. If |state| is "`errored`", return [=a promise rejected with=] |stream|.\[[storedError]]. + 1. Assert: |state| is "`writable"` or `"erroring`". + 1. Return ! [$WritableStreamDefaultWriterClose$](|writer|). + +

This abstract operation helps implement the error propagation semantics of + {{ReadableStream}}'s {{ReadableStream/pipeTo()}}. +

+ +
+ WritableStreamDefaultWriterEnsureClosedPromiseRejected(|writer|, + |error|) performs the following steps: + + 1. If |writer|.\[[closedPromise]].\[[PromiseState]] is "`pending`", [=reject=] + |writer|.\[[closedPromise]] with |error|. + 1. Otherwise, set |writer|.\[[closedPromise]] to [=a promise rejected with=] |error|. + 1. Set |writer|.\[[closedPromise]].\[[PromiseIsHandled]] to true. +
+ +
+ WritableStreamDefaultWriterEnsureReadyPromiseRejected(|writer|, + |error|) performs the following steps: + + 1. If |writer|.\[[readyPromise]].\[[PromiseState]] is "`pending`", [=reject=] + |writer|.\[[readyPromise]] with |error|. + 1. Otherwise, set |writer|.\[[readyPromise]] to [=a promise rejected with=] |error|. + 1. Set |writer|.\[[readyPromise]].\[[PromiseIsHandled]] to true. +
+ +
+ WritableStreamDefaultWriterGetDesiredSize(|writer|) + performs the following steps: + + 1. Let |stream| be |writer|.\[[ownerWritableStream]]. + 1. Let |state| be |stream|.\[[state]]. + 1. If |state| is "`errored"` or `"erroring`", return null. + 1. If |state| is "`closed`", return 0. + 1. Return ! + [$WritableStreamDefaultControllerGetDesiredSize$](|stream|.\[[writableStreamController]]). +
+ +
+ WritableStreamDefaultWriterRelease(|writer|) + performs the following steps: + + 1. Let |stream| be |writer|.\[[ownerWritableStream]]. + 1. Assert: |stream| is not undefined. + 1. Assert: |stream|.\[[writer]] is |writer|. + 1. Let |releasedError| be a new {{TypeError}}. + 1. Perform ! [$WritableStreamDefaultWriterEnsureReadyPromiseRejected$](|writer|, |releasedError|). + 1. Perform ! [$WritableStreamDefaultWriterEnsureClosedPromiseRejected$](|writer|, |releasedError|). + 1. Set |stream|.\[[writer]] to undefined. + 1. Set |writer|.\[[ownerWritableStream]] to undefined. +
+ +
+ WritableStreamDefaultWriterWrite(|writer|, |chunk|) + performs the following steps: + + 1. Let |stream| be |writer|.\[[ownerWritableStream]]. + 1. Assert: |stream| is not undefined. + 1. Let |controller| be |stream|.\[[writableStreamController]]. + 1. Let |chunkSize| be ! [$WritableStreamDefaultControllerGetChunkSize$](|controller|, |chunk|). + 1. If |stream| is not equal to |writer|.\[[ownerWritableStream]], return [=a promise rejected + with=] a {{TypeError}} exception. + 1. Let |state| be |stream|.\[[state]]. + 1. If |state| is "`errored`", return [=a promise rejected with=] |stream|.\[[storedError]]. + 1. If ! [$WritableStreamCloseQueuedOrInFlight$](|stream|) is true or |state| is "`closed`", return + [=a promise rejected with=] a {{TypeError}} exception indicating that the stream is closing or + closed. + 1. If |state| is "`erroring`", return [=a promise rejected with=] |stream|.\[[storedError]]. + 1. Assert: |state| is "`writable`". + 1. Let |promise| be ! [$WritableStreamAddWriteRequest$](|stream|). + 1. Perform ! [$WritableStreamDefaultControllerWrite$](|controller|, |chunk|, |chunkSize|). + 1. Return |promise|. +
+ +

Default controllers

+ +The following abstract operations support the implementation of the +{{WritableStreamDefaultController}} class. + + +
+ SetUpWritableStreamDefaultController(|stream|, + |controller|, |startAlgorithm|, |writeAlgorithm|, |closeAlgorithm|, |abortAlgorithm|, + |highWaterMark|, |sizeAlgorithm|) performs the following steps: + + 1. Assert: |stream| [=implements=] {{WritableStream}}. + 1. Assert: |stream|.\[[writableStreamController]] is undefined. + 1. Set |controller|.\[[controlledWritableStream]] to |stream|. + 1. Set |stream|.\[[writableStreamController]] to |controller|. + 1. Perform ! [$ResetQueue$](|controller|). + 1. Set |controller|.\[[started]] to false. + 1. Set |controller|.\[[strategySizeAlgorithm]] to |sizeAlgorithm|. + 1. Set |controller|.\[[strategyHWM]] to |highWaterMark|. + 1. Set |controller|.\[[writeAlgorithm]] to |writeAlgorithm|. + 1. Set |controller|.\[[closeAlgorithm]] to |closeAlgorithm|. + 1. Set |controller|.\[[abortAlgorithm]] to |abortAlgorithm|. + 1. Let |backpressure| be ! [$WritableStreamDefaultControllerGetBackpressure$](|controller|). + 1. Perform ! [$WritableStreamUpdateBackpressure$](|stream|, |backpressure|). + 1. Let |startResult| be the result of performing |startAlgorithm|. (This may throw an exception.) + 1. Let |startPromise| be [=a promise resolved with=] |startResult|. + 1. [=Upon fulfillment=] of |startPromise|, + 1. Assert: |stream|.\[[state]] is "`writable"` or `"erroring`". + 1. Set |controller|.\[[started]] to true. + 1. Perform ! [$WritableStreamDefaultControllerAdvanceQueueIfNeeded$](|controller|). + 1. [=Upon rejection=] of |startPromise| with reason |r|, + 1. Assert: |stream|.\[[state]] is "`writable"` or `"erroring`". + 1. Set |controller|.\[[started]] to true. + 1. Perform ! [$WritableStreamDealWithRejection$](|stream|, |r|). +
+ +
+ SetUpWritableStreamDefaultControllerFromUnderlyingSink(|stream|, + |underlyingSink|, |underlyingSinkDict|, |highWaterMark|, |sizeAlgorithm|) performs the + following steps: + + 1. Let |controller| be a [=new=] {{WritableStreamDefaultController}}. + 1. Let |startAlgorithm| be an algorithm that returns undefined. + 1. Let |writeAlgorithm| be an algorithm that returns [=a promise resolved with=] undefined. + 1. Let |closeAlgorithm| be an algorithm that returns [=a promise resolved with=] undefined. + 1. Let |abortAlgorithm| be an algorithm that returns [=a promise resolved with=] undefined. + 1. If |underlyingSinkDict|["{{UnderlyingSink/start}}"] [=map/exists=], then set |startAlgorithm| to + an algorithm which returns the result of [=invoking=] + |underlyingSinkDict|["{{UnderlyingSink/start}}"] with argument list « |controller| » + and [=callback this value=] |underlyingSink|. + 1. If |underlyingSinkDict|["{{UnderlyingSink/write}}"] [=map/exists=], then set |writeAlgorithm| to + an algorithm which takes an argument |chunk| and returns the result of [=invoking=] + |underlyingSinkDict|["{{UnderlyingSink/write}}"] with argument list « |chunk|, + |controller| » and [=callback this value=] |underlyingSink|. + 1. If |underlyingSinkDict|["{{UnderlyingSink/close}}"] [=map/exists=], then set |closeAlgorithm| to + an algorithm which returns the result of [=invoking=] + |underlyingSinkDict|["{{UnderlyingSink/close}}"] with argument list «» and [=callback this + value=] |underlyingSink|. + 1. If |underlyingSinkDict|["{{UnderlyingSink/abort}}"] [=map/exists=], then set |abortAlgorithm| to + an algorithm which takes an argument |reason| and returns the result of [=invoking=] + |underlyingSinkDict|["{{UnderlyingSink/abort}}"] with argument list « |reason| » and + [=callback this value=] |underlyingSink|. + 1. Perform ? [$SetUpWritableStreamDefaultController$](|stream|, |controller|, |startAlgorithm|, + |writeAlgorithm|, |closeAlgorithm|, |abortAlgorithm|, |highWaterMark|, |sizeAlgorithm|). +
+ +
+ WritableStreamDefaultControllerAdvanceQueueIfNeeded(|controller|) + performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledWritableStream]]. + 1. If |controller|.\[[started]] is false, return. + 1. If |stream|.\[[inFlightWriteRequest]] is not undefined, return. + 1. Let |state| be |stream|.\[[state]]. + 1. Assert: |state| is not "`closed"` or `"errored`". + 1. If |state| is "`erroring`", + 1. Perform ! [$WritableStreamFinishErroring$](|stream|). + 1. Return. + 1. If |controller|.\[[queue]] is empty, return. + 1. Let |writeRecord| be ! [$PeekQueueValue$](|controller|). + 1. If |writeRecord| is "`close`", perform ! + [$WritableStreamDefaultControllerProcessClose$](|controller|). + 1. Otherwise, perform ! [$WritableStreamDefaultControllerProcessWrite$](|controller|, + |writeRecord|.\[[chunk]]). +
+ +
+ WritableStreamDefaultControllerClearAlgorithms(|controller|) + is called once the stream is closed or errored and the algorithms will not be executed any more. By + removing the algorithm references it permits the [=underlying sink=] object to be garbage + collected even if the {{WritableStream}} itself is still referenced. + +

This is observable using weak + references. See tc39/proposal-weakrefs#31 for more + detail. + + It performs the following steps: + + 1. Set |controller|.\[[writeAlgorithm]] to undefined. + 1. Set |controller|.\[[closeAlgorithm]] to undefined. + 1. Set |controller|.\[[abortAlgorithm]] to undefined. + 1. Set |controller|.\[[strategySizeAlgorithm]] to undefined. + +

This algorithm will be performed multiple times in some edge cases. After the first + time it will do nothing. +

+ +
+ WritableStreamDefaultControllerClose(|controller|) + performs the following steps: + + 1. Perform ! [$EnqueueValueWithSize$](|controller|, "`close`", 0). + 1. Perform ! [$WritableStreamDefaultControllerAdvanceQueueIfNeeded$](|controller|). +
+ +
+ WritableStreamDefaultControllerError(|controller|, + |error|) performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledWritableStream]]. + 1. Assert: |stream|.\[[state]] is `"writable"`. + 1. Perform ! [$WritableStreamDefaultControllerClearAlgorithms$](|controller|). + 1. Perform ! [$WritableStreamStartErroring$](|stream|, |error|). +
+ +
+ WritableStreamDefaultControllerErrorIfNeeded(|controller|, + |error|) performs the following steps: + + 1. If |controller|.\[[controlledWritableStream]].\[[state]] is "`writable`", perform ! + [$WritableStreamDefaultControllerError$](|controller|, |error|). +
+ +
+ WritableStreamDefaultControllerGetChunkSize(|controller|, + |chunk|) performs the following steps: + + 1. Let |returnValue| be the result of performing |controller|.\[[strategySizeAlgorithm]], passing + in |chunk|, and interpreting the result as a [=completion record=]. + 1. If |returnValue| is an abrupt completion, + 1. Perform ! [$WritableStreamDefaultControllerErrorIfNeeded$](|controller|, + |returnValue|.\[[Value]]). + 1. Return 1. + 1. Return |returnValue|.\[[Value]]. +
+ +
+ WritableStreamDefaultControllerGetBackpressure(|controller|) + performs the following steps: + + 1. Let |desiredSize| be ! [$WritableStreamDefaultControllerGetDesiredSize$](|controller|). + 1. Return true if |desiredSize| ≤ 0, or false otherwise. +
+ +
+ WritableStreamDefaultControllerGetDesiredSize(|controller|) + performs the following steps: + + 1. Return |controller|.\[[strategyHWM]] − |controller|.\[[queueTotalSize]]. +
+ +
+ WritableStreamDefaultControllerProcessClose(|controller|) + performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledWritableStream]]. + 1. Perform ! [$WritableStreamMarkCloseRequestInFlight$](|stream|). + 1. Perform ! [$DequeueValue$](|controller|). + 1. Assert: |controller|.\[[queue]] is empty. + 1. Let |sinkClosePromise| be the result of performing |controller|.\[[closeAlgorithm]]. + 1. Perform ! [$WritableStreamDefaultControllerClearAlgorithms$](|controller|). + 1. [=Upon fulfillment=] of |sinkClosePromise|, + 1. Perform ! [$WritableStreamFinishInFlightClose$](|stream|). + 1. [=Upon rejection=] of |sinkClosePromise| with reason |reason|, + 1. Perform ! [$WritableStreamFinishInFlightCloseWithError$](|stream|, |reason|). +
+ +
+ WritableStreamDefaultControllerProcessWrite(|controller|, + |chunk|) performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledWritableStream]]. + 1. Perform ! [$WritableStreamMarkFirstWriteRequestInFlight$](|stream|). + 1. Let |sinkWritePromise| be the result of performing |controller|.\[[writeAlgorithm]], passing in + |chunk|. + 1. [=Upon fulfillment=] of |sinkWritePromise|, + 1. Perform ! [$WritableStreamFinishInFlightWrite$](|stream|). + 1. Let |state| be |stream|.\[[state]]. + 1. Assert: |state| is "`writable"` or `"erroring`". + 1. Perform ! [$DequeueValue$](|controller|). + 1. If ! [$WritableStreamCloseQueuedOrInFlight$](|stream|) is false and |state| is "`writable`", + 1. Let |backpressure| be ! [$WritableStreamDefaultControllerGetBackpressure$](|controller|). + 1. Perform ! [$WritableStreamUpdateBackpressure$](|stream|, |backpressure|). + 1. Perform ! [$WritableStreamDefaultControllerAdvanceQueueIfNeeded$](|controller|). + 1. [=Upon rejection=] of |sinkWritePromise| with |reason|, + 1. If |stream|.\[[state]] is "`writable`", perform ! + [$WritableStreamDefaultControllerClearAlgorithms$](|controller|). + 1. Perform ! [$WritableStreamFinishInFlightWriteWithError$](|stream|, |reason|). +
+ +
+ WritableStreamDefaultControllerWrite(|controller|, + |chunk|, |chunkSize|) performs the following steps: + + 1. Let |writeRecord| be Record {\[[chunk]]: |chunk|}. + 1. Let |enqueueResult| be [$EnqueueValueWithSize$](|controller|, |writeRecord|, |chunkSize|). + 1. If |enqueueResult| is an abrupt completion, + 1. Perform ! [$WritableStreamDefaultControllerErrorIfNeeded$](|controller|, + |enqueueResult|.\[[Value]]). + 1. Return. + 1. Let |stream| be |controller|.\[[controlledWritableStream]]. + 1. If ! [$WritableStreamCloseQueuedOrInFlight$](|stream|) is false and |stream|.\[[state]] is + "`writable`", + 1. Let |backpressure| be ! [$WritableStreamDefaultControllerGetBackpressure$](|controller|). + 1. Perform ! [$WritableStreamUpdateBackpressure$](|stream|, |backpressure|). + 1. Perform ! [$WritableStreamDefaultControllerAdvanceQueueIfNeeded$](|controller|). +

Transform streams

Using transform streams

- The natural way to use a transform stream is to place it in a pipe between a readable stream - and a writable stream. Chunks that travel from the readable stream to the writable stream - will be transformed as they pass through the transform stream. Backpressure is respected, so data will not be - read faster than it can be transformed and consumed. + The natural way to use a transform stream is to place it in a [=piping|pipe=] between a [=readable + stream=] and a [=writable stream=]. [=Chunks=] that travel from the [=readable stream=] to the + [=writable stream=] will be transformed as they pass through the transform stream. + [=Backpressure=] is respected, so data will not be read faster than it can be transformed and + consumed. + + + readableStream + .pipeThrough(transformStream) + .pipeTo(writableStream) + .then(() => console.log("All data successfully transformed!")) + .catch(e => console.error("Something went wrong!", e)); + +
-

-    readableStream
-      .pipeThrough(transformStream)
-      .pipeTo(writableStream)
-      .then(() => console.log("All data successfully transformed!"))
-      .catch(e => console.error("Something went wrong!", e));
-  
+
+ You can also use the {{TransformStream/readable}} and {{TransformStream/writable}} properties of a + transform stream directly to access the usual interfaces of a [=readable stream=] and [=writable + stream=]. In this example we supply data to the [=writable side=] of the stream using its + [=writer=] interface. The [=readable side=] is then piped to + anotherWritableStream. + + + const writer = transformStream.writable.getWriter(); + writer.write("input chunk"); + transformStream.readable.pipeTo(anotherWritableStream); +
-
- You can also use the {{TransformStream/readable}} and {{TransformStream/writable}} properties of a transform stream - directly to access the usual interfaces of a readable stream and writable stream. In this example we - supply data to the writable side of the stream using its writer interface. The readable side is - then piped to anotherWritableStream. +
+ One use of [=identity transform streams=] is to easily convert between readable and writable + streams. For example, the {{fetch()}} API accepts a readable stream [=request/body|request body=], + but it can be more convenient to write data for uploading via a writable stream interface. Using + an identity transform stream addresses this: + + + const { writable, readable } = new TransformStream(); + fetch("...", { body: readable }).then(response => /* ... */); + + const writer = writable.getWriter(); + writer.write(new Uint8Array([0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x73, 0x21])); + writer.close(); + + + Another use of identity transform streams is to add additional buffering to a [=pipe=]. In this + example we add extra buffering between readableStream and + writableStream. + + + const writableStrategy = new ByteLengthQueuingStrategy({ highWaterMark: 1024 * 1024 }); + + readableStream + .pipeThrough(new TransformStream(undefined, writableStrategy)) + .pipeTo(writableStream); + +
+ +

The {{TransformStream}} class

+ +The {{TransformStream}} class is a concrete instance of the general [=transform stream=] concept. + +

Interface definition

+ +The Web IDL definition for the {{TransformStream}} class is given as follows: + + +[Exposed=(Window,Worker,Worklet)] +interface TransformStream { + constructor(object transformer, + optional QueuingStrategy writableStrategy = {}, + optional QueuingStrategy readableStrategy = {}); + + readonly attribute ReadableStream readable; + readonly attribute WritableStream writable; +}; + + +

Internal slots

+ +Instances of {{TransformStream}} are created with the internal slots described in the following +table: + + + + + + + + + + + + +
Internal SlotDescription (non-normative)
\[[backpressure]] + Whether there was backpressure on \[[readable]] the last time it was + observed +
\[[backpressureChangePromise]] + A promise which is fulfilled and replaced every time the value of + \[[backpressure]] changes +
\[[readable]] + The {{ReadableStream}} instance controlled by this object +
\[[transformStreamController]] + A {{TransformStreamDefaultController}} created with the ability to + control \[[readable]] and \[[writable]] +
\[[writable]] + The {{WritableStream}} instance controlled by this object +
+ +

The transformer API

+ +The {{TransformStream()}} constructor accepts as its first argument a JavaScript object representing +the [=transformer=]. Such objects can contain any of the following methods: + + +dictionary Transformer { + TransformStreamControllerCallback start; + TransformStreamTransformCallback transform; + TransformStreamControllerCallback flush; + any readableType; + any writableType; +}; - <pre><code class="lang-javascript"> - const writer = transformStream.writable.getWriter(); - writer.write("input chunk"); - transformStream.readable.pipeTo(anotherWritableStream); - </code></pre> -</div> +callback TransformStreamControllerCallback = Promise<void> (TransformStreamDefaultController controller); +callback TransformStreamTransformCallback = Promise<void> (TransformStreamDefaultController controller, optional any chunk); + -
- One use of identity transform streams is to easily convert between readable and writable streams. For example, - the {{fetch()}} API accepts a readable stream request body, but it can be more - convenient to write data for uploading via a writable stream interface. Using an identity transform stream addresses - this: +
+
start(controller)
+
+

A function that is called immediately during creation of the {{TransformStream}}. + +

Typically this is used to enqueue prefix [=chunks=], using + {{TransformStreamDefaultController/enqueue()|controller.enqueue()}}. Those chunks will be read + from the [=readable side=] but don't depend on any writes to the [=writable side=]. + +

If this initial process is asynchronous, for example because it takes some effort to acquire + the prefix chunks, the function can return a promise to signal success or failure; a rejected + promise will error the stream. Any thrown exceptions will be re-thrown by the + {{TransformStream()}} constructor. + +

transform(chunk, controller)
+
+

A function called when a new [=chunk=] originally written to the [=writable side=] is ready to + be transformed. The stream implementation guarantees that this function will be called only after + previous transforms have succeeded, and never before {{Transformer/start|start()}} has completed + or after {{Transformer/flush|flush()}} has been called. + +

This function performs the actual transformation work of the transform stream. It can enqueue + the results using {{TransformStreamDefaultController/enqueue()|controller.enqueue()}}. This + permits a single chunk written to the writable side to result in zero or multiple chunks on the + [=readable side=], depending on how many times + {{TransformStreamDefaultController/enqueue()|controller.enqueue()}} is called. + [[#example-ts-lipfuzz]] demonstrates this by sometimes enqueuing zero chunks. + +

If the process of transforming is asynchronous, this function can return a promise to signal + success or failure of the transformation. A rejected promise will error both the readable and + writable sides of the transform stream. + +

If no {{Transformer/transform|transform()}} method is supplied, the identity transform is + used, which enqueues chunks unchanged from the writable side to the readable side. + +

flush(controller)
+
+

A function called after all [=chunks=] written to the [=writable side=] have been transformed + by successfully passing through {{Transformer/transform|transform()}}, and the writable side is + about to be closed. + +

Typically this is used to enqueue suffix chunks to the [=readable side=], before that too + becomes closed. An example can be seen in [[#example-ts-lipfuzz]]. + +

If the flushing process is asynchronous, the function can return a promise to signal success + or failure; the result will be communicated to the caller of + {{WritableStreamDefaultWriter/write()|stream.writable.write()}}. Additionally, a rejected + promise will error both the readable and writable sides of the stream. Throwing an exception is + treated the same as returning a rejected promise. + +

(Note that there is no need to call + {{TransformStreamDefaultController/terminate()|controller.terminate()}} inside + {{Transformer/flush|flush()}}; the stream is already in the process of successfully closing down, + and terminating it would be counterproductive.) + +

readableType
+
+

This property is reserved for future use, so any attempts to supply a value will throw an + exception. -


-    const { writable, readable } = new TransformStream();
-    fetch("...", { body: readable }).then(response => /* ... */);
+ 
writableType
+
+

This property is reserved for future use, so any attempts to supply a value will throw an + exception. +

- const writer = writable.getWriter(); - writer.write(new Uint8Array([0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x73, 0x21])); - writer.close(); -
+The controller object passed to {{Transformer/start|start()}}, +{{Transformer/transform|transform()}}, and {{Transformer/flush|flush()}} is an instance of +{{TransformStreamDefaultController}}, and has the ability to enqueue [=chunks=] to the [=readable +side=], or to terminate or error the stream. + +

Constructor and properties

+ +
+
stream = new {{TransformStream/constructor(transformer, writableStrategy, readableStrategy)|TransformStream}}(transformer[, writableStrategy[, readableStrategy]]) +
+

Creates a new {{TransformStream}} wrapping the provided [=transformer=]. See + [[#transformer-api]] for more details on the transformer argument. + +

The writableStrategy and readableStrategy arguments are + the [=queuing strategy=] objects for the [=writable side|writable=] and [=readable + side|readable=] sides respectively. These are used in the construction of the {{WritableStream}} + and {{ReadableStream}} objects and can be used to add buffering to a {{TransformStream}}, in + order to smooth out variations in the speed of the transformation, or to increase the amount of + buffering in a [=pipe=]. If they are not provided, the default behavior will be the same as a + {{CountQueuingStrategy}}, with respective [=high water marks=] of 1 and 0. + +

readable = stream.{{TransformStream/readable}} +
+

Returns a {{ReadableStream}} representing the [=readable side=] of this transform stream. + +

writable = stream.{{TransformStream/writable}} +
+

Returns a {{WritableStream}} representing the [=writable side=] of this transform stream. +

- Another use of identity transform streams is to add additional buffering to a pipe. In this example we add - extra buffering between readableStream and writableStream. +
+ The TransformStream(|transformer|, + |writableStrategy|, |readableStrategy|) constructor steps are: + + 1. Let |transformerDict| be |transformer|, [=converted to an IDL value=] of type {{Transformer}}. +

We cannot declare the |transformer| argument as having the {{Transformer}} type + directly, because doing so would lose the reference to the original object. We need to retain + the object so we can [=invoke=] the various methods on it. + 1. If |transformerDict|["{{Transformer/readableType}}"] [=map/exists=], throw a {{RangeError}} + exception. + 1. If |transformerDict|["{{Transformer/writableType}}"] [=map/exists=], throw a {{RangeError}} + exception. + 1. Let |writableSizeAlgorithm| be ? [$ExtractSizeAlgorithm$](|writableStrategy|). + 1. Let |writableHighWaterMark| be ? [$ExtractHighWaterMark$](|writableStrategy|, 1). + 1. Let |readableSizeAlgorithm| be ? [$ExtractSizeAlgorithm$](|readableStrategy|). + 1. Let |readableHighWaterMark| be ? [$ExtractHighWaterMark$](|readableStrategy|, 0). + 1. Let |startPromise| be [=a new promise=]. + 1. Perform ! [$InitializeTransformStream$]([=this=], |startPromise|, |writableHighWaterMark|, + |writableSizeAlgorithm|, |readableHighWaterMark|, |readableSizeAlgorithm|). + 1. Perform ? [$SetUpTransformStreamDefaultControllerFromTransformer$]([=this=], |transformer|, + |transformerDict|). + 1. Let |startPromise| be [=a new promise=]. + 1. If |transformerDict|["{{Transformer/start}}"] [=map/exists=], then [=resolve=] |startPromise| + with the result of [=invoking=] |transformerDict|["{{Transformer/start}}"] with argument list + « [=this=].\[[transformStreamController]] » and [=callback this value=] |transformer|. + 1. Otherwise, [=resolve=] |startPromise| with undefined. +

-

-    const writableStrategy = new ByteLengthQueuingStrategy({ highWaterMark: 1024 * 1024 });
+
+ The readable attribute's getter steps + are: - readableStream - .pipeThrough(new TransformStream(undefined, writableStrategy)) - .pipeTo(writableStream); -
+ 1. Return [=this=].\[[readable]].
-

Class TransformStream

+
+ The writable attribute's getter steps + are: -

Class definition

+ 1. Return [=this=].\[[writable]]. +
-
+

The {{TransformStreamDefaultController}} class

-This section is non-normative. +The {{TransformStreamDefaultController}} class has methods that allow manipulation of the +associated {{ReadableStream}} and {{WritableStream}}. When constructing a {{TransformStream}}, the +[=transformer=] object is given a corresponding {{TransformStreamDefaultController}} instance to +manipulate. -If one were to write the {{TransformStream}} class in something close to the syntax of [[!ECMASCRIPT]], it would look -like +

Interface definition

-

-  class TransformStream {
-    constructor(transformer = {}, writableStrategy = {}, readableStrategy = {})
+The Web IDL definition for the {{TransformStreamDefaultController}} class is given as follows:
 
-    get readable()
-    get writable()
-  }
-
+ +[Exposed=(Window,Worker,Worklet)] +interface TransformStreamDefaultController { + readonly attribute unrestricted double? desiredSize; -</div> + void enqueue(optional any chunk); + void error(optional any reason); + void terminate(); +}; + -

Internal slots

+

Internal slots

-Instances of {{TransformStream}} are created with the internal slots described in the following table: +Instances of {{TransformStreamDefaultController}} are created with the internal slots described in +the following table: - - - - - - + - + + + - + - + - - - +
Internal SlotDescription (non-normative)
\[[backpressure]] - Whether there was backpressure on \[[readable]] the last time it was observed -
Internal SlotDescription (non-normative)
\[[backpressureChangePromise]] - A promise which is fulfilled and replaced every time the value of \[[backpressure]] - changes -
\[[controlledTransformStream]] + The {{TransformStream}} instance controlled
\[[readable]] - The {{ReadableStream}} instance controlled by this object -
\[[flushAlgorithm]] + A promise-returning algorithm which communicates a requested close to + the [=transformer=]
\[[transformStreamController]] - A {{TransformStreamDefaultController}} created with the ability to control \[[readable]] - and \[[writable]]; also used for the IsTransformStream brand check -
\[[writable]] - The {{WritableStream}} instance controlled by this object -
\[[transformAlgorithm]] + A promise-returning algorithm, taking one argument (the [=chunk=] to + transform), which requests the [=transformer=] perform its transformation
-

new TransformStream(transformer = {}, writableStrategy = {}, -readableStrategy = {})

+

Methods and properties

+ +
+
desiredSize = controller.{{TransformStreamDefaultController/desiredSize}} +
+

Returns the [=desired size to fill a stream's internal queue|desired size to fill the + readable side's internal queue=]. It can be negative, if the queue is over-full. + +

controller.{{TransformStreamDefaultController/enqueue()|enqueue}}(chunk) +
+

Enqueues the given [=chunk=] chunk in the [=readable side=] of the controlled + transform stream. + +

controller.{{TransformStreamDefaultController/error()|error}}(e) +
+

Errors the both the [=readable side=] and the [=writable side=] of the controlled transform + stream, making all future interactions with it fail with the given error e. Any + [=chunks=] queued for transformation will be discarded. + +

controller.{{TransformStreamDefaultController/terminate()|terminate}}() +
+

Closes the [=readable side=] and errors the [=writable side=] of the controlled transform + stream. This is useful when the [=transformer=] only needs to consume a portion of the [=chunks=] + written to the [=writable side=]. +

-
- The transformer argument represents the transformer, as described in [[#transformer-api]]. - - The writableStrategy and readableStrategy arguments are the queuing strategy objects - for the writable and readable sides respectively. These are used in the construction of the {{WritableStream}} and - {{ReadableStream}} objects and can be used to add buffering to a {{TransformStream}}, in order to smooth out - variations in the speed of the transformation, or to increase the amount of buffering in a pipe. If they are - not provided, the default behavior will be the same as a {{CountQueuingStrategy}}, with respective high water - marks of 1 and 0. -
- - - 1. Let _writableSizeFunction_ be ? GetV(_writableStrategy_, `"size"`). - 1. Let _writableHighWaterMark_ be ? GetV(_writableStrategy_, `"highWaterMark"`). - 1. Let _readableSizeFunction_ be ? GetV(_readableStrategy_, `"size"`). - 1. Let _readableHighWaterMark_ be ? GetV(_readableStrategy_, `"highWaterMark"`). - 1. Let _writableType_ be ? GetV(_transformer_, `"writableType"`). - 1. If _writableType_ is not *undefined*, throw a *RangeError* exception. - 1. Let _writableSizeAlgorithm_ be ? MakeSizeAlgorithmFromSizeFunction(_writableSizeFunction_). - 1. If _writableHighWaterMark_ is *undefined*, set _writableHighWaterMark_ to *1*. - 1. Set _writableHighWaterMark_ to ? ValidateAndNormalizeHighWaterMark(_writableHighWaterMark_). - 1. Let _readableType_ be ? GetV(_transformer_, `"readableType"`). - 1. If _readableType_ is not *undefined*, throw a *RangeError* exception. - 1. Let _readableSizeAlgorithm_ be ? MakeSizeAlgorithmFromSizeFunction(_readableSizeFunction_). - 1. If _readableHighWaterMark_ is *undefined*, set _readableHighWaterMark_ to *0*. - 1. Set _readableHighWaterMark_ be ? ValidateAndNormalizeHighWaterMark(_readableHighWaterMark_). - 1. Let _startPromise_ be a new promise. - 1. Perform ! InitializeTransformStream(*this*, _startPromise_, _writableHighWaterMark_, _writableSizeAlgorithm_, - _readableHighWaterMark_, _readableSizeAlgorithm_). - 1. Perform ? SetUpTransformStreamDefaultControllerFromTransformer(*this*, _transformer_). - 1. Let _startResult_ be ? InvokeOrNoop(_transformer_, `"start"`, « *this*.[[transformStreamController]] »). - 1. Resolve _startPromise_ with _startResult_. - - -

Transformer API

+
+ The desiredSize attribute's getter steps are: -
+ 1. Let |readableController| be + [=this=].\[[controlledTransformStream]].\[[readable]].\[[readableStreamController]]. + 1. Return ! [$ReadableStreamDefaultControllerGetDesiredSize$](|readableController|). +
-This section is non-normative. +
+ The enqueue(|chunk|) method steps are: -The {{TransformStream()}} constructor accepts as its first argument a JavaScript object representing the -transformer. Such objects can contain any of the following methods: + 1. Perform ? [$TransformStreamDefaultControllerEnqueue$]([=this=], |chunk|). +
-
-
start(controller)
-
-

A function that is called immediately during creation of the {{TransformStream}}.

+
+ The error(|e|) method steps are: -

Typically this is used to enqueue prefix chunks, using - {{TransformStreamDefaultController/enqueue()|controller.enqueue()}}. Those chunks will be read from the readable - side but don't depend on any writes to the writable side.

+ 1. Perform ? [$TransformStreamDefaultControllerError$]([=this=], |e|). +
-

If this initial process is asynchronous, for example because it takes some effort to acquire the prefix - chunks, the function can return a promise to signal success or failure; a rejected promise will error the stream. - Any thrown exceptions will be re-thrown by the {{TransformStream()}} constructor.

-
+
+ The terminate() method steps are: -
transform(chunk, controller)
-
-

A function called when a new chunk originally written to the writable side is ready to be - transformed. The stream implementation guarantees that this function will be called only after previous transforms - have succeeded, and never before {{underlying sink/start()}} has completed or after {{transformer/flush()}} has been - called.

- -

This function performs the actual transformation work of the transform stream. It can enqueue the results using - {{TransformStreamDefaultController/enqueue()|controller.enqueue()}}. This permits a single chunk written to the - writable side to result in zero or multiple chunks on the readable side, depending on how many times - {{TransformStreamDefaultController/enqueue()|controller.enqueue()}} is called. [[#example-ts-lipfuzz]] demonstrates - this by sometimes enqueuing zero chunks.

- -

If the process of transforming is asynchronous, this function can return a promise to signal success or failure - of the transformation. A rejected promise will error both the readable and writable sides of the transform - stream.

- -

If no {{transformer/transform()}} is supplied, the identity transform is used, which enqueues chunks unchanged - from the writable side to the readable side.

-
- -
flush(controller)
-
-

A function called after all chunks written to the writable side have been transformed by - successfully passing through {{transformer/transform()}}, and the writable side is about to be closed.

+ 1. Perform ? [$TransformStreamDefaultControllerTerminate$]([=this=]). +
-

Typically this is used to enqueue suffix chunks to the readable side, before that too becomes closed. An - example can be seen in [[#example-ts-lipfuzz]].

+

Abstract operations

+ +

Working with transform streams

+ +The following abstract operations operate on {{TransformStream}} instances at a higher level. Some +are even meant to be generally useful by other specifications. + +
+ CreateTransformStream(|startAlgorithm|, |transformAlgorithm|, |flushAlgorithm|[, + |writableHighWaterMark|[, |writableSizeAlgorithm|[, |readableHighWaterMark|[, + |readableSizeAlgorithm|]]]]) is meant to be called from other specifications that wish to + create {{TransformStream}} instances. The |transformAlgorithm| and |flushAlgorithm| algorithms + must return promises; if supplied, |writableHighWaterMark| and |readableHighWaterMark| must be + non-negative, non-NaN numbers; and if supplied, |writableSizeAlgorithm| and + |readableSizeAlgorithm| must be algorithms accepting [=chunk=] objects and returning numbers. + + It performs the following steps: + + 1. If |writableHighWaterMark| was not passed, set it to 1. + 1. If |writableSizeAlgorithm| was not passed, set it to an algorithm that returns 1. + 1. If |readableHighWaterMark| was not passed, set it to 0. + 1. If |readableSizeAlgorithm| was not passed, set it to an algorithm that returns 1. + 1. Assert: ! [$IsNonNegativeNumber$](|writableHighWaterMark|) is true. + 1. Assert: ! [$IsNonNegativeNumber$](|readableHighWaterMark|) is true. + 1. Let |stream| be a [=new=] {{TransformStream}}. + 1. Let |startPromise| be [=a new promise=]. + 1. Perform ! [$InitializeTransformStream$](|stream|, |startPromise|, |writableHighWaterMark|, + |writableSizeAlgorithm|, |readableHighWaterMark|, |readableSizeAlgorithm|). + 1. Let |controller| be a [=new=] {{TransformStreamDefaultController}}. + 1. Perform ! [$SetUpTransformStreamDefaultController$](|stream|, |controller|, + |transformAlgorithm|, |flushAlgorithm|). + 1. Let |startResult| be the result of performing |startAlgorithm|. (This may throw an exception.) + 1. [=Resolve=] |startPromise| with |startResult|. + 1. Return |stream|. + +

This abstract operation will throw an exception if and only if the supplied + |startAlgorithm| throws. +

-

If the flushing process is asynchronous, the function can return a promise to signal success or failure; the - result will be communicated to the caller of {{WritableStreamDefaultWriter/write()|stream.writable.write()}}. - Additionally, a rejected promise will error both the readable and writable sides of the stream. Throwing an - exception is treated the same as returning a rejected promise.

+
+ InitializeTransformStream(|stream|, |startPromise|, + |writableHighWaterMark|, |writableSizeAlgorithm|, |readableHighWaterMark|, + |readableSizeAlgorithm|) performs the following steps: + + 1. Let |startAlgorithm| be an algorithm that returns |startPromise|. + 1. Let |writeAlgorithm| be the following steps, taking a |chunk| argument: + 1. Return ! [$TransformStreamDefaultSinkWriteAlgorithm$](|stream|, |chunk|). + 1. Let |abortAlgorithm| be the following steps, taking a |reason| argument: + 1. Return ! [$TransformStreamDefaultSinkAbortAlgorithm$](|stream|, |reason|). + 1. Let |closeAlgorithm| be the following steps: + 1. Return ! [$TransformStreamDefaultSinkCloseAlgorithm$](|stream|). + 1. Set |stream|.\[[writable]] to ! [$CreateWritableStream$](|startAlgorithm|, |writeAlgorithm|, + |closeAlgorithm|, |abortAlgorithm|, |writableHighWaterMark|, |writableSizeAlgorithm|). + 1. Let |pullAlgorithm| be the following steps: + 1. Return ! [$TransformStreamDefaultSourcePullAlgorithm$](|stream|). + 1. Let |cancelAlgorithm| be the following steps, taking a |reason| argument: + 1. Perform ! [$TransformStreamErrorWritableAndUnblockWrite$](|stream|, |reason|). + 1. Return [=a promise resolved with=] undefined. + 1. Set |stream|.\[[readable]] to ! [$CreateReadableStream$](|startAlgorithm|, |pullAlgorithm|, + |cancelAlgorithm|, |readableHighWaterMark|, |readableSizeAlgorithm|). + 1. Set |stream|.\[[backpressure]] and |stream|.\[[backpressureChangePromise]] to undefined. +

The \[[backpressure]] slot is set to undefined so that it can be initialized by + [$TransformStreamSetBackpressure$]. Alternatively, implementations can use a strictly boolean + value for \[[backpressure]] and change the way it is initialized. This will not be visible to + user code so long as the initialization is correctly completed before the transformer's + {{Transformer/start|start()}} method is called. + 1. Perform ! [$TransformStreamSetBackpressure$](|stream|, true). + 1. Set |stream|.\[[transformStreamController]] to undefined. +

-

(Note that there is no need to call {{TransformStreamDefaultController/terminate()|controller.terminate()}} - inside {{transformer/flush()}}; the stream is already in the process of successfully closing down, and terminating - it would be counterproductive.)

- -
+
+ TransformStreamError(|stream|, |e|) performs the following steps: -The controller object passed to {{transformer/start()}}, {{transformer/transform()}}, and -{{transformer/flush()}} is an instance of {{TransformStreamDefaultController}}, and has the ability to enqueue -chunks to the readable side, or to terminate or error the stream. + 1. Perform ! + [$ReadableStreamDefaultControllerError$](|stream|.\[[readable]].\[[readableStreamController]], + |e|). + 1. Perform ! [$TransformStreamErrorWritableAndUnblockWrite$](|stream|, |e|). +

This operation works correctly when one or both sides are already errored. As a + result, calling algorithms do not need to check stream states when responding to an error + condition.

-

Properties of the {{TransformStream}} prototype

+
+ TransformStreamErrorWritableAndUnblockWrite(|stream|, + |e|) performs the following steps: + + 1. Perform ! + [$TransformStreamDefaultControllerClearAlgorithms$](|stream|.\[[transformStreamController]]). + 1. Perform ! + [$WritableStreamDefaultControllerErrorIfNeeded$](|stream|.\[[writable]].\[[writableStreamController]], + |e|). + 1. If |stream|.\[[backpressure]] is true, perform ! [$TransformStreamSetBackpressure$](|stream|, + false). + +

The [$TransformStreamDefaultSinkWriteAlgorithm$] abstract operation could be + waiting for the promise stored in the \[[backpressureChangePromise]] slot to resolve. The call to + [$TransformStreamSetBackpressure$] ensures that the promise always resolves. +

-
get readable
+
+ TransformStreamSetBackpressure(|stream|, + |backpressure|) performs the following steps: -
- The readable getter gives access to the readable side of the transform stream. + 1. Assert: |stream|.\[[backpressure]] is not |backpressure|. + 1. If |stream|.\[[backpressureChangePromise]] is not undefined, [=resolve=] + stream.\[[backpressureChangePromise]] with undefined. + 1. Set |stream|.\[[backpressureChangePromise]] to [=a new promise=]. + 1. Set |stream|.\[[backpressure]] to |backpressure|.
- - 1. If ! IsTransformStream(*this*) is *false*, throw a *TypeError* exception. - 1. Return *this*.[[readable]]. - +

Default controllers

-
get writable
+The following abstract operations support the implementaiton of the +{{TransformStreamDefaultController}} class. -
- The writable getter gives access to the writable side of the transform stream. -
- - - 1. If ! IsTransformStream(*this*) is *false*, throw a *TypeError* exception. - 1. Return *this*.[[writable]]. - - -

General transform stream abstract operations

- -

CreateTransformStream ( -startAlgorithm, transformAlgorithm, flushAlgorithm -[, writableHighWaterMark [, writableSizeAlgorithm [, readableHighWaterMark [, -readableSizeAlgorithm ] ] ] ] )

- -This abstract operation is meant to be called from other specifications that wish to create {{TransformStream}} -instances. The transformAlgorithm and flushAlgorithm algorithms must return promises; if supplied, -writableHighWaterMark and readableHighWaterMark must be non-negative, non-NaN numbers; and if -supplied, writableSizeAlgorithm and readableSizeAlgorithm must be algorithms accepting -chunk objects and returning numbers. - -

CreateTransformStream throws an exception if and only if the supplied startAlgorithm -throws.

- - - 1. If _writableHighWaterMark_ was not passed, set it to *1*. - 1. If _writableSizeAlgorithm_ was not passed, set it to an algorithm that returns *1*. - 1. If _readableHighWaterMark_ was not passed, set it to *0*. - 1. If _readableSizeAlgorithm_ was not passed, set it to an algorithm that returns *1*. - 1. Assert: ! IsNonNegativeNumber(_writableHighWaterMark_) is *true*. - 1. Assert: ! IsNonNegativeNumber(_readableHighWaterMark_) is *true*. - 1. Let _stream_ be ObjectCreate(the original value of `TransformStream`'s `prototype` property). - 1. Let _startPromise_ be a new promise. - 1. Perform ! InitializeTransformStream(_stream_, _startPromise_, _writableHighWaterMark_, _writableSizeAlgorithm_, - _readableHighWaterMark_, _readableSizeAlgorithm_). - 1. Let _controller_ be ObjectCreate(the original value of `TransformStreamDefaultController`'s `prototype` - property). - 1. Perform ! SetUpTransformStreamDefaultController(_stream_, _controller_, _transformAlgorithm_, _flushAlgorithm_). - 1. Let _startResult_ be the result of performing _startAlgorithm_. (This may throw an exception.) - 1. Resolve _startPromise_ with _startResult_. - 1. Return _stream_. - - -

InitializeTransformStream ( -stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, -readableHighWaterMark, readableSizeAlgorithm )

- - - 1. Let _startAlgorithm_ be an algorithm that returns _startPromise_. - 1. Let _writeAlgorithm_ be the following steps, taking a _chunk_ argument: - 1. Return ! TransformStreamDefaultSinkWriteAlgorithm(_stream_, _chunk_). - 1. Let _abortAlgorithm_ be the following steps, taking a _reason_ argument: - 1. Return ! TransformStreamDefaultSinkAbortAlgorithm(_stream_, _reason_). - 1. Let _closeAlgorithm_ be the following steps: - 1. Return ! TransformStreamDefaultSinkCloseAlgorithm(_stream_). - 1. Set _stream_.[[writable]] to ! CreateWritableStream(_startAlgorithm_, _writeAlgorithm_, _closeAlgorithm_, - _abortAlgorithm_, _writableHighWaterMark_, _writableSizeAlgorithm_). - 1. Let _pullAlgorithm_ be the following steps: - 1. Return ! TransformStreamDefaultSourcePullAlgorithm(_stream_). - 1. Let _cancelAlgorithm_ be the following steps, taking a _reason_ argument: - 1. Perform ! TransformStreamErrorWritableAndUnblockWrite(_stream_, _reason_). - 1. Return a promise resolved with *undefined*. - 1. Set _stream_.[[readable]] to ! CreateReadableStream(_startAlgorithm_, _pullAlgorithm_, _cancelAlgorithm_, - _readableHighWaterMark_, _readableSizeAlgorithm_). - 1. Set _stream_.[[backpressure]] and _stream_.[[backpressureChangePromise]] to *undefined*. -

The [[backpressure]] slot is set to *undefined* so that it can be initialized by - TransformStreamSetBackpressure. Alternatively, implementations can use a strictly boolean value for - [[backpressure]] and change the way it is initialized. This will not be visible to user code so long as the - initialization is correctly completed before _transformer_'s {{transformer/start()}} method is called.

- 1. Perform ! TransformStreamSetBackpressure(_stream_, *true*). - 1. Set _stream_.[[transformStreamController]] to *undefined*. -
- -

IsTransformStream ( x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have a [[transformStreamController]] internal slot, return *false*. - 1. Return *true*. - - -

TransformStreamError ( stream, -e )

- - - 1. Perform ! ReadableStreamDefaultControllerError(_stream_.[[readable]].[[readableStreamController]], _e_). - 1. Perform ! TransformStreamErrorWritableAndUnblockWrite(_stream_, _e_). - - -

This operation works correctly when one or both sides are already errored. As a result, calling -algorithms do not need to check stream states when responding to an error condition.

- -

TransformStreamErrorWritableAndUnblockWrite ( stream, e )

- - - 1. Perform ! TransformStreamDefaultControllerClearAlgorithms(_stream_.[[transformStreamController]]). - 1. Perform ! WritableStreamDefaultControllerErrorIfNeeded(_stream_.[[writable]].[[writableStreamController]], _e_). - 1. If _stream_.[[backpressure]] is *true*, perform ! TransformStreamSetBackpressure(_stream_, *false*). - - -

The TransformStreamDefaultSinkWriteAlgorithm abstract operation could be waiting for the promise -stored in the \[[backpressureChangePromise]] slot to resolve. This call to TransformStreamSetBackpressure ensures that -the promise always resolves.

- -

TransformStreamSetBackpressure -( stream, backpressure )

- - - 1. Assert: _stream_.[[backpressure]] is not _backpressure_. - 1. If _stream_.[[backpressureChangePromise]] is not *undefined*, resolve - stream.[[backpressureChangePromise]] with *undefined*. - 1. Set _stream_.[[backpressureChangePromise]] to a new promise. - 1. Set _stream_.[[backpressure]] to _backpressure_. - - -

Class -TransformStreamDefaultController

- -The {{TransformStreamDefaultController}} class has methods that allow manipulation of the associated {{ReadableStream}} -and {{WritableStream}}. When constructing a {{TransformStream}}, the transformer object is given a corresponding -{{TransformStreamDefaultController}} instance to manipulate. - -

Class definition

- -
+
+ SetUpTransformStreamDefaultController(|stream|, + |controller|, |transformAlgorithm|, |flushAlgorithm|) performs the following steps: -This section is non-normative. + 1. Assert: |stream| [=implements=] {{TransformStream}}. + 1. Assert: |stream|.\[[transformStreamController]] is undefined. + 1. Set |controller|.\[[controlledTransformStream]] to |stream|. + 1. Set |stream|.\[[transformStreamController]] to |controller|. + 1. Set |controller|.\[[transformAlgorithm]] to |transformAlgorithm|. + 1. Set |controller|.\[[flushAlgorithm]] to |flushAlgorithm|. +
-If one were to write the {{TransformStreamDefaultController}} class in something close to the syntax of [[!ECMASCRIPT]], -it would look like +
+ SetUpTransformStreamDefaultControllerFromTransformer(|stream|, + |transformer|, |transformerDict|) performs the following steps: + + 1. Let |controller| be a [=new=] {{TransformStreamDefaultController}}. + 1. Let |transformAlgorithm| be the following steps, taking a |chunk| argument: + 1. Let |result| be [$TransformStreamDefaultControllerEnqueue$](|controller|, |chunk|). + 1. If |result| is an abrupt completion, return [=a promise rejected with=] |result|.\[[Value]]. + 1. Otherwise, return [=a promise resolved with=] undefined. + 1. Let |flushAlgorithm| be an algorithm which returns [=a promise resolved with=] undefined. + 1. If |transformerDict|["{{Transformer/transform}}"] [=map/exists=], set |transformAlgorithm| to an + algorithm which takes an argument |chunk| and returns the result of [=invoking=] + |transformerDict|["{{Transformer/transform}}"] with argument list « |chunk|, + |controller| ») and [=callback this value=] |transformer|. + 1. If |transformerDict|["{{Transformer/flush}}"] [=map/exists=], set |flushAlgorithm| to an + algorithm which returns the result of [=invoking=] |transformerDict|["{{Transformer/flush}}"] + with argument list « |controller| » and [=callback this value=] |transformer|. + 1. Perform ! [$SetUpTransformStreamDefaultController$](|stream|, |controller|, + |transformAlgorithm|, |flushAlgorithm|). +
-

-  class TransformStreamDefaultController {
-    constructor() // always throws
+
+ TransformStreamDefaultControllerClearAlgorithms(|controller|) + is called once the stream is closed or errored and the algorithms will not be executed any more. + By removing the algorithm references it permits the [=transformer=] object to be garbage collected + even if the {{TransformStream}} itself is still referenced. - get desiredSize() +

This is observable using weak + references. See tc39/proposal-weakrefs#31 for more + detail. - enqueue(chunk) - error(reason) - terminate() - } -

+ It performs the following steps: + 1. Set |controller|.\[[transformAlgorithm]] to undefined. + 1. Set |controller|.\[[flushAlgorithm]] to undefined.
-

Internal slots

- -Instances of {{TransformStreamDefaultController}} are created with the internal slots described in the following table: - - - - - - - - - - - - - -
Internal SlotDescription (non-normative)
\[[controlledTransformStream]] - The {{TransformStream}} instance controlled; also used for the - IsTransformStreamDefaultController brand check -
\[[flushAlgorithm]] - A promise-returning algorithm which communicates a requested close to the - transformer -
\[[transformAlgorithm]] - A promise-returning algorithm, taking one argument (the chunk to transform), which - requests the transformer perform its transformation -
+
+ TransformStreamDefaultControllerEnqueue(|controller|, |chunk|) is meant to be called + by other specifications that wish to enqueue [=chunks=] in the [=readable side=], in the same way + a developer would enqueue chunks using the stream's associated controller object. Specifications + should not do this to streams or controllers they did not create. + + It performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledTransformStream]]. + 1. Let |readableController| be |stream|.\[[readable]].\[[readableStreamController]]. + 1. If ! [$ReadableStreamDefaultControllerCanCloseOrEnqueue$](|readableController|) is false, throw + a {{TypeError}} exception. + 1. Let |enqueueResult| be [$ReadableStreamDefaultControllerEnqueue$](|readableController|, + |chunk|). + 1. If |enqueueResult| is an abrupt completion, + 1. Perform ! [$TransformStreamErrorWritableAndUnblockWrite$](|stream|, + |enqueueResult|.\[[Value]]). + 1. Throw |stream|.\[[readable]].\[[storedError]]. + 1. Let |backpressure| be ! + [$ReadableStreamDefaultControllerHasBackpressure$](|readableController|). + 1. If |backpressure| is not |stream|.\[[backpressure]], + 1. Assert: |backpressure| is true. + 1. Perform ! [$TransformStreamSetBackpressure$](|stream|, true). +
+
+ TransformStreamDefaultControllerError(|controller|, |e|) is meant to be called by + other specifications that wish to move the transform stream to an errored state, in the same way a + developer would error the stream using the stream's associated controller object. Specifications + should not do this to streams or controllers they did not create. -

new TransformStreamDefaultController()

+ It performs the following steps: -
- The TransformStreamDefaultController constructor cannot be used directly; - {{TransformStreamDefaultController}} instances are created automatically during {{TransformStream}} construction. + 1. Perform ! [$TransformStreamError$](|controller|.\[[controlledTransformStream]], |e|).
- - 1. Throw a *TypeError* exception. - - -

Properties of the {{TransformStreamDefaultController}} prototype

+
+ TransformStreamDefaultControllerPerformTransform(|controller|, |chunk|) + performs the following steps: + + 1. Let |transformPromise| be the result of performing + |controller|.\[[transformAlgorithm]], passing |chunk|. + 1. Return the result of [=reacting=] to |transformPromise| with the following + rejection steps given the argument |r|: + 1. Perform ! [$TransformStreamError$](|controller|.\[[controlledTransformStream]], |r|). + 1. Throw |r|. +
-
get -desiredSize
+
+ TransformStreamDefaultControllerTerminate(|controller|) is meant to be called by + other specifications that wish to terminate the transform stream, in the same way a + developer-created stream would be terminated by its associated controller object. Specifications + should not do this to streams or controllers they did not create. + + It performs the following steps: + + 1. Let |stream| be |controller|.\[[controlledTransformStream]]. + 1. Let |readableController| be |stream|.\[[readable]].\[[readableStreamController]]. + 1. Perform ! [$ReadableStreamDefaultControllerClose$](|readableController|). + 1. Let |error| be a {{TypeError}} exception indicating that the stream has been terminated. + 1. Perform ! [$TransformStreamErrorWritableAndUnblockWrite$](|stream|, |error|). +
-
- The desiredSize getter returns the desired size - to fill the readable side's internal queue. It can be negative, if the queue is over-full. +

Default sinks

+ +The following abstract operations are used to implement the [=underlying sink=] for the [=writable +side=] of [=transform streams=]. + +
+ TransformStreamDefaultSinkWriteAlgorithm(|stream|, + |chunk|) performs the following steps: + + 1. Assert: |stream|.\[[writable]].\[[state]] is "`writable`". + 1. Let |controller| be |stream|.\[[transformStreamController]]. + 1. If |stream|.\[[backpressure]] is true, + 1. Let |backpressureChangePromise| be |stream|.\[[backpressureChangePromise]]. + 1. Assert: |backpressureChangePromise| is not undefined. + 1. Return the result of [=reacting=] to |backpressureChangePromise| with the following fulfillment + steps: + 1. Let |writable| be |stream|.\[[writable]]. + 1. Let |state| be |writable|.\[[state]]. + 1. If |state| is "`erroring`", throw |writable|.\[[storedError]]. + 1. Assert: |state| is "`writable`". + 1. Return ! [$TransformStreamDefaultControllerPerformTransform$](|controller|, |chunk|). + 1. Return ! [$TransformStreamDefaultControllerPerformTransform$](|controller|, |chunk|).
- - 1. If ! IsTransformStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. Let _readableController_ be *this*.[[controlledTransformStream]].[[readable]].[[readableStreamController]]. - 1. Return ! ReadableStreamDefaultControllerGetDesiredSize(_readableController_). - +
+ TransformStreamDefaultSinkAbortAlgorithm(|stream|, + |reason|) performs the following steps: -
enqueue(chunk)
+ 1. Perform ! [$TransformStreamError$](|stream|, |reason|). + 1. Return [=a promise resolved with=] undefined. +
-
- The enqueue method will enqueue a given chunk in the readable side. +
+ TransformStreamDefaultSinkCloseAlgorithm(|stream|) + performs the following steps: + + 1. Let |readable| be |stream|.\[[readable]]. + 1. Let |controller| be |stream|.\[[transformStreamController]]. + 1. Let |flushPromise| be the result of performing |controller|.\[[flushAlgorithm]]. + 1. Perform ! [$TransformStreamDefaultControllerClearAlgorithms$](|controller|). + 1. Return the result of [=reacting=] to |flushPromise|: + 1. If |flushPromise| was fulfilled, then: + 1. If |readable|.\[[state]] is "`errored`", throw |readable|.\[[storedError]]. + 1. Let |readableController| be |readable|.\[[readableStreamController]]. + 1. If ! [$ReadableStreamDefaultControllerCanCloseOrEnqueue$](|readableController|) is true, + perform ! [$ReadableStreamDefaultControllerClose$](|readableController|). + 1. If |flushPromise| was rejected with reason |r|, then: + 1. Perform ! [$TransformStreamError$](|stream|, |r|). + 1. Throw |readable|.\[[storedError]].
- - 1. If ! IsTransformStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. Perform ? TransformStreamDefaultControllerEnqueue(*this*, _chunk_). - +

Default source

-
error(reason)
+The following abstract operation is used to implement the [=underlying source=] for the [=readable +side=] of [=transform streams=]. -
- The error method will error both the readable side and the writable side of the controlled - transform stream, making all future interactions fail with the given reason. Any chunks - queued for transformation will be discarded. -
+
+ TransformStreamDefaultSourcePullAlgorithm(|stream|) + performs the following steps: - - 1. If ! IsTransformStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. Perform ! TransformStreamDefaultControllerError(*this*, _reason_). - + 1. Assert: |stream|.\[[backpressure]] is true. + 1. Assert: |stream|.\[[backpressureChangePromise]] is not undefined. + 1. Perform ! [$TransformStreamSetBackpressure$](|stream|, false). + 1. Return |stream|.\[[backpressureChangePromise]]. +
-
terminate()
+

Queuing strategies

-
- The terminate method will close the readable side and error the writable side of the - controlled transform stream. This is useful when the transformer only needs to consume a portion of the - chunks written to the writable side. -
- - - 1. If ! IsTransformStreamDefaultController(*this*) is *false*, throw a *TypeError* exception. - 1. Perform ! TransformStreamDefaultControllerTerminate(*this*). - - -

Transform stream default controller abstract operations

- -

IsTransformStreamDefaultController ( x )

- - - 1. If Type(_x_) is not Object, return *false*. - 1. If _x_ does not have an [[controlledTransformStream]] internal slot, return *false*. - 1. Return *true*. - - -

SetUpTransformStreamDefaultController ( stream, controller, transformAlgorithm, -flushAlgorithm )

- - - 1. Assert: ! IsTransformStream(_stream_) is *true*. - 1. Assert: _stream_.[[transformStreamController]] is *undefined*. - 1. Set _controller_.[[controlledTransformStream]] to _stream_. - 1. Set _stream_.[[transformStreamController]] to _controller_. - 1. Set _controller_.[[transformAlgorithm]] to _transformAlgorithm_. - 1. Set _controller_.[[flushAlgorithm]] to _flushAlgorithm_. - - -

SetUpTransformStreamDefaultControllerFromTransformer ( stream, transformer )

- - - 1. Assert: _transformer_ is not *undefined*. - 1. Let _controller_ be ObjectCreate(the original value of `TransformStreamDefaultController`'s `prototype` - property). - 1. Let _transformAlgorithm_ be the following steps, taking a _chunk_ argument: - 1. Let _result_ be TransformStreamDefaultControllerEnqueue(_controller_, _chunk_). - 1. If _result_ is an abrupt completion, return a promise rejected with _result_.[[Value]]. - 1. Otherwise, return a promise resolved with *undefined*. - 1. Let _transformMethod_ be ? GetV(_transformer_, `"transform"`). - 1. If _transformMethod_ is not *undefined*, - 1. If ! IsCallable(_transformMethod_) is *false*, throw a *TypeError* exception. - 1. Set _transformAlgorithm_ to the following steps, taking a _chunk_ argument: - 1. Return ! PromiseCall(_transformMethod_, _transformer_, « _chunk_, _controller_ »). - 1. Let _flushAlgorithm_ be ? CreateAlgorithmFromUnderlyingMethod(_transformer_, `"flush"`, *0*, « controller »). - 1. Perform ! SetUpTransformStreamDefaultController(_stream_, _controller_, _transformAlgorithm_, _flushAlgorithm_). - - -

TransformStreamDefaultControllerClearAlgorithms ( controller )

- -This abstract operation is called once the stream is closed or errored and the algorithms will not be executed any more. -By removing the algorithm references it permits the transformer object to be garbage collected even if the -{{TransformStream}} itself is still referenced. - -

The results of this algorithm are not currently observable, but could become so if JavaScript eventually -adds weak references. But even without that factor, -implementations will likely want to include similar steps.

- - - 1. Set _controller_.[[transformAlgorithm]] to *undefined*. - 1. Set _controller_.[[flushAlgorithm]] to *undefined*. - - -

TransformStreamDefaultControllerEnqueue ( controller, chunk )

- -This abstract operation can be called by other specifications that wish to enqueue chunks in the readable -side, in the same way a developer would enqueue chunks using the stream's associated controller object. -Specifications should not do this to streams they did not create. - - - 1. Let _stream_ be _controller_.[[controlledTransformStream]]. - 1. Let _readableController_ be _stream_.[[readable]].[[readableStreamController]]. - 1. If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(_readableController_) is *false*, throw a *TypeError* - exception. - 1. Let _enqueueResult_ be ReadableStreamDefaultControllerEnqueue(_readableController_, _chunk_). - 1. If _enqueueResult_ is an abrupt completion, - 1. Perform ! TransformStreamErrorWritableAndUnblockWrite(_stream_, _enqueueResult_.[[Value]]). - 1. Throw _stream_.[[readable]].[[storedError]]. - 1. Let _backpressure_ be ! ReadableStreamDefaultControllerHasBackpressure(_readableController_). - 1. If _backpressure_ is not _stream_.[[backpressure]], - 1. Assert: _backpressure_ is *true*. - 1. Perform ! TransformStreamSetBackpressure(_stream_, *true*). - - -

TransformStreamDefaultControllerError ( controller, e )

- -This abstract operation can be called by other specifications that wish to move a transform stream to an errored state, -in the same way a developer would error a stream using its associated controller object. Specifications should -not do this to streams they did not create. - - - 1. Perform ! TransformStreamError(_controller_.[[controlledTransformStream]], _e_). - - -

TransformStreamDefaultControllerPerformTransform ( controller, chunk )

- - - 1. Let _transformPromise_ be the result of performing _controller_.[[transformAlgorithm]], passing _chunk_. - 1. Return the result of reacting to _transformPromise_ with the following rejection steps given the - argument _r_: - 1. Perform ! TransformStreamError(_controller_.[[controlledTransformStream]], _r_). - 1. Throw _r_. - - -

TransformStreamDefaultControllerTerminate ( controller )

- -This abstract operation can be called by other specifications that wish to terminate a transform stream, in the same way -a developer-created stream would be closed by its associated controller object. Specifications should not do -this to streams they did not create. - - - 1. Let _stream_ be _controller_.[[controlledTransformStream]]. - 1. Let _readableController_ be _stream_.[[readable]].[[readableStreamController]]. - 1. Perform ! ReadableStreamDefaultControllerClose(_readableController_). - 1. Let _error_ be a *TypeError* exception indicating that the stream has been terminated. - 1. Perform ! TransformStreamErrorWritableAndUnblockWrite(_stream_, _error_). - - -

Transform stream default sink abstract operations

- -

TransformStreamDefaultSinkWriteAlgorithm ( stream, chunk )

- - - 1. Assert: _stream_.[[writable]].[[state]] is `"writable"`. - 1. Let _controller_ be _stream_.[[transformStreamController]]. - 1. If _stream_.[[backpressure]] is *true*, - 1. Let _backpressureChangePromise_ be _stream_.[[backpressureChangePromise]]. - 1. Assert: _backpressureChangePromise_ is not *undefined*. - 1. Return the result of reacting to _backpressureChangePromise_ with the following fulfillment steps: - 1. Let _writable_ be _stream_.[[writable]]. - 1. Let _state_ be _writable_.[[state]]. - 1. If _state_ is `"erroring"`, throw _writable_.[[storedError]]. - 1. Assert: _state_ is `"writable"`. - 1. Return ! TransformStreamDefaultControllerPerformTransform(_controller_, _chunk_). - 1. Return ! TransformStreamDefaultControllerPerformTransform(_controller_, _chunk_). - - -

TransformStreamDefaultSinkAbortAlgorithm ( stream, reason )

- - - 1. Perform ! TransformStreamError(_stream_, _reason_). - 1. Return a promise resolved with *undefined*. - - -

TransformStreamDefaultSinkCloseAlgorithm( stream )

- - - 1. Let _readable_ be _stream_.[[readable]]. - 1. Let _controller_ be _stream_.[[transformStreamController]]. - 1. Let _flushPromise_ be the result of performing _controller_.[[flushAlgorithm]]. - 1. Perform ! TransformStreamDefaultControllerClearAlgorithms(_controller_). - 1. Return the result of reacting to _flushPromise_: - 1. If _flushPromise_ was fulfilled, then: - 1. If _readable_.[[state]] is `"errored"`, throw _readable_.[[storedError]]. - 1. Let _readableController_ be _readable_.[[readableStreamController]]. - 1. If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(_readableController_) is *true*, perform ! - ReadableStreamDefaultControllerClose(_readableController_). - 1. If _flushPromise_ was rejected with reason _r_, then: - 1. Perform ! TransformStreamError(_stream_, _r_). - 1. Throw _readable_.[[storedError]]. - - -

Transform stream default source abstract operations

- -

TransformStreamDefaultSourcePullAlgorithm( stream -)

- - - 1. Assert: _stream_.[[backpressure]] is *true*. - 1. Assert: _stream_.[[backpressureChangePromise]] is not *undefined*. - 1. Perform ! TransformStreamSetBackpressure(_stream_, *false*). - 1. Return _stream_.[[backpressureChangePromise]]. - - -

Other stream APIs and operations

- -

Queuing strategies

- -

The queuing strategy API

+

The queuing strategy API

-
+The {{ReadableStream()}}, {{WritableStream()}}, and {{TransformStream()}} constructors all accept +at least one argument representing an appropriate [=queuing strategy=] for the stream being +created. Such objects contain the following properties: -This section is non-normative. + +dictionary QueuingStrategy { + unrestricted double highWaterMark; + QueuingStrategySize size; +}; -The {{ReadableStream()}}, {{WritableStream()}}, and {{TransformStream()}} constructors all accept at least one argument -representing an appropriate <a>queuing strategy</a> for the stream being created. Such objects contain the following -properties: +callback QueuingStrategySize = unrestricted double (optional any chunk); +
-
size(chunk) (non-byte streams only)
+
size(chunk) (non-byte streams only)
+
+

A function that computes and returns the finite non-negative size of the given [=chunk=] + value. + +

The result is used to determine [=backpressure=], manifesting via the appropriate + desiredSize + property: either {{ReadableStreamDefaultController/desiredSize|defaultController.desiredSize}}, + {{ReadableByteStreamController/desiredSize|byteController.desiredSize}}, or + {{WritableStreamDefaultWriter/desiredSize|writer.desiredSize}}, depending on where the queuing + strategy is being used. For readable streams, it also governs when the [=underlying source=]'s + {{UnderlyingSource/pull|pull()}} method is called. + +

This function has to be idempotent and not cause side effects; very strange results can occur + otherwise. + +

For [=readable byte streams=], this function is not used, as chunks are always measured in + bytes. + +

highWaterMark
-

A function that computes and returns the finite non-negative size of the given chunk value.

- -

The result is used to determine backpressure, manifesting via the appropriate desiredSize - property: either - {{ReadableStreamDefaultController/desiredSize|defaultController.desiredSize}}, - {{ReadableByteStreamController/desiredSize|byteController.desiredSize}}, or - {{WritableStreamDefaultWriter/desiredSize|writer.desiredSize}}, depending on where the queuing strategy is being - used. For readable streams, it also governs when the underlying source's {{underlying source/pull()}} method - is called.

+

A non-negative number indicating the [=high water mark=] of the stream using this queuing + strategy. +

-

This function has to be idempotent and not cause side effects; very strange results can occur otherwise.

+Any object with these properties can be used when a queuing strategy object is expected. However, +we provide two built-in queuing strategy classes that provide a common vocabulary for certain +cases: {{ByteLengthQueuingStrategy}} and {{CountQueuingStrategy}}. They both make use of the +following Web IDL fragment for their constructors: -

For readable byte streams, this function is not used, as chunks are always measured in bytes.

- + +dictionary QueuingStrategyInit { + required unrestricted double highWaterMark; +}; + -
highWaterMark
-
-

A non-negative number indicating the high water mark of the stream using this queuing strategy.

-
- +

The {{ByteLengthQueuingStrategy}} class

-Any object with these properties can be used when a queuing strategy object is expected. However, we provide two -built-in queuing strategy classes that provide a common vocabulary for certain cases: {{ByteLengthQueuingStrategy}} and -{{CountQueuingStrategy}}. +A common [=queuing strategy=] when dealing with bytes is to wait until the accumulated +byteLength properties of the incoming [=chunks=] reaches a specified high-water mark. +As such, this is provided as a built-in [=queuing strategy=] that can be used when constructing +streams. +
+ When creating a [=readable stream=] or [=writable stream=], you can supply a byte-length queuing + strategy directly: + + + const stream = new ReadableStream( + { ... }, + new ByteLengthQueuingStrategy({ highWaterMark: 16 * 1024 }) + ); + + + In this case, 16 KiB worth of [=chunks=] can be enqueued by the readable stream's [=underlying + source=] before the readable stream implementation starts sending [=backpressure=] signals to the + underlying source. + + + const stream = new WritableStream( + { ... }, + new ByteLengthQueuingStrategy({ highWaterMark: 32 * 1024 }) + ); + + + In this case, 32 KiB worth of [=chunks=] can be accumulated in the writable stream's internal + queue, waiting for previous writes to the [=underlying sink=] to finish, before the writable + stream starts sending [=backpressure=] signals to any [=producers=].
-

Class ByteLengthQueuingStrategy

+

It is not necessary to use {{ByteLengthQueuingStrategy}} with [=readable byte +streams=], as they always measure chunks in bytes. Attempting to construct a byte stream with a +{{ByteLengthQueuingStrategy}} will fail. -A common queuing strategy when dealing with bytes is to wait until the accumulated byteLength -properties of the incoming chunks reaches a specified high-water mark. As such, this is provided as a built-in -queuing strategy that can be used when constructing streams. +

Interface definition

-
- When creating a readable stream or writable stream, you can supply a byte-length queuing strategy - directly: +The Web IDL definition for the {{ByteLengthQueuingStrategy}} class is given as follows: + + +[Exposed=(Window,Worker,Worklet)] +interface ByteLengthQueuingStrategy { + constructor(QueuingStrategyInit init); - <pre><code class="lang-javascript"> - const stream = new ReadableStream( - { ... }, - new ByteLengthQueuingStrategy({ highWaterMark: 16 * 1024 }) - ); - </code></pre> + attribute unrestricted double highWaterMark; + readonly attribute Function size; +}; + - In this case, 16 KiB worth of chunks can be enqueued by the readable stream's underlying source before - the readable stream implementation starts sending backpressure signals to the underlying source. +

Internal slots

-

-    const stream = new WritableStream(
-      { ... },
-      new ByteLengthQueuingStrategy({ highWaterMark: 32 * 1024 })
-    );
-  
+Instances of {{ByteLengthQueuingStrategy}} have a \[[highWaterMark]] internal slot, storing the +value given in the constructor. - In this case, 32 KiB worth of chunks can be accumulated in the writable stream's internal queue, waiting for - previous writes to the underlying sink to finish, before the writable stream starts sending - backpressure signals to any producers. +
+ Additionally, every [=/global object=] |globalObject| has an associated byte length queuing + strategy size function, which is a {{Function}} whose value must be initialized as follows: + + 1. Let |steps| be the following steps, given |chunk|: + 1. Return ? [$GetV$](|chunk|, "`byteLength`"). + 1. Let |F| be ! [$CreateBuiltinFunction$](|steps|, « », |globalObject|'s [=relevant Realm=]). + 1. Perform ! [$SetFunctionName$](|F|, "`size`"). + 1. Perform ! [$SetFunctionLength$](|F|, 1). + 1. Set |globalObject|'s [=byte length queuing strategy size function=] to a {{Function}} that + represents a reference to |F|. (The [=callback context=] does not matter, since this function is + never [=invoked=].) + +

This design is somewhat historical. It is motivated by the desire to ensure that + {{ByteLengthQueuingStrategy/size}} is a function, not a method, i.e. it does not check its + this value. See whatwg/streams#1005 and heycam/webidl#819 for more background.

-

- It is not necessary to use {{ByteLengthQueuingStrategy}} with readable byte streams, as they always measure - chunks in bytes. Attempting to construct a byte stream with a {{ByteLengthQueuingStrategy}} will fail. -

+

Constructor and properties

-
Class definition
+
+
strategy = new {{ByteLengthQueuingStrategy/constructor(init)|ByteLengthQueuingStrategy}}({ {{QueuingStrategyInit/highWaterMark}} }) +
+

Creates a new {{ByteLengthQueuingStrategy}} with the provided [=high water mark=]. -

+

Note that the provided high water mark will not be validated ahead of time. Instead, if it is + negative, NaN, or not a number, the resulting {{ByteLengthQueuingStrategy}} will cause the + corresponding stream constructor to throw. -This section is non-normative. +

highWaterMark = strategy.{{ByteLengthQueuingStrategy/highWaterMark}} +
strategy.{{ByteLengthQueuingStrategy/highWaterMark}} = highWaterMark +
+

Returns the [=high water mark=] provided to the constructor, or sets it to a new value. -If one were to write the {{ByteLengthQueuingStrategy}} class in something close to the syntax of [[!ECMASCRIPT]], it -would look like +

Note that high water marks are only read from [=queuing strategies=] upon stream construction, + i.e. setting this property will not change the behavior of any already-constructed streams. -


-  class ByteLengthQueuingStrategy {
-    constructor({ highWaterMark })
-    size(chunk)
-  }
-
+
strategy.{{ByteLengthQueuingStrategy/size}}(chunk) +
+

Measures the size of chunk by returning the value of its + byteLength property. +

-Each {{ByteLengthQueuingStrategy}} instance will additionally have an own data property -highWaterMark set by its constructor. +
+ The ByteLengthQueuingStrategy(|init|) constructor steps are: + 1. Set [=this=].\[[highWaterMark]] to |init|["{{QueuingStrategyInit/highWaterMark}}"].
-
new -ByteLengthQueuingStrategy({ highWaterMark })
+
+ The highWaterMark + attribute's getter steps are: -
- The constructor takes a non-negative number for the high-water mark, and stores it on the object as a property. + 1. Return [=this=].\[[highWaterMark]]. + + Its setter steps are: + + 1. Set [=this=].\[[highWaterMark]] to [=the given value=].
- - 1. Perform ! CreateDataProperty(*this*, `"highWaterMark"`, _highWaterMark_). - +
+ The size attribute's getter + steps are: -
Properties of the {{ByteLengthQueuingStrategy}} prototype
+ 1. Return [=this=]'s [=relevant global object=]'s [=byte length queuing strategy size function=]. +
-
size(chunk)
+

The {{CountQueuingStrategy}} class

-
- The size method returns the given chunk's byteLength property. (If the chunk doesn't have - one, it will return undefined, causing the stream using this strategy to error.) +A common [=queuing strategy=] when dealing with streams of generic objects is to simply count the +number of chunks that have been accumulated so far, waiting until this number reaches a specified +high-water mark. As such, this strategy is also provided out of the box. - This method is intentionally generic; it does not require that its this value be a - ByteLengthQueuingStrategy object. +
+ When creating a [=readable stream=] or [=writable stream=], you can supply a count queuing + strategy directly: + + + const stream = new ReadableStream( + { ... }, + new CountQueuingStrategy({ highWaterMark: 10 }) + ); + + + In this case, 10 [=chunks=] (of any kind) can be enqueued by the readable stream's [=underlying + source=] before the readable stream implementation starts sending [=backpressure=] signals to the + underlying source. + + + const stream = new WritableStream( + { ... }, + new CountQueuingStrategy({ highWaterMark: 5 }) + ); + + + In this case, five [=chunks=] (of any kind) can be accumulated in the writable stream's internal + queue, waiting for previous writes to the [=underlying sink=] to finish, before the writable + stream starts sending [=backpressure=] signals to any [=producers=].
- - 1. Return ? GetV(_chunk_, `"byteLength"`). - +

Interface definition

-

Class CountQueuingStrategy

+The Web IDL definition for the {{CountQueuingStrategy}} class is given as follows: -A common queuing strategy when dealing with streams of generic objects is to simply count the number of chunks -that have been accumulated so far, waiting until this number reaches a specified high-water mark. As such, this -strategy is also provided out of the box. + +[Exposed=(Window,Worker,Worklet)] +interface CountQueuingStrategy { + constructor(QueuingStrategyInit init); -<div class="example" id="example-cqs"> - When creating a <a>readable stream</a> or <a>writable stream</a>, you can supply a count queuing strategy directly: + attribute unrestricted double highWaterMark; + readonly attribute Function size; +}; + -

-    const stream = new ReadableStream(
-      { ... },
-      new CountQueuingStrategy({ highWaterMark: 10 })
-    );
-  
+

Internal slots

- In this case, 10 chunks (of any kind) can be enqueued by the readable stream's underlying source before - the readable stream implementation starts sending backpressure signals to the underlying source. +Instances of {{CountQueuingStrategy}} have a \[[highWaterMark]] internal slot, storing the +value given in the constructor. -

-    const stream = new WritableStream(
-      { ... },
-      new CountQueuingStrategy({ highWaterMark: 5 })
-    );
-  
+
+ Additionally, every [=/global object=] |globalObject| has an associated count queuing strategy + size function, which is a {{Function}} whose value must be initialized as follows: - In this case, five chunks (of any kind) can be accumulated in the writable stream's internal queue, waiting - for previous writes to the underlying sink to finish, before the writable stream starts sending - backpressure signals to any producers. + 1. Let |steps| be the following steps: + 1. Return 1. + 1. Let |F| be ! [$CreateBuiltinFunction$](|steps|, « », |globalObject|'s [=relevant Realm=]). + 1. Perform ! [$SetFunctionName$](|F|, "`size`"). + 1. Perform ! [$SetFunctionLength$](|F|, 0). + 1. Set |globalObject|'s [=count queuing strategy size function=] to a {{Function}} that represents + a reference to |F|. (The [=callback context=] does not matter, since this function is never + [=invoked=].) + +

This design is somewhat historical. It is motivated by the desire to ensure that + {{CountQueuingStrategy/size}} is a function, not a method, i.e. it does not check its + this value. See whatwg/streams#1005 and heycam/webidl#819 for more background.

-
Class definition
+

Constructor and properties

-
+
+
strategy = new {{CountQueuingStrategy/constructor(init)|CountQueuingStrategy}}({ {{QueuingStrategyInit/highWaterMark}} }) +
+

Creates a new {{CountQueuingStrategy}} with the provided [=high water mark=]. -This section is non-normative. +

Note that the provided high water mark will not be validated ahead of time. Instead, if it is + negative, NaN, or not a number, the resulting {{CountQueuingStrategy}} will cause the + corresponding stream constructor to throw. -If one were to write the {{CountQueuingStrategy}} class in something close to the syntax of [[!ECMASCRIPT]], it would -look like +

highWaterMark = strategy.{{CountQueuingStrategy/highWaterMark}} +
strategy.{{CountQueuingStrategy/highWaterMark}} = highWaterMark +
+

Returns the [=high water mark=] provided to the constructor, or sets it to a new value. -


-  class CountQueuingStrategy {
-    constructor({ highWaterMark })
-    size(chunk)
-  }
-
+

Note that high water marks are only read from [=queuing strategies=] upon stream construction, + i.e. setting this property will not change the behavior of any already-constructed streams. + +

strategy.{{CountQueuingStrategy/size}}(chunk) +
+

Measures the size of chunk by always returning 1. This ensures that the total + queue size is a count of the number of chunks in the queue. +

-Each {{CountQueuingStrategy}} instance will additionally have an own data property highWaterMark -set by its constructor. +
+ The CountQueuingStrategy(|init|) constructor steps are: + 1. Set [=this=].\[[highWaterMark]] to |init|["{{QueuingStrategyInit/highWaterMark}}"].
-
new -CountQueuingStrategy({ highWaterMark })
+
+ The highWaterMark + attribute's getter steps are: -
- The constructor takes a non-negative number for the high-water mark, and stores it on the object as a property. + 1. Return [=this=].\[[highWaterMark]]. + + Its setter steps are: + + 1. Set [=this=].\[[highWaterMark]] to [=the given value=].
- - 1. Perform ! CreateDataProperty(*this*, `"highWaterMark"`, _highWaterMark_). - +
+ The size attribute's getter + steps are: -
Properties of the {{CountQueuingStrategy}} prototype
+ 1. Return [=this=]'s [=relevant global object=]'s [=count queuing strategy size function=]. +
-
size()
+

Abstract operations

-
- The size method returns one always, so that the total queue size is a count of the number of chunks in - the queue. - - This method is intentionally generic; it does not require that its this value be a - CountQueuingStrategy object. -
- - - 1. Return *1*. - - -

Queue-with-sizes operations

- -The streams in this specification use a "queue-with-sizes" data structure to store queued up values, along with their -determined sizes. Various specification objects contain a queue-with-sizes, represented by the object having two paired -internal slots, always named \[[queue]] and \[[queueTotalSize]]. \[[queue]] is a List of Records with \[[value]] and -\[[size]] fields, and \[[queueTotalSize]] is a JavaScript {{Number}}, i.e. a double-precision floating point number. - -The following abstract operations are used when operating on objects that contain queues-with-sizes, in order to ensure -that the two internal slots stay synchronized. - -

Due to the limited precision of floating-point arithmetic, the framework specified here, of keeping a -running total in the \[[queueTotalSize]] slot, is not equivalent to adding up the size of all chunks in -\[[queue]]. (However, this only makes a difference when there is a huge (~1015) variance in size between -chunks, or when trillions of chunks are enqueued.)

- -

DequeueValue ( container )

- - - 1. Assert: _container_ has [[queue]] and [[queueTotalSize]] internal slots. - 1. Assert: _container_.[[queue]] is not empty. - 1. Let _pair_ be the first element of _container_.[[queue]]. - 1. Remove _pair_ from _container_.[[queue]], shifting all other elements downward (so that the second becomes the - first, and so on). - 1. Set _container_.[[queueTotalSize]] to _container_.[[queueTotalSize]] − _pair_.[[size]]. - 1. If _container_.[[queueTotalSize]] < *0*, set _container_.[[queueTotalSize]] to *0*. (This can occur due to - rounding errors.) - 1. Return _pair_.[[value]]. - - -

EnqueueValueWithSize ( container, -value, size )

- - - 1. Assert: _container_ has [[queue]] and [[queueTotalSize]] internal slots. - 1. Let _size_ be ? ToNumber(_size_). - 1. If ! IsFiniteNonNegativeNumber(_size_) is *false*, throw a *RangeError* exception. - 1. Append Record {[[value]]: _value_, [[size]]: _size_} as the last element of _container_.[[queue]]. - 1. Set _container_.[[queueTotalSize]] to _container_.[[queueTotalSize]] + _size_. - - -

PeekQueueValue ( container )

- - - 1. Assert: _container_ has [[queue]] and [[queueTotalSize]] internal slots. - 1. Assert: _container_.[[queue]] is not empty. - 1. Let _pair_ be the first element of _container_.[[queue]]. - 1. Return _pair_.[[value]]. - - -

ResetQueue ( container )

- - - 1. Assert: _container_ has [[queue]] and [[queueTotalSize]] internal slots. - 1. Set _container_.[[queue]] to a new empty List. - 1. Set _container_.[[queueTotalSize]] to *0*. - - -

Miscellaneous operations

- -A few abstract operations are used in this specification for utility purposes. We define them here. - -

CreateAlgorithmFromUnderlyingMethod ( underlyingObject, methodName, -algoArgCount, extraArgs )

- - - 1. Assert: _underlyingObject_ is not *undefined*. - 1. Assert: ! IsPropertyKey(_methodName_) is *true*. - 1. Assert: _algoArgCount_ is *0* or *1*. - 1. Assert: _extraArgs_ is a List. - 1. Let _method_ be ? GetV(_underlyingObject_, _methodName_). - 1. If _method_ is not *undefined*, - 1. If ! IsCallable(_method_) is *false*, throw a *TypeError* exception. - 1. If _algoArgCount_ is *0*, return an algorithm that performs the following steps: - 1. Return ! PromiseCall(_method_, _underlyingObject_, _extraArgs_). - 1. Otherwise, return an algorithm that performs the following steps, taking an _arg_ argument: - 1. Let _fullArgs_ be a List consisting of _arg_ followed by the elements of _extraArgs_ in order. - 1. Return ! PromiseCall(_method_, _underlyingObject_, _fullArgs_). - 1. Return an algorithm which returns a promise resolved with *undefined*. - - -

InvokeOrNoop ( O, P, args -)

+The following algorithms are used by the stream constructors to extract the relevant pieces from +a {{QueuingStrategy}} dictionary. -
- InvokeOrNoop is a slight modification of the [[!ECMASCRIPT]] Invoke abstract operation to return - undefined when the method is not present. +
+ ExtractHighWaterMark(|strategy|, |defaultHWM|) + performs the following steps: + + 1. If |strategy|["{{QueuingStrategy/highWaterMark}}"] does not [=map/exist=], return |defaultHWM|. + 1. Let |highWaterMark| be |strategy|["{{QueuingStrategy/highWaterMark}}"]. + 1. If |highWaterMark| is NaN or |highWaterMark| < 0, throw a {{RangeError}} exception. + 1. Return |highWaterMark|. + +

+∞ is explicitly allowed as a valid [=high water mark=]. It causes [=backpressure=] + to never be applied. +

+ +
+ ExtractSizeAlgorithm(|strategy|) + performs the following steps: + + 1. If |strategy|["{{QueuingStrategy/size}}"] does not [=map/exist=], return an algorithm that + returns 1. + 1. Return an algorithm that performs the following steps, taking a |chunk| argument: + 1. Return the result of [=invoke|invoking=] |strategy|["{{QueuingStrategy/size}}"] with argument + list « |chunk| ».
- - 1. Assert: _O_ is not *undefined*. - 1. Assert: ! IsPropertyKey(_P_) is *true*. - 1. Assert: _args_ is a List. - 1. Let _method_ be ? GetV(_O_, _P_). - 1. If _method_ is *undefined*, return *undefined*. - 1. Return ? Call(_method_, _O_, _args_). - +

Supporting abstract operations

+ +The following abstract operations each support the implementation of more than one type of stream, +and as such are not grouped under the major sections above. + +

Queue-with-sizes

+ +The streams in this specification use a "queue-with-sizes" data structure to store queued up +values, along with their determined sizes. Various specification objects contain a +queue-with-sizes, represented by the object having two paired internal slots, always named +\[[queue]] and \[[queueTotalSize]]. \[[queue]] is a [=list=] of Records with \[[value]] and +\[[size]] fields, and \[[queueTotalSize]] is a JavaScript {{Number}}, i.e. a double-precision +floating point number. + +The following abstract operations are used when operating on objects that contain +queues-with-sizes, in order to ensure that the two internal slots stay synchronized. + +

Due to the limited precision of floating-point arithmetic, the framework +specified here, of keeping a running total in the \[[queueTotalSize]] slot, is not +equivalent to adding up the size of all [=chunks=] in \[[queue]]. (However, this only makes a +difference when there is a huge (~1015) variance in size between chunks, or when +trillions of chunks are enqueued.) + +

+ DequeueValue(|container|) performs the + following steps: + + 1. Assert: |container| has \[[queue]] and \[[queueTotalSize]] internal slots. + 1. Assert: |container|.\[[queue]] is not [=list/is empty|empty=]. + 1. Let |pair| be |container|.\[[queue]][0]. + 1. [=list/Remove=] |pair| from |container|.\[[queue]]. + 1. Set |container|.\[[queueTotalSize]] to |container|.\[[queueTotalSize]] − |pair|.\[[size]]. + 1. If |container|.\[[queueTotalSize]] < 0, set |container|.\[[queueTotalSize]] to 0. (This can + occur due to rounding errors.) + 1. Return |pair|.\[[value]]. +
-

IsFiniteNonNegativeNumber ( v -)

+
+ EnqueueValueWithSize(|container|, |value|, |size|) performs the + following steps: - - 1. If ! IsNonNegativeNumber(_v_) is *false*, return *false*. - 1. If _v_ is *+∞*, return *false*. - 1. Return *true*. - + 1. Assert: |container| has \[[queue]] and \[[queueTotalSize]] internal slots. + 1. If ! [$IsNonNegativeNumber$](|v|) is false, throw a {{RangeError}} exception. + 1. If |v| is +∞, throw a {{RangeError}} exception. + 1. [=list/Append=] Record {\[[value]]: |value|, \[[size]]: |size|} to |container|.\[[queue]]. + 1. Set |container|.\[[queueTotalSize]] to |container|.\[[queueTotalSize]] + |size|. +
-

IsNonNegativeNumber ( v )

+
+ PeekQueueValue(|container|) + performs the following steps: - - 1. If Type(_v_) is not Number, return *false*. - 1. If _v_ is *NaN*, return *false*. - 1. If _v_ < *0*, return *false*. - 1. Return *true*. - + 1. Assert: |container| has \[[queue]] and \[[queueTotalSize]] internal slots. + 1. Assert: |container|.\[[queue]] is not [=list/is empty|empty=]. + 1. Let |pair| be |container|.\[[queue]][0]. + 1. Return |pair|.\[[value]]. +
-

PromiseCall ( F, V, -args )

+
+ ResetQueue(|container|) + performs the following steps: -
- This encapsulates the relevant promise-related parts of the Web IDL call a user object's operation algorithm - for use while we work on moving to Web IDL. -
- - - 1. Assert: ! IsCallable(_F_) is *true*. - 1. Assert: _V_ is not *undefined*. - 1. Assert: _args_ is a List. - 1. Let _returnValue_ be Call(_F_, _V_, _args_). - 1. If _returnValue_ is an abrupt completion, return a promise rejected with _returnValue_.[[Value]]. - 1. Otherwise, return a promise resolved with _returnValue_.[[Value]]. - - -

TransferArrayBuffer ( O )

- - - 1. Assert: Type(_O_) is Object. - 1. Assert: _O_ has an [[ArrayBufferData]] internal slot. - 1. Assert: ! IsDetachedBuffer(_O_) is *false*. - 1. Let _arrayBufferData_ be _O_.[[ArrayBufferData]]. - 1. Let _arrayBufferByteLength_ be _O_.[[ArrayBufferByteLength]]. - 1. Perform ! DetachArrayBuffer(_O_). - 1. Return a new ArrayBuffer object (created in the current Realm Record) whose - [[ArrayBufferData]] internal slot value is _arrayBufferData_ and whose [[ArrayBufferByteLength]] internal slot - value is _arrayBufferByteLength_. - - -

ValidateAndNormalizeHighWaterMark ( highWaterMark )

- - - 1. Set _highWaterMark_ to ? ToNumber(_highWaterMark_). - 1. If _highWaterMark_ is *NaN* or _highWaterMark_ < *0*, throw a *RangeError* exception. -

*+∞* is explicitly allowed as a valid high water mark. It causes backpressure to never be applied.

- 1. Return _highWaterMark_. -
- -

MakeSizeAlgorithmFromSizeFunction ( size )

- - - 1. If _size_ is *undefined*, return an algorithm that returns *1*. - 1. If ! IsCallable(_size_) is *false*, throw a *TypeError* exception. - 1. Return an algorithm that performs the following steps, taking a _chunk_ argument: - 1. Return ? Call(_size_, *undefined*, « _chunk_ »). - - -

Global properties

- -The following constructors must be exposed on the global object as data properties of the same name: - -
    -
  • {{ReadableStream}} -
  • {{WritableStream}} -
  • {{TransformStream}} -
  • {{ByteLengthQueuingStrategy}} -
  • {{CountQueuingStrategy}} -
- -The attributes of these properties must be { \[[Writable]]: true, \[[Enumerable]]: false, -\[[Configurable]]: true }. + 1. Assert: |container| has \[[queue]] and \[[queueTotalSize]] internal slots. + 1. Set |container|.\[[queue]] to a new empty [=list=]. + 1. Set |container|.\[[queueTotalSize]] to 0. +
-
- The {{ReadableStreamDefaultReader}}, {{ReadableStreamBYOBReader}}, {{ReadableStreamDefaultController}}, - {{ReadableByteStreamController}}, {{WritableStreamDefaultWriter}}, {{WritableStreamDefaultController}}, and - {{TransformStreamDefaultController}} classes are specifically not exposed, as they are not independently useful. +

Miscellaneous

+ +The following abstract operations are a grab-bag of utilities. + +
+ IsNonNegativeNumber(|v|) performs the following steps: + + 1. If [$Type$](|v|) is not Number, return false. + 1. If |v| is NaN, return false. + 1. If |v| < 0, return false. + 1. Return true. +
+ +
+ TransferArrayBuffer(|O|) performs the following steps: + + 1. Assert: [$Type$](|O|) is Object. + 1. Assert: |O| has an \[[ArrayBufferData]] internal slot. + 1. Assert: ! [$IsDetachedBuffer$](|O|) is false. + 1. Let |arrayBufferData| be |O|.\[[ArrayBufferData]]. + 1. Let |arrayBufferByteLength| be |O|.\[[ArrayBufferByteLength]]. + 1. Perform ! [$DetachArrayBuffer$](|O|). + 1. Return a new {{ArrayBuffer}} object, created in [=the current Realm=], whose + \[[ArrayBufferData]] internal slot value is |arrayBufferData| and whose + \[[ArrayBufferByteLength]] internal slot value is |arrayBufferByteLength|.

Examples of creating streams

@@ -5458,663 +5602,654 @@ The attributes of these properties must be { \[[Writable]]: trueThis section, and all its subsections, are non-normative. -The previous examples throughout the standard have focused on how to use streams. Here we show how to create a stream, -using the {{ReadableStream}} or {{WritableStream}} constructors. +The previous examples throughout the standard have focused on how to use streams. Here we show how +to create a stream, using the {{ReadableStream}}, {{WritableStream}}, and {{TransformStream}} +constructors. -

A readable stream with an underlying push source (no backpressure support)

+

A readable stream with an underlying push source (no +backpressure support)

-The following function creates readable streams that wrap {{WebSocket}} instances [[HTML]], which are push sources -that do not support backpressure signals. It illustrates how, when adapting a push source, usually most of the work -happens in the {{underlying source/start()}} function. +The following function creates [=readable streams=] that wrap {{WebSocket}} instances [[HTML]], +which are [=push sources=] that do not support backpressure signals. It illustrates how, when +adapting a push source, usually most of the work happens in the {{UnderlyingSource/start|start()}} +method. -

-  function makeReadableWebSocketStream(url, protocols) {
-    const ws = new WebSocket(url, protocols);
-    ws.binaryType = "arraybuffer";
+
+function makeReadableWebSocketStream(url, protocols) {
+  const ws = new WebSocket(url, protocols);
+  ws.binaryType = "arraybuffer";
 
-    return new ReadableStream({
-      start(controller) {
-        ws.onmessage = event => controller.enqueue(event.data);
-        ws.onclose = () => controller.close();
-        ws.onerror = () => controller.error(new Error("The WebSocket errored!"));
-      },
+  return new ReadableStream({
+    start(controller) {
+      ws.onmessage = event => controller.enqueue(event.data);
+      ws.onclose = () => controller.close();
+      ws.onerror = () => controller.error(new Error("The WebSocket errored!"));
+    },
 
-      cancel() {
-        ws.close();
-      }
-    });
-  }
-</code></pre>
+    cancel() {
+      ws.close();
+    }
+  });
+}
+
 
-We can then use this function to create readable streams for a web socket, and pipe that stream to an arbitrary
-writable stream:
+We can then use this function to create readable streams for a web socket, and pipe that stream to
+an arbitrary writable stream:
 
-

-  const webSocketStream = makeReadableWebSocketStream("wss://example.com:443/", "protocol");
+
+const webSocketStream = makeReadableWebSocketStream("wss://example.com:443/", "protocol");
 
-  webSocketStream.pipeTo(writableStream)
-    .then(() => console.log("All data successfully written!"))
-    .catch(e => console.error("Something went wrong!", e));
-</code></pre>
+webSocketStream.pipeTo(writableStream)
+  .then(() => console.log("All data successfully written!"))
+  .catch(e => console.error("Something went wrong!", e));
+
 
 
- This specific style of wrapping a web socket interprets web socket messages directly as chunks. - This can be a convenient abstraction, for example when piping to a writable stream or transform - stream for which each web socket message makes sense as a chunk to consume or transform. - - However, often when people talk about "adding streams support to web sockets", they are hoping instead for a new - capability to send an individual web socket message in a streaming fashion, so that e.g. a file could be transferred - in a single message without holding all of its contents in memory on the client side. To accomplish this goal, we'd - instead want to allow individual web socket messages to themselves be {{ReadableStream}} instances. That isn't what we - show in the above example. - - For more background, see this - discussion. + This specific style of wrapping a web socket interprets web socket messages directly as + [=chunks=]. This can be a convenient abstraction, for example when [=piping=] to a [=writable + stream=] or [=transform stream=] for which each web socket message makes sense as a chunk to + consume or transform. + + However, often when people talk about "adding streams support to web sockets", they are hoping + instead for a new capability to send an individual web socket message in a streaming fashion, so + that e.g. a file could be transferred in a single message without holding all of its contents in + memory on the client side. To accomplish this goal, we'd instead want to allow individual web + socket messages to themselves be {{ReadableStream}} instances. That isn't what we show in the + above example. + + For more background, see this discussion.
-

A readable stream with an underlying push source and backpressure support

+

A readable stream with an underlying push source and +backpressure support

-The following function returns readable streams that wrap "backpressure sockets," which are hypothetical objects -that have the same API as web sockets, but also provide the ability to pause and resume the flow of data with their -readStop and readStart methods. In doing so, this example shows how to apply -backpressure to underlying sources that support it. +The following function returns [=readable streams=] that wrap "backpressure sockets," which are +hypothetical objects that have the same API as web sockets, but also provide the ability to pause +and resume the flow of data with their readStop and readStart methods. In +doing so, this example shows how to apply [=backpressure=] to [=underlying sources=] that support +it. -

-  function makeReadableBackpressureSocketStream(host, port) {
-    const socket = createBackpressureSocket(host, port);
+
+function makeReadableBackpressureSocketStream(host, port) {
+  const socket = createBackpressureSocket(host, port);
 
-    return new ReadableStream({
-      start(controller) {
-        socket.ondata = event => {
-          controller.enqueue(event.data);
+  return new ReadableStream({
+    start(controller) {
+      socket.ondata = event => {
+        controller.enqueue(event.data);
 
-          if (controller.desiredSize <= 0) {
-            // The internal queue is full, so propagate
-            // the backpressure signal to the underlying source.
-            socket.readStop();
-          }
-        };
+        if (controller.desiredSize <= 0) {
+          // The internal queue is full, so propagate
+          // the backpressure signal to the underlying source.
+          socket.readStop();
+        }
+      };
 
-        socket.onend = () => controller.close();
-        socket.onerror = () => controller.error(new Error("The socket errored!"));
-      },
+      socket.onend = () => controller.close();
+      socket.onerror = () => controller.error(new Error("The socket errored!"));
+    },
 
-      pull() {
-        // This is called if the internal queue has been emptied, but the
-        // stream's consumer still wants more data. In that case, restart
-        // the flow of data if we have previously paused it.
-        socket.readStart();
-      },
+    pull() {
+      // This is called if the internal queue has been emptied, but the
+      // stream's consumer still wants more data. In that case, restart
+      // the flow of data if we have previously paused it.
+      socket.readStart();
+    },
 
-      cancel() {
-        socket.close();
-      }
-    });
-  }
-</code></pre>
-
-We can then use this function to create readable streams for such "backpressure sockets" in the same way we do for web
-sockets. This time, however, when we pipe to a destination that cannot accept data as fast as the socket is producing
-it, or if we leave the stream alone without reading from it for some time, a backpressure signal will be sent to the
-socket.
-
-<h3 id="example-rbs-push">A readable byte stream with an underlying push source (no backpressure support)</h3>
-
-The following function returns <a>readable byte streams</a> that wraps a hypothetical UDP socket API, including a
-promise-returning <code>select2()</code> method that is meant to be evocative of the POSIX select(2) system call.
-
-Since the UDP protocol does not have any built-in backpressure support, the backpressure signal given by
-{{ReadableByteStreamController/desiredSize}} is ignored, and the stream ensures that when data is available from the
-socket but not yet requested by the developer, it is enqueued in the stream's <a>internal queue</a>, to avoid overflow
-of the kernel-space queue and a consequent loss of data.
-
-This has some interesting consequences for how <a>consumers</a> interact with the stream. If the consumer does not read
-data as fast as the socket produces it, the <a>chunks</a> will remain in the stream's <a>internal queue</a>
-indefinitely. In this case, using a <a>BYOB reader</a> will cause an extra copy, to move the data from the stream's
-internal queue to the developer-supplied buffer. However, if the consumer consumes the data quickly enough, a <a>BYOB
-reader</a> will allow zero-copy reading directly into developer-supplied buffers.
-
-(You can imagine a more complex version of this example which uses {{ReadableByteStreamController/desiredSize}} to
-inform an out-of-band backpressure signaling mechanism, for example by sending a message down the socket to adjust the
-rate of data being sent. That is left as an exercise for the reader.)
-
-<pre><code class="lang-javascript">
-  const DEFAULT_CHUNK_SIZE = 65536;
-
-  function makeUDPSocketStream(host, port) {
-    const socket = createUDPSocket(host, port);
-
-    return new ReadableStream({
-      type: "bytes",
-
-      start(controller) {
-        readRepeatedly().catch(e => controller.error(e));
-
-        function readRepeatedly() {
-          return socket.select2().then(() => {
-            // Since the socket can become readable even when there’s
-            // no pending BYOB requests, we need to handle both cases.
-            let bytesRead;
-            if (controller.byobRequest) {
-              const v = controller.byobRequest.view;
-              bytesRead = socket.readInto(v.buffer, v.byteOffset, v.byteLength);
-              controller.byobRequest.respond(bytesRead);
-            } else {
-              const buffer = new ArrayBuffer(DEFAULT_CHUNK_SIZE);
-              bytesRead = socket.readInto(buffer, 0, DEFAULT_CHUNK_SIZE);
-              controller.enqueue(new Uint8Array(buffer, 0, bytesRead));
-            }
-
-            if (bytesRead === 0) {
-              controller.close();
-              return;
-            }
-
-            return readRepeatedly();
-          });
-        }
-      },
+    cancel() {
+      socket.close();
+    }
+  });
+}
+
 
-      cancel() {
-        socket.close();
-      }
-    });
-  }
-
+We can then use this function to create readable streams for such "backpressure sockets" in the +same way we do for web sockets. This time, however, when we pipe to a destination that cannot +accept data as fast as the socket is producing it, or if we leave the stream alone without reading +from it for some time, a backpressure signal will be sent to the socket. -{{ReadableStream}} instances returned from this function can now vend BYOB readers, with all of the -aforementioned benefits and caveats. +

A readable byte stream with an underlying push source (no backpressure +support)

-

A readable stream with an underlying pull source

+The following function returns [=readable byte streams=] that wraps a hypothetical UDP socket API, +including a promise-returning select2() method that is meant to be evocative of the +POSIX select(2) system call. -The following function returns readable streams that wrap portions of the -Node.js file system API (which themselves map fairly directly to C's -fopen, fread, and fclose trio). Files are a typical example of pull -sources. Note how in contrast to the examples with push sources, most of the work here happens on-demand in the -{{underlying source/pull()}} function, and not at startup time in the {{underlying source/start()}} function. +Since the UDP protocol does not have any built-in backpressure support, the backpressure signal +given by {{ReadableByteStreamController/desiredSize}} is ignored, and the stream ensures that when +data is available from the socket but not yet requested by the developer, it is enqueued in the +stream's [=internal queue=], to avoid overflow of the kernel-space queue and a consequent loss of +data. -

-  const fs = require("pr/fs"); // https://github.com/jden/pr
-  const CHUNK_SIZE = 1024;
+This has some interesting consequences for how [=consumers=] interact with the stream. If the
+consumer does not read data as fast as the socket produces it, the [=chunks=] will remain in the
+stream's [=internal queue=] indefinitely. In this case, using a [=BYOB reader=] will cause an extra
+copy, to move the data from the stream's internal queue to the developer-supplied buffer. However,
+if the consumer consumes the data quickly enough, a [=BYOB reader=] will allow zero-copy reading
+directly into developer-supplied buffers.
 
-  function makeReadableFileStream(filename) {
-    let fd;
-    let position = 0;
+(You can imagine a more complex version of this example which uses
+{{ReadableByteStreamController/desiredSize}} to inform an out-of-band backpressure signaling
+mechanism, for example by sending a message down the socket to adjust the rate of data being sent.
+That is left as an exercise for the reader.)
 
-    return new ReadableStream({
-      start() {
-        return fs.open(filename, "r").then(result => {
-          fd = result;
-        });
-      },
+
+const DEFAULT_CHUNK_SIZE = 65536;
 
-      pull(controller) {
-        const buffer = new ArrayBuffer(CHUNK_SIZE);
+function makeUDPSocketStream(host, port) {
+  const socket = createUDPSocket(host, port);
 
-        return fs.read(fd, buffer, 0, CHUNK_SIZE, position).then(bytesRead => {
-          if (bytesRead === 0) {
-            return fs.close(fd).then(() => controller.close());
+  return new ReadableStream({
+    type: "bytes",
+
+    start(controller) {
+      readRepeatedly().catch(e => controller.error(e));
+
+      function readRepeatedly() {
+        return socket.select2().then(() => {
+          // Since the socket can become readable even when there’s
+          // no pending BYOB requests, we need to handle both cases.
+          let bytesRead;
+          if (controller.byobRequest) {
+            const v = controller.byobRequest.view;
+            bytesRead = socket.readInto(v.buffer, v.byteOffset, v.byteLength);
+            controller.byobRequest.respond(bytesRead);
           } else {
-            position += bytesRead;
+            const buffer = new ArrayBuffer(DEFAULT_CHUNK_SIZE);
+            bytesRead = socket.readInto(buffer, 0, DEFAULT_CHUNK_SIZE);
             controller.enqueue(new Uint8Array(buffer, 0, bytesRead));
           }
-        });
-      },
 
-      cancel() {
-        return fs.close(fd);
-      }
-    });
-  }
-</code></pre>
+          if (bytesRead === 0) {
+            controller.close();
+            return;
+          }
 
-We can then create and use readable streams for files just as we could before for sockets.
+          return readRepeatedly();
+        });
+      }
+    },
 
-<h3 id="example-rbs-pull">A readable byte stream with an underlying pull source</h3>
+    cancel() {
+      socket.close();
+    }
+  });
+}
+
 
-The following function returns readable byte streams that allow efficient zero-copy reading of files, again
-using the Node.js file system API. Instead of using a predetermined chunk
-size of 1024, it attempts to fill the developer-supplied buffer, allowing full control.
+{{ReadableStream}} instances returned from this function can now vend [=BYOB readers=], with all of
+the aforementioned benefits and caveats.
 
-

-  const fs = require("pr/fs"); // https://github.com/jden/pr
-  const DEFAULT_CHUNK_SIZE = 1024;
+

A readable stream with an underlying pull source

- function makeReadableByteFileStream(filename) { - let fd; - let position = 0; +The following function returns [=readable streams=] that wrap portions of the Node.js file system API (which themselves map fairly +directly to C's fopen, fread, and fclose trio). Files are a +typical example of [=pull sources=]. Note how in contrast to the examples with push sources, most +of the work here happens on-demand in the {{UnderlyingSource/pull|pull()}} function, and not at +startup time in the {{UnderlyingSource/start|start()}} function. + + +const fs = require("fs").promises; +const CHUNK_SIZE = 1024; + +function makeReadableFileStream(filename) { + let fileHandle; + let position = 0; + + return new ReadableStream({ + async start() { + fileHandle = await fs.open(filename, "r"); + }, + + async pull(controller) { + const buffer = new ArrayBuffer(CHUNK_SIZE); + + const { bytesRead } = await fileHandle.read(buffer, 0, CHUNK_SIZE, position); + if (bytesRead === 0) { + await fileHandle.close(); + controller.close(); + } else { + position += bytesRead; + controller.enqueue(new Uint8Array(buffer, 0, bytesRead)); + } + }, - return new ReadableStream({ - type: "bytes", + cancel() { + return fileHandle.close(); + } + }); +} + - start() { - return fs.open(filename, "r").then(result => { - fd = result; - }); - }, +We can then create and use readable streams for files just as we could before for sockets. - pull(controller) { - // Even when the consumer is using the default reader, the auto-allocation - // feature allocates a buffer and passes it to us via byobRequest. - const v = controller.byobRequest.view; +

A readable byte stream with an underlying pull source

- return fs.read(fd, v.buffer, v.byteOffset, v.byteLength, position).then(bytesRead => { - if (bytesRead === 0) { - return fs.close(fd).then(() => controller.close()); - } else { - position += bytesRead; - controller.byobRequest.respond(bytesRead); - } - }); - }, +The following function returns [=readable byte streams=] that allow efficient zero-copy reading of +files, again using the Node.js file system API. +Instead of using a predetermined chunk size of 1024, it attempts to fill the developer-supplied +buffer, allowing full control. + + +const fs = require("fs").promises; +const DEFAULT_CHUNK_SIZE = 1024; + +function makeReadableByteFileStream(filename) { + let fileHandle; + let position = 0; + + return new ReadableStream({ + type: "bytes", + + async start() { + fileHandle = await fs.open(filename, "r"); + }, + + pull(controller) { + // Even when the consumer is using the default reader, the auto-allocation + // feature allocates a buffer and passes it to us via byobRequest. + const v = controller.byobRequest.view; + + const { bytesRead } = await fileHandle.read(v.buffer, v.byteOffset, v.byteLength); + if (bytesRead === 0) { + await fileHandle.close(); + controller.close(); + } else { + position += bytesRead; + controller.byobRequest.respond(bytesRead); + } + }, - cancel() { - return fs.close(fd); - }, + cancel() { + return fs.close(fd); + }, - autoAllocateChunkSize: DEFAULT_CHUNK_SIZE - }); - } -</code></pre> + autoAllocateChunkSize: DEFAULT_CHUNK_SIZE + }); +} + -With this in hand, we can create and use BYOB readers for the returned {{ReadableStream}}. But we can -also create default readers, using them in the same simple and generic manner as usual. The adaptation between -the low-level byte tracking of the underlying byte source shown here, and the higher-level chunk-based -consumption of a default reader, is all taken care of automatically by the streams implementation. The -auto-allocation feature, via the autoAllocateChunkSize option, even allows -us to write less code, compared to the manual branching in [[#example-rbs-push]]. +With this in hand, we can create and use [=BYOB readers=] for the returned {{ReadableStream}}. But +we can also create [=default readers=], using them in the same simple and generic manner as usual. +The adaptation between the low-level byte tracking of the [=underlying byte source=] shown here, +and the higher-level chunk-based consumption of a [=default reader=], is all taken care of +automatically by the streams implementation. The auto-allocation feature, via the +{{UnderlyingSource/autoAllocateChunkSize}} option, even allows us to write less code, compared to +the manual branching in [[#example-rbs-push]].

A writable stream with no backpressure or success signals

-The following function returns a writable stream that wraps a {{WebSocket}} [[HTML]]. Web sockets do not provide -any way to tell when a given chunk of data has been successfully sent (without awkward polling of -{{WebSocket/bufferedAmount}}, which we leave as an exercise to the reader). As such, this writable stream has no ability -to communicate accurate backpressure signals or write success/failure to its producers. That is, the -promises returned by its writer's {{WritableStreamDefaultWriter/write()}} method and +The following function returns a [=writable stream=] that wraps a {{WebSocket}} [[HTML]]. Web +sockets do not provide any way to tell when a given chunk of data has been successfully sent +(without awkward polling of {{WebSocket/bufferedAmount}}, which we leave as an exercise to the +reader). As such, this writable stream has no ability to communicate accurate [=backpressure=] +signals or write success/failure to its [=producers=]. That is, the promises returned by its +[=writer=]'s {{WritableStreamDefaultWriter/write()}} method and {{WritableStreamDefaultWriter/ready}} getter will always fulfill immediately. -

-  function makeWritableWebSocketStream(url, protocols) {
-    const ws = new WebSocket(url, protocols);
-
-    return new WritableStream({
-      start(controller) {
-        ws.onerror = () => {
-          controller.error(new Error("The WebSocket errored!"));
-          ws.onclose = null;
-        };
-        ws.onclose = () => controller.error(new Error("The server closed the connection unexpectedly!"));
-        return new Promise(resolve => ws.onopen = resolve);
-      },
-
-      write(chunk) {
-        ws.send(chunk);
-        // Return immediately, since the web socket gives us no easy way to tell
-        // when the write completes.
-      },
-
-      close() {
-        return closeWS(1000);
-      },
-
-      abort(reason) {
-        return closeWS(4000, reason && reason.message);
-      },
-    });
-
-    function closeWS(code, reasonString) {
-      return new Promise((resolve, reject) => {
-        ws.onclose = e => {
-          if (e.wasClean) {
-            resolve();
-          } else {
-            reject(new Error("The connection was not closed cleanly"));
-          }
-        };
-        ws.close(code, reasonString);
-      });
-    }
-  }
-
- -We can then use this function to create writable streams for a web socket, and pipe an arbitrary readable stream to it: + +function makeWritableWebSocketStream(url, protocols) { + const ws = new WebSocket(url, protocols); -<pre><code class="lang-javascript"> - const webSocketStream = makeWritableWebSocketStream("wss://example.com:443/", "protocol"); - - readableStream.pipeTo(webSocketStream) - .then(() => console.log("All data successfully written!")) - .catch(e => console.error("Something went wrong!", e)); -</code></pre> + return new WritableStream({ + start(controller) { + ws.onerror = () => { + controller.error(new Error("The WebSocket errored!")); + ws.onclose = null; + }; + ws.onclose = () => controller.error(new Error("The server closed the connection unexpectedly!")); + return new Promise(resolve => ws.onopen = resolve); + }, -<p class="note">See <a href="#note-web-socket-wrapping-examples">the earlier note</a> about this style of wrapping web -sockets into streams.</p> + write(chunk) { + ws.send(chunk); + // Return immediately, since the web socket gives us no easy way to tell + // when the write completes. + }, -<h3 id="example-ws-backpressure">A writable stream with backpressure and success signals</h3> + close() { + return closeWS(1000); + }, -The following function returns <a>writable streams</a> that wrap portions of the <a -href="https://nodejs.org/api/fs.html">Node.js file system API</a> (which themselves map fairly directly to C's -<code>fopen</code>, <code>fwrite</code>, and <code>fclose</code> trio). Since the API we are wrapping provides a way to -tell when a given write succeeds, this stream will be able to communicate <a>backpressure</a> signals as well as whether -an individual write succeeded or failed. + abort(reason) { + return closeWS(4000, reason && reason.message); + }, + }); -<pre><code class="lang-javascript"> - const fs = require("pr/fs"); // https://github.com/jden/pr + function closeWS(code, reasonString) { + return new Promise((resolve, reject) => { + ws.onclose = e => { + if (e.wasClean) { + resolve(); + } else { + reject(new Error("The connection was not closed cleanly")); + } + }; + ws.close(code, reasonString); + }); + } +} + - function makeWritableFileStream(filename) { - let fd; +We can then use this function to create writable streams for a web socket, and pipe an arbitrary +readable stream to it: - return new WritableStream({ - start() { - return fs.open(filename, "w").then(result => { - fd = result; - }); - }, + +const webSocketStream = makeWritableWebSocketStream("wss://example.com:443/", "protocol"); - write(chunk) { - return fs.write(fd, chunk, 0, chunk.length); - }, +readableStream.pipeTo(webSocketStream) + .then(() => console.log("All data successfully written!")) + .catch(e => console.error("Something went wrong!", e)); + - close() { - return fs.close(fd); - }, +

See the earlier note about this +style of wrapping web sockets into streams. - abort() { - return fs.close(fd); - } - }); - } -

+

A writable stream with backpressure and success signals

-We can then use this function to create a writable stream for a file, and write individual chunks of data to it: +The following function returns [=writable streams=] that wrap portions of the Node.js file system API (which themselves map fairly +directly to C's fopen, fwrite, and fclose trio). Since the +API we are wrapping provides a way to tell when a given write succeeds, this stream will be able to +communicate [=backpressure=] signals as well as whether an individual write succeeded or failed. -

-  const fileStream = makeWritableFileStream("/example/path/on/fs.txt");
-  const writer = fileStream.getWriter();
+
+const fs = require("fs").promises;
 
-  writer.write("To stream, or not to stream\n");
-  writer.write("That is the question\n");
+function makeWritableFileStream(filename) {
+  let fileHandle;
 
-  writer.close()
-    .then(() => console.log("chunks written and stream closed successfully!"))
-    .catch(e => console.error(e));
-</code></pre>
+  return new WritableStream({
+    async start() {
+      fileHandle = await fs.open(filename, "w");
+    },
 
-Note that if a particular call to <code>fs.write</code> takes a longer time, the returned promise will fulfill later.
-In the meantime, additional writes can be queued up, which are stored in the stream's internal queue. The accumulation
-of chunks in this queue can change the stream to return a pending promise from the {{WritableStreamDefaultWriter/ready}}
-getter, which is a signal to <a>producers</a> that they would benefit from backing off and stopping writing, if
-possible.
+    write(chunk) {
+      return fileHandle.write(chunk, 0, chunk.length);
+    },
 
-The way in which the writable stream queues up writes is especially important in this case, since as stated in
-<a href="https://nodejs.org/api/fs.html#fs_fs_write_fd_data_position_encoding_callback">the documentation for
-<code>fs.write</code></a>, "it is unsafe to use <code>fs.write</code> multiple times on the same file without waiting
-for the [promise]." But we don't have to worry about that when writing the <code>makeWritableFileStream</code>
-function, since the stream implementation guarantees that the <a>underlying sink</a>'s {{underlying sink/write()}}
-method will not be called until any promises returned by previous calls have fulfilled!
+    close() {
+      return fs.close(fd);
+    },
 
-<h3 id="example-both">A { readable, writable } stream pair wrapping the same underlying resource</h3>
+    abort() {
+      return fs.close(fd);
+    }
+  });
+}
+
+
+We can then use this function to create a writable stream for a file, and write individual
+[=chunks=] of data to it:
+
+
+const fileStream = makeWritableFileStream("/example/path/on/fs.txt");
+const writer = fileStream.getWriter();
+
+writer.write("To stream, or not to stream\n");
+writer.write("That is the question\n");
+
+writer.close()
+  .then(() => console.log("chunks written and stream closed successfully!"))
+  .catch(e => console.error(e));
+
+
+Note that if a particular call to fileHandle.write takes a longer time, the returned
+promise will fulfill later. In the meantime, additional writes can be queued up, which are stored
+in the stream's internal queue. The accumulation of chunks in this queue can change the stream to
+return a pending promise from the {{WritableStreamDefaultWriter/ready}} getter, which is a signal
+to [=producers=] that they would benefit from backing off and stopping writing, if possible.
+
+The way in which the writable stream queues up writes is especially important in this case, since
+as stated in the
+documentation for fileHandle.write, "it is unsafe to use
+filehandle.write multiple times on the same file without waiting for the promise." But
+we don't have to worry about that when writing the makeWritableFileStream function,
+since the stream implementation guarantees that the [=underlying sink=]'s
+{{UnderlyingSink/write|write()}} method will not be called until any promises returned by previous
+calls have fulfilled!
+
+

A { readable, writable } stream pair wrapping the same underlying +resource

The following function returns an object of the form { readable, writable }, with the -readable property containing a readable stream and the writable property containing a -writable stream, where both streams wrap the same underlying web socket resource. In essence, this combines -[[#example-rs-push-no-backpressure]] and [[#example-ws-no-backpressure]]. - -While doing so, it illustrates how you can use JavaScript classes to create reusable underlying sink and underlying -source abstractions. - -

-  function streamifyWebSocket(url, protocol) {
-    const ws = new WebSocket(url, protocols);
-    ws.binaryType = "arraybuffer";
-
-    return {
-      readable: new ReadableStream(new WebSocketSource(ws)),
-      writable: new WritableStream(new WebSocketSink(ws))
-    };
+readable property containing a readable stream and the writable property
+containing a writable stream, where both streams wrap the same underlying web socket resource. In
+essence, this combines [[#example-rs-push-no-backpressure]] and [[#example-ws-no-backpressure]].
+
+While doing so, it illustrates how you can use JavaScript classes to create reusable underlying
+sink and underlying source abstractions.
+
+
+function streamifyWebSocket(url, protocol) {
+  const ws = new WebSocket(url, protocols);
+  ws.binaryType = "arraybuffer";
+
+  return {
+    readable: new ReadableStream(new WebSocketSource(ws)),
+    writable: new WritableStream(new WebSocketSink(ws))
+  };
+}
+
+class WebSocketSource {
+  constructor(ws) {
+    this._ws = ws;
   }
 
-  class WebSocketSource {
-    constructor(ws) {
-      this._ws = ws;
-    }
-
-    start(controller) {
-      this._ws.onmessage = event => controller.enqueue(event.data);
-      this._ws.onclose = () => controller.close();
+  start(controller) {
+    this._ws.onmessage = event => controller.enqueue(event.data);
+    this._ws.onclose = () => controller.close();
 
-      this._ws.addEventListener("error", () => {
-        controller.error(new Error("The WebSocket errored!"));
-      });
-    }
+    this._ws.addEventListener("error", () => {
+      controller.error(new Error("The WebSocket errored!"));
+    });
+  }
 
-    cancel() {
-      this._ws.close();
-    }
+  cancel() {
+    this._ws.close();
   }
+}
 
-  class WebSocketSink {
-    constructor(ws) {
-      this._ws = ws;
-    }
+class WebSocketSink {
+  constructor(ws) {
+    this._ws = ws;
+  }
 
-    start(controller) {
-      this._ws.onclose = () => controller.error(new Error("The server closed the connection unexpectedly!"));
-      this._ws.addEventListener("error", () => {
-        controller.error(new Error("The WebSocket errored!"));
-        this._ws.onclose = null;
-      });
+  start(controller) {
+    this._ws.onclose = () => controller.error(new Error("The server closed the connection unexpectedly!"));
+    this._ws.addEventListener("error", () => {
+      controller.error(new Error("The WebSocket errored!"));
+      this._ws.onclose = null;
+    });
 
-      return new Promise(resolve => this._ws.onopen = resolve);
-    }
+    return new Promise(resolve => this._ws.onopen = resolve);
+  }
 
-    write(chunk) {
-      this._ws.send(chunk);
-    }
+  write(chunk) {
+    this._ws.send(chunk);
+  }
 
-    close() {
-      return this._closeWS(1000);
-    }
+  close() {
+    return this._closeWS(1000);
+  }
 
-    abort(reason) {
-      return this._closeWS(4000, reason && reason.message);
-    }
+  abort(reason) {
+    return this._closeWS(4000, reason && reason.message);
+  }
 
-    _closeWS(code, reasonString) {
-      return new Promise((resolve, reject) => {
-        this._ws.onclose = e => {
-          if (e.wasClean) {
-            resolve();
-          } else {
-            reject(new Error("The connection was not closed cleanly"));
-          }
-        };
-        this._ws.close(code, reasonString);
-      });
-    }
+  _closeWS(code, reasonString) {
+    return new Promise((resolve, reject) => {
+      this._ws.onclose = e => {
+        if (e.wasClean) {
+          resolve();
+        } else {
+          reject(new Error("The connection was not closed cleanly"));
+        }
+      };
+      this._ws.close(code, reasonString);
+    });
   }
-</code></pre>
+}
+
 
-We can then use the objects created by this function to communicate with a remote web socket, using the standard stream
-APIs:
+We can then use the objects created by this function to communicate with a remote web socket, using
+the standard stream APIs:
 
-

-  const streamyWS = streamifyWebSocket("wss://example.com:443/", "protocol");
-  const writer = streamyWS.writable.getWriter();
-  const reader = streamyWS.readable.getReader();
+
+const streamyWS = streamifyWebSocket("wss://example.com:443/", "protocol");
+const writer = streamyWS.writable.getWriter();
+const reader = streamyWS.readable.getReader();
 
-  writer.write("Hello");
-  writer.write("web socket!");
+writer.write("Hello");
+writer.write("web socket!");
 
-  reader.read().then(({ value, done }) => {
-    console.log("The web socket says: ", value);
-  });
-</code></pre>
+reader.read().then(({ value, done }) => {
+  console.log("The web socket says: ", value);
+});
+
 
-Note how in this setup canceling the readable side will implicitly close the writable side,
-and similarly, closing or aborting the writable side will implicitly close the readable side.
+Note how in this setup canceling the readable side will implicitly close the
+writable side, and similarly, closing or aborting the writable side will
+implicitly close the readable side.
 
-

See the earlier note about this style of wrapping web -sockets into streams.

+

See the earlier note about this +style of wrapping web sockets into streams.

A transform stream that replaces template tags

-It's often useful to substitute tags with variables on a stream of data, where the parts that need to be replaced are -small compared to the overall data size. This example presents a simple way to do that. It maps strings to strings, -transforming a template like "Time: \{{time}} Message: \{{message}}" to "Time: 15:36 Message: -hello" assuming that { time: "15:36", message: "hello" } was passed in the -substitutions parameter to LipFuzzTransformer. - -This example also demonstrates one way to deal with a situation where a chunk contains partial data that cannot be -transformed until more data is received. In this case, a partial template tag will be accumulated in the -partialChunk instance variable until either the end of the tag is found or the end of the stream is -reached. - -

-  class LipFuzzTransformer {
-    constructor(substitutions) {
-      this.substitutions = substitutions;
-      this.partialChunk = "";
-      this.lastIndex = undefined;
-    }
+It's often useful to substitute tags with variables on a stream of data, where the parts that need
+to be replaced are small compared to the overall data size. This example presents a simple way to
+do that. It maps strings to strings, transforming a template like "Time: \{{time}} Message:
+\{{message}}" to "Time: 15:36 Message: hello" assuming that { time:
+"15:36", message: "hello" } was passed in the substitutions parameter to
+LipFuzzTransformer.
+
+This example also demonstrates one way to deal with a situation where a chunk contains partial data
+that cannot be transformed until more data is received. In this case, a partial template tag will
+be accumulated in the partialChunk property until either the end of the tag is found or
+the end of the stream is reached.
+
+
+class LipFuzzTransformer {
+  constructor(substitutions) {
+    this.substitutions = substitutions;
+    this.partialChunk = "";
+    this.lastIndex = undefined;
+  }
 
-    transform(chunk, controller) {
-      chunk = this.partialChunk + chunk;
-      this.partialChunk = "";
-      // lastIndex is the index of the first character after the last substitution.
-      this.lastIndex = 0;
-      chunk = chunk.replace(/\{\{([a-zA-Z0-9_-]+)\}\}/g, this.replaceTag.bind(this));
-      // Regular expression for an incomplete template at the end of a string.
-      const partialAtEndRegexp = /\{(\{([a-zA-Z0-9_-]+(\})?)?)?$/g;
-      // Avoid looking at any characters that have already been substituted.
-      partialAtEndRegexp.lastIndex = this.lastIndex;
-      this.lastIndex = undefined;
-      const match = partialAtEndRegexp.exec(chunk);
-      if (match) {
-        this.partialChunk = chunk.substring(match.index);
-        chunk = chunk.substring(0, match.index);
-      }
-      controller.enqueue(chunk);
+  transform(chunk, controller) {
+    chunk = this.partialChunk + chunk;
+    this.partialChunk = "";
+    // lastIndex is the index of the first character after the last substitution.
+    this.lastIndex = 0;
+    chunk = chunk.replace(/\{\{([a-zA-Z0-9_-]+)\}\}/g, this.replaceTag.bind(this));
+    // Regular expression for an incomplete template at the end of a string.
+    const partialAtEndRegexp = /\{(\{([a-zA-Z0-9_-]+(\})?)?)?$/g;
+    // Avoid looking at any characters that have already been substituted.
+    partialAtEndRegexp.lastIndex = this.lastIndex;
+    this.lastIndex = undefined;
+    const match = partialAtEndRegexp.exec(chunk);
+    if (match) {
+      this.partialChunk = chunk.substring(match.index);
+      chunk = chunk.substring(0, match.index);
     }
+    controller.enqueue(chunk);
+  }
 
-    flush(controller) {
-      if (this.partialChunk.length > 0) {
-        controller.enqueue(this.partialChunk);
-      }
+  flush(controller) {
+    if (this.partialChunk.length > 0) {
+      controller.enqueue(this.partialChunk);
     }
+  }
 
-    replaceTag(match, p1, offset) {
-      let replacement = this.substitutions[p1];
-      if (replacement === undefined) {
-        replacement = "";
-      }
-      this.lastIndex = offset + replacement.length;
-      return replacement;
+  replaceTag(match, p1, offset) {
+    let replacement = this.substitutions[p1];
+    if (replacement === undefined) {
+      replacement = "";
     }
+    this.lastIndex = offset + replacement.length;
+    return replacement;
   }
-</code></pre>
+}
+
 
-In this case we define the transformer to be passed to the {{TransformStream}} constructor as a class. This is
-useful when there is instance data to track.
+In this case we define the [=transformer=] to be passed to the {{TransformStream}} constructor as a
+class. This is useful when there is instance data to track.
 
 The class would be used in code like:
 
-

-  const data = { userName, displayName, icon, date };
-  const ts = new TransformStream(new LipFuzzTransformer(data));
-
-  fetchEvent.respondWith(
-    fetch(fetchEvent.request.url).then(response => {
-      const transformedBody = response.body
-        // Decode the binary-encoded response to string
-        .pipeThrough(new TextDecoderStream())
-        // Apply the LipFuzzTransformer
-        .pipeThrough(ts)
-        // Encode the transformed string
-        .pipeThrough(new TextEncoderStream());
-      return new Response(transformedBody);
-    })
-  );
-
- -
For simplicity, LipFuzzTransformer performs unescaped text substitutions. In real -applications, a template system that performs context-aware escaping is good practice for security and robustness.
+ +const data = { userName, displayName, icon, date }; +const ts = new TransformStream(new LipFuzzTransformer(data)); + +fetchEvent.respondWith( + fetch(fetchEvent.request.url).then(response => { + const transformedBody = response.body + // Decode the binary-encoded response to string + .pipeThrough(new TextDecoderStream()) + // Apply the LipFuzzTransformer + .pipeThrough(ts) + // Encode the transformed string + .pipeThrough(new TextEncoderStream()); + return new Response(transformedBody); + }) +); + + +

For simplicity, LipFuzzTransformer performs unescaped text +substitutions. In real applications, a template system that performs context-aware escaping is good +practice for security and robustness.

A transform stream created from a sync mapper function

-The following function allows creating new {{TransformStream}} instances from synchronous "mapper" functions, of the -type you would normally pass to {{Array.prototype/map|Array.prototype.map}}. It demonstrates that the API is concise -even for trivial transforms. +The following function allows creating new {{TransformStream}} instances from synchronous "mapper" +functions, of the type you would normally pass to {{Array.prototype/map|Array.prototype.map}}. It +demonstrates that the API is concise even for trivial transforms. -

-  function mapperTransformStream(mapperFunction) {
-    return new TransformStream({
-      transform(chunk, controller) {
-        controller.enqueue(mapperFunction(chunk));
-      }
-    });
-  }
-
+ +function mapperTransformStream(mapperFunction) { + return new TransformStream({ + transform(chunk, controller) { + controller.enqueue(mapperFunction(chunk)); + } + }); +} + This function can then be used to create a {{TransformStream}} that uppercases all its inputs: -

-  const ts = mapperTransformStream(chunk => chunk.toUpperCase());
-  const writer = ts.writable.getWriter();
-  const reader = ts.readable.getReader();
+
+const ts = mapperTransformStream(chunk => chunk.toUpperCase());
+const writer = ts.writable.getWriter();
+const reader = ts.readable.getReader();
 
-  writer.write("No need to shout");
+writer.write("No need to shout");
 
-  // Logs "NO NEED TO SHOUT":
-  reader.read().then(({ value }) => console.log(value));
-</code></pre>
+// Logs "NO NEED TO SHOUT":
+reader.read().then(({ value }) => console.log(value));
+
 
-Although a synchronous transform never causes backpressure itself, it will only transform chunks as long as there is no
-backpressure, so resources will not be wasted.
+Although a synchronous transform never causes backpressure itself, it will only transform chunks as
+long as there is no backpressure, so resources will not be wasted.
 
 Exceptions error the stream in a natural way:
 
-

-  const ts = mapperTransformStream(chunk => JSON.parse(chunk));
-  const writer = ts.writable.getWriter();
-  const reader = ts.readable.getReader();
+
+const ts = mapperTransformStream(chunk => JSON.parse(chunk));
+const writer = ts.writable.getWriter();
+const reader = ts.readable.getReader();
 
-  writer.write("[1, ");
-
-  // Logs a SyntaxError, twice:
-  reader.read().catch(e => console.error(e));
-  writer.write("{}").catch(e => console.error(e));
-</code></pre>
-
-<h2 id="conventions" class="no-num">Conventions</h2>
-
-This specification depends on the Infra Standard. [[!INFRA]]
+writer.write("[1, ");
 
-This specification uses algorithm conventions very similar to those of [[!ECMASCRIPT]], whose rules should be used to
-interpret it (apart from the exceptions enumerated below). In particular, the objects specified here should be treated
-as <a href="https://tc39.github.io/ecma262/#sec-ecmascript-standard-built-in-objects">built-in objects</a>. For example,
-their <code>name</code> and <code>length</code> properties are derived as described by that specification, as are the
-default property descriptor values and the treatment of missing, <emu-val>undefined</emu-val>, or surplus arguments.
-
-We also depart from the [[!ECMASCRIPT]] conventions in the following ways, mostly for brevity. It is hoped (and vaguely
-planned) that the conventions of ECMAScript itself will evolve in these ways.
-
-<ul>
-  <li> We prefix section headings with <code>new</code> to indicate they are defining constructors; when doing so, we
-  assume that NewTarget will be checked before the algorithm starts.
-  <li> We use the default argument notation <code>= {}</code> in a couple of cases, meaning that before the algorithm
-    starts, <emu-val>undefined</emu-val> (including the implicit <emu-val>undefined</emu-val> when no argument is
-    provided) is instead treated as a new object created as if by ObjectCreate(%ObjectPrototype%). (This object may then
-    be destructured, if combined with the below destructuring convention.)
-  <li> We use destructuring notation in function and method declarations, and assume that <a
-    abstract-op>DestructuringAssignmentEvaluation</a> was performed appropriately before the algorithm starts.
-  <li> We use "<emu-val>this</emu-val>" instead of "<emu-val>this</emu-val> value".
-</ul>
-
-It's also worth noting that, as in [[!ECMASCRIPT]], all numbers are represented as double-precision floating point
-values, and all arithmetic operations performed on them must be done in the standard way for such values.
+// Logs a SyntaxError, twice:
+reader.read().catch(e => console.error(e));
+writer.write("{}").catch(e => console.error(e));
+
 
 

Acknowledgments

diff --git a/local-watch.js b/local-watch.js deleted file mode 100644 index 8c8924789..000000000 --- a/local-watch.js +++ /dev/null @@ -1,83 +0,0 @@ -'use strict'; -const fs = require('fs'); -const childProcess = require('child_process'); -const promiseDebounce = require('promise-debounce'); -const emuAlgify = require('emu-algify'); - -const INPUT = 'index.bs'; - -let fsWatcher; - -const build = promiseDebounce(() => { - log('Building...'); - - try { - childProcess.execSync( - `bikeshed spec ${INPUT} index.html.postbs --md-Text-Macro="SNAPSHOT-LINK "`, - { encoding: 'utf-8', stdio: 'inherit' } - ); - log('(bikeshed done)'); - } catch (e) { - error('Error executing bikeshed:\n'); - console.error(e.stdout); - } - - const input = fs.readFileSync('index.html.postbs', { encoding: 'utf-8' }); - fs.unlinkSync('index.html.postbs'); - - return emuAlgify(input, { throwingIndicators: true }) - .then(output => { - fs.writeFileSync('index.html.new', output); - fs.renameSync('index.html.new', 'index.html'); - log('Build complete'); - }) - .catch(err => { - error('Error executing ecmarkupify:\n'); - console.error(err); - } - ); -}); - -function onChange(eventType, filename) { - log(`Saw ${eventType} event with filename '${filename}'`); - // Restart the watch in case the file has been renamed or deleted. This fixes an issue where the file stopped being - // watched when a different branch was checked out. - tryWatch(); -} - -// If index.bs exists, start watching it and run a build. Otherwise retry with a truncated exponential delay until it -// starts to exist. -function tryWatch(delay) { - if (fsWatcher !== undefined) { - fsWatcher.close(); - fsWatcher = undefined; - } - try { - fsWatcher = fs.watch(INPUT, onChange); - build(); - } catch (e) { - if (e.code === 'ENOENT') { - log(`${INPUT} not there right now. Waiting a bit.`); - if (delay === undefined) { - delay = 100; - } - delay *= 2; - if (delay > 20000) { - delay = 20000; - } - setTimeout(tryWatch, delay, delay); - } else { - throw e; - } - } -} - -tryWatch(); - -function log(s) { - console.log(`[${(new Date()).toISOString()}] ${s}`); -} - -function error(s) { - console.error(`[${(new Date()).toISOString()}] ${s}`); -} diff --git a/package.json b/package.json deleted file mode 100644 index b49f7348f..000000000 --- a/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "private": true, - "description": "Build process supporting stuff for WHATWG Streams Standard document", - "scripts": { - "local-watch": "node ./local-watch.js" - }, - "devDependencies": { - "emu-algify": "^2.2.0", - "promise-debounce": "^1.0.0" - } -} From ae7ce5123fdbdaab8fbf9ee8289d3cc457c90856 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Tue, 14 Apr 2020 11:19:45 -0400 Subject: [PATCH 02/30] Callback context does matter, actually --- index.bs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.bs b/index.bs index 5edff586a..7b90c1f7a 100644 --- a/index.bs +++ b/index.bs @@ -5286,8 +5286,8 @@ value given in the constructor. 1. Perform ! [$SetFunctionName$](|F|, "`size`"). 1. Perform ! [$SetFunctionLength$](|F|, 1). 1. Set |globalObject|'s [=byte length queuing strategy size function=] to a {{Function}} that - represents a reference to |F|. (The [=callback context=] does not matter, since this function is - never [=invoked=].) + represents a reference to |F|, with [=callback context=] equal to |globalObject|'s [=relevant + settings object=].

This design is somewhat historical. It is motivated by the desire to ensure that {{ByteLengthQueuingStrategy/size}} is a function, not a method, i.e. it does not check its @@ -5408,8 +5408,8 @@ value given in the constructor. 1. Perform ! [$SetFunctionName$](|F|, "`size`"). 1. Perform ! [$SetFunctionLength$](|F|, 0). 1. Set |globalObject|'s [=count queuing strategy size function=] to a {{Function}} that represents - a reference to |F|. (The [=callback context=] does not matter, since this function is never - [=invoked=].) + a reference to |F|, with [=callback context=] equal to |globalObject|'s [=relevant settings + object=].

This design is somewhat historical. It is motivated by the desire to ensure that {{CountQueuingStrategy/size}} is a function, not a method, i.e. it does not check its From e44ec8e7e06c51441273dd6c452b10c6513562b9 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Tue, 14 Apr 2020 11:21:33 -0400 Subject: [PATCH 03/30] Align with spec-factory Follows https://github.com/whatwg/spec-factory/pull/7 --- .editorconfig | 9 ++++++--- .pr-preview.json | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.editorconfig b/.editorconfig index f0b203f28..47ebb2529 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ root = true end_of_line = lf insert_final_newline = true charset = utf-8 -indent_size = 1 +indent_size = 2 indent_style = space trim_trailing_whitespace = true max_line_length = 100 @@ -12,8 +12,11 @@ max_line_length = 100 [Makefile] indent_style = tab -[.travis.yml] -indent_size = 2 +[*.bs] +indent_size = 1 + +[*.py] +indent_size = 4 [*.js] max_line_length = 120 diff --git a/.pr-preview.json b/.pr-preview.json index 03fdd6beb..3b9efb150 100644 --- a/.pr-preview.json +++ b/.pr-preview.json @@ -1,9 +1,9 @@ { - "src_file": "index.bs", - "type": "bikeshed", - "params": { - "force": 1, - "md-status": "LS-PR", - "md-Text-Macro": "PR-NUMBER {{ pull_request.number }}" - } + "src_file": "index.bs", + "type": "bikeshed", + "params": { + "force": 1, + "md-status": "LS-PR", + "md-Text-Macro": "PR-NUMBER {{ pull_request.number }}" + } } From dbf913f0483deff9f0915fcb89b484c8787c5b84 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Tue, 14 Apr 2020 11:56:30 -0400 Subject: [PATCH 04/30] Make first arguments optional again --- index.bs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/index.bs b/index.bs index 7b90c1f7a..4943746bf 100644 --- a/index.bs +++ b/index.bs @@ -453,7 +453,7 @@ The Web IDL definition for the {{ReadableStream}} class is given as follows:

[Exposed=(Window,Worker,Worklet)] interface ReadableStream { - constructor(object underlyingSource, optional QueuingStrategy strategy = {}); + constructor(optional object underlyingSource, optional QueuingStrategy strategy = {}); readonly attribute boolean locked; @@ -745,6 +745,7 @@ option. If {{UnderlyingSource/type}} is set to undefined (including via omission The <dfn id="rs-constructor" constructor for="ReadableStream">ReadableStream(|underlyingSource|, |strategy|)</dfn> constructor steps are: + 1. If |underlyingSource| is missing, set it to null. 1. Let |underlyingSourceDict| be |underlyingSource|, [=converted to an IDL value=] of type {{UnderlyingSource}}. <p class="note">We cannot declare the |underlyingSource| argument as having the @@ -3173,7 +3174,7 @@ The Web IDL definition for the {{WritableStream}} class is given as follows: <xmp class="idl"> [Exposed=(Window,Worker,Worklet)] interface WritableStream { - constructor(object underlyingSink, optional QueuingStrategy strategy = {}); + constructor(optional object underlyingSink, optional QueuingStrategy strategy = {}); readonly attribute boolean locked; @@ -3404,6 +3405,7 @@ as seen for example in [[#example-ws-no-backpressure]]. The <dfn id="ws-constructor" constructor for="WritableStream">WritableStream(|underlyingSink|, |strategy|)</dfn> constructor steps are: + 1. If |underlyingSink| is missing, set it to null. 1. Let |underlyingSinkDict| be |underlyingSinkDict|, [=converted to an IDL value=] of type {{UnderlyingSink}}. <p class="note">We cannot declare the |underlyingSink| argument as having the {{UnderlyingSink}} @@ -4554,7 +4556,7 @@ The Web IDL definition for the {{TransformStream}} class is given as follows: <xmp class="idl"> [Exposed=(Window,Worker,Worklet)] interface TransformStream { - constructor(object transformer, + constructor(optional object transformer, optional QueuingStrategy writableStrategy = {}, optional QueuingStrategy readableStrategy = {}); @@ -4686,11 +4688,15 @@ side=], or to terminate or error the stream. <h4 id="ts-prototype">Constructor and properties</h4> <dl class="domintro non-normative"> - <dt><code><var ignore>stream</var> = new {{TransformStream/constructor(transformer, writableStrategy, readableStrategy)|TransformStream}}(<var ignore>transformer</var>[, <var ignore>writableStrategy</var>[, <var ignore>readableStrategy</var>]])</code> + <dt><code><var ignore>stream</var> = new {{TransformStream/constructor(transformer, writableStrategy, readableStrategy)|TransformStream}}([<var ignore>transformer</var>[, <var ignore>writableStrategy</var>[, <var ignore>readableStrategy</var>]]])</code> <dd> <p>Creates a new {{TransformStream}} wrapping the provided [=transformer=]. See [[#transformer-api]] for more details on the <var ignore>transformer</var> argument. + <p>If no <var ignore>transformer</var> argument is supplied, then the result will be an [=identity + transform stream=]. See <a href="#example-transform-identity">this example</a> for some cases + where that can be useful. + <p>The <var ignore>writableStrategy</var> and <var ignore>readableStrategy</var> arguments are the [=queuing strategy=] objects for the [=writable side|writable=] and [=readable side|readable=] sides respectively. These are used in the construction of the {{WritableStream}} @@ -4712,6 +4718,7 @@ side=], or to terminate or error the stream. The <dfn id="ts-constructor" constructor for="TransformStream">TransformStream(|transformer|, |writableStrategy|, |readableStrategy|)</dfn> constructor steps are: + 1. If |transformer| is missing, set it to null. 1. Let |transformerDict| be |transformer|, [=converted to an IDL value=] of type {{Transformer}}. <p class="note">We cannot declare the |transformer| argument as having the {{Transformer}} type directly, because doing so would lose the reference to the original object. We need to retain From 2304a01427b165d62d7c75935d9f4067db143576 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Tue, 14 Apr 2020 17:51:27 -0400 Subject: [PATCH 05/30] Start of ref impl updates - Update dependencies - Update WPT submodule - Re-do the queuing strategies in the new style. (Tests not updated yet.) --- reference-implementation/.eslintignore | 1 + reference-implementation/.gitignore | 1 + reference-implementation/compile-idl.js | 21 +++++++++++++++ .../lib/ByteLengthQueuingStrategy-impl.js | 26 +++++++++++++++++++ .../lib/ByteLengthQueuingStrategy.webidl | 7 +++++ .../lib/CountQueuingStrategy-impl.js | 26 +++++++++++++++++++ .../lib/CountQueuingStrategy.webidl | 7 +++++ .../lib/QueuingStrategyInit.webidl | 3 +++ .../lib/byte-length-queuing-strategy.js | 12 --------- .../lib/count-queuing-strategy.js | 12 --------- reference-implementation/lib/index.js | 8 +++--- .../lib/readable-stream.js | 1 - reference-implementation/package.json | 16 +++++++----- reference-implementation/web-platform-tests | 2 +- 14 files changed, 107 insertions(+), 36 deletions(-) create mode 100644 reference-implementation/compile-idl.js create mode 100644 reference-implementation/lib/ByteLengthQueuingStrategy-impl.js create mode 100644 reference-implementation/lib/ByteLengthQueuingStrategy.webidl create mode 100644 reference-implementation/lib/CountQueuingStrategy-impl.js create mode 100644 reference-implementation/lib/CountQueuingStrategy.webidl create mode 100644 reference-implementation/lib/QueuingStrategyInit.webidl delete mode 100644 reference-implementation/lib/byte-length-queuing-strategy.js delete mode 100644 reference-implementation/lib/count-queuing-strategy.js diff --git a/reference-implementation/.eslintignore b/reference-implementation/.eslintignore index 46e585972..d9729fadd 100644 --- a/reference-implementation/.eslintignore +++ b/reference-implementation/.eslintignore @@ -1,2 +1,3 @@ web-platform-tests/ coverage/ +generated/ diff --git a/reference-implementation/.gitignore b/reference-implementation/.gitignore index 2386693ff..c84396803 100644 --- a/reference-implementation/.gitignore +++ b/reference-implementation/.gitignore @@ -4,3 +4,4 @@ package-lock.json .nyc_output/ coverage/ +generated/ diff --git a/reference-implementation/compile-idl.js b/reference-implementation/compile-idl.js new file mode 100644 index 000000000..e6db4f4f4 --- /dev/null +++ b/reference-implementation/compile-idl.js @@ -0,0 +1,21 @@ +'use strict'; +/* eslint-disable no-console, no-process-exit */ +const { mkdirSync } = require('fs'); +const path = require('path'); +const Transformer = require('webidl2js'); + +const input = path.resolve(__dirname, './lib'); +const output = path.resolve(__dirname, './generated'); + +mkdirSync(output, { recursive: true }); + +const transformer = new Transformer({ + implSuffix: '-impl' +}); + +transformer.addSource(input, input); +transformer.generate(output) + .catch(err => { + console.error(err.stack); + process.exit(1); + }); diff --git a/reference-implementation/lib/ByteLengthQueuingStrategy-impl.js b/reference-implementation/lib/ByteLengthQueuingStrategy-impl.js new file mode 100644 index 000000000..ebc23938c --- /dev/null +++ b/reference-implementation/lib/ByteLengthQueuingStrategy-impl.js @@ -0,0 +1,26 @@ +'use strict'; + +exports.implementation = class ByteLengthQueuingStrategyImpl { + constructor(globalObject, [{ highWaterMark }]) { + this._globalObject = globalObject; + this.highWaterMark = highWaterMark; + } + + get size() { + initializeSizeFunction(this._globalObject); + return sizeFunctionWeakMap.get(this._globalObject); + } +}; + +const sizeFunctionWeakMap = new WeakMap(); +function initializeSizeFunction(globalObject) { + if (sizeFunctionWeakMap.has(globalObject)) { + return; + } + + // We need to set the 'name' property: + // eslint-disable-next-line prefer-arrow-callback + sizeFunctionWeakMap.set(globalObject, function size(chunk) { + return chunk.byteLength; + }); +} diff --git a/reference-implementation/lib/ByteLengthQueuingStrategy.webidl b/reference-implementation/lib/ByteLengthQueuingStrategy.webidl new file mode 100644 index 000000000..e712dd3a9 --- /dev/null +++ b/reference-implementation/lib/ByteLengthQueuingStrategy.webidl @@ -0,0 +1,7 @@ +[Exposed=(Window,Worker,Worklet)] +interface ByteLengthQueuingStrategy { + constructor(QueuingStrategyInit init); + + attribute unrestricted double highWaterMark; + readonly attribute Function size; +}; diff --git a/reference-implementation/lib/CountQueuingStrategy-impl.js b/reference-implementation/lib/CountQueuingStrategy-impl.js new file mode 100644 index 000000000..719433e95 --- /dev/null +++ b/reference-implementation/lib/CountQueuingStrategy-impl.js @@ -0,0 +1,26 @@ +'use strict'; + +exports.implementation = class CountQueuingStrategyImpl { + constructor(globalObject, [{ highWaterMark }]) { + this._globalObject = globalObject; + this.highWaterMark = highWaterMark; + } + + get size() { + initializeSizeFunction(this._globalObject); + return sizeFunctionWeakMap.get(this._globalObject); + } +}; + +const sizeFunctionWeakMap = new WeakMap(); +function initializeSizeFunction(globalObject) { + if (sizeFunctionWeakMap.has(globalObject)) { + return; + } + + // We need to set the 'name' property: + // eslint-disable-next-line prefer-arrow-callback + sizeFunctionWeakMap.set(globalObject, function size() { + return 1; + }); +} diff --git a/reference-implementation/lib/CountQueuingStrategy.webidl b/reference-implementation/lib/CountQueuingStrategy.webidl new file mode 100644 index 000000000..a5ba8ec5a --- /dev/null +++ b/reference-implementation/lib/CountQueuingStrategy.webidl @@ -0,0 +1,7 @@ +[Exposed=(Window,Worker,Worklet)] +interface CountQueuingStrategy { + constructor(QueuingStrategyInit init); + + attribute unrestricted double highWaterMark; + readonly attribute Function size; +}; diff --git a/reference-implementation/lib/QueuingStrategyInit.webidl b/reference-implementation/lib/QueuingStrategyInit.webidl new file mode 100644 index 000000000..17b795cd9 --- /dev/null +++ b/reference-implementation/lib/QueuingStrategyInit.webidl @@ -0,0 +1,3 @@ +dictionary QueuingStrategyInit { + required unrestricted double highWaterMark; +}; diff --git a/reference-implementation/lib/byte-length-queuing-strategy.js b/reference-implementation/lib/byte-length-queuing-strategy.js deleted file mode 100644 index 97f312e12..000000000 --- a/reference-implementation/lib/byte-length-queuing-strategy.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; -const { createDataProperty } = require('./helpers.js'); - -module.exports = class ByteLengthQueuingStrategy { - constructor({ highWaterMark }) { - createDataProperty(this, 'highWaterMark', highWaterMark); - } - - size(chunk) { - return chunk.byteLength; - } -}; diff --git a/reference-implementation/lib/count-queuing-strategy.js b/reference-implementation/lib/count-queuing-strategy.js deleted file mode 100644 index 0893a95ae..000000000 --- a/reference-implementation/lib/count-queuing-strategy.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; -const { createDataProperty } = require('./helpers.js'); - -module.exports = class CountQueuingStrategy { - constructor({ highWaterMark }) { - createDataProperty(this, 'highWaterMark', highWaterMark); - } - - size() { - return 1; - } -}; diff --git a/reference-implementation/lib/index.js b/reference-implementation/lib/index.js index d42621689..a5492de0e 100644 --- a/reference-implementation/lib/index.js +++ b/reference-implementation/lib/index.js @@ -5,12 +5,14 @@ const { ReadableStream } = require('./readable-stream.js'); const { WritableStream } = require('./writable-stream.js'); const { TransformStream } = require('./transform-stream.js'); -const ByteLengthQueuingStrategy = require('./byte-length-queuing-strategy.js'); -const CountQueuingStrategy = require('./count-queuing-strategy.js'); +const ByteLengthQueuingStrategy = require('../generated/ByteLengthQueuingStrategy.js'); +const CountQueuingStrategy = require('../generated/CountQueuingStrategy.js'); window.ReadableStream = ReadableStream; window.WritableStream = WritableStream; window.TransformStream = TransformStream; -window.ByteLengthQueuingStrategy = ByteLengthQueuingStrategy; window.CountQueuingStrategy = CountQueuingStrategy; window.gc = gc; + +ByteLengthQueuingStrategy.install(window); +CountQueuingStrategy.install(window); diff --git a/reference-implementation/lib/readable-stream.js b/reference-implementation/lib/readable-stream.js index 371dd6166..c44dfaa72 100644 --- a/reference-implementation/lib/readable-stream.js +++ b/reference-implementation/lib/readable-stream.js @@ -1,5 +1,4 @@ 'use strict'; -/* global AbortSignal:false */ const assert = require('assert'); const { ArrayBufferCopy, CreateAlgorithmFromUnderlyingMethod, IsFiniteNonNegativeNumber, InvokeOrNoop, diff --git a/reference-implementation/package.json b/reference-implementation/package.json index 731e48759..a7f59923a 100644 --- a/reference-implementation/package.json +++ b/reference-implementation/package.json @@ -2,22 +2,24 @@ "private": true, "description": "Reference implementation and tests for the WHATWG Streams Standard", "scripts": { + "pretest": "node compile-idl.js", "test": "npm run lint && npm run wpt", - "wpt": "node --expose_gc run-web-platform-tests.js", + "wpt": "npm run pretest && node --expose_gc run-web-platform-tests.js", "sync-wpt": "git submodule update --init", - "lint": "eslint \"**/*.js\"", + "lint": "eslint .", "coverage": "nyc --reporter=lcov npm test && opener coverage/lcov-report/index.html" }, "author": "Domenic Denicola <d@domenic.me> (https://domenic.me/)", "license": "(CC0-1.0 OR MIT)", "devDependencies": { - "browserify": "^16.2.3", - "debug": "^4.1.0", - "eslint": "^5.12.1", + "browserify": "^16.5.1", + "debug": "^4.1.1", + "eslint": "^6.8.0", "minimatch": "^3.0.4", - "nyc": "^13.0.1", + "nyc": "^15.0.1", "opener": "^1.5.1", - "wpt-runner": "^2.7.0" + "webidl2js": "^15.1.0", + "wpt-runner": "^3.0.1" }, "nyc": { "include": [ diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index 0ba0c4c07..55d456135 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit 0ba0c4c07c8d2c23efdcc84dfc9043a3fdccbf19 +Subproject commit 55d4561356b8dea58d251755431ec9646f521d98 From 504f25d1ff82e95ac2a34f229d69f7d3c40d8502 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 15 Apr 2020 16:27:13 -0400 Subject: [PATCH 06/30] Writable stream updates Passes most tests except blocked on https://github.com/jsdom/webidl2js/pull/123#issuecomment-614278671 and https://github.com/jsdom/webidl2js/issues/79 --- Makefile | 13 +- index.bs | 24 +- reference-implementation/.eslintrc.json | 12 +- reference-implementation/compile-idl.js | 3 +- .../lib/QueuingStrategy.webidl | 6 + .../lib/UnderlyingSink.webidl | 12 + .../lib/WritableStream-impl.js | 56 ++ .../lib/WritableStream.webidl | 10 + .../WritableStreamDefaultController-impl.js | 28 + .../WritableStreamDefaultController.webidl | 4 + .../lib/WritableStreamDefaultWriter-impl.js | 74 ++ .../lib/WritableStreamDefaultWriter.webidl | 13 + .../lib/abstract-ops/internal-methods.js | 4 + .../lib/abstract-ops/miscellaneous.js | 17 + .../{ => abstract-ops}/queue-with-sizes.js | 8 +- .../lib/abstract-ops/queuing-strategy.js | 26 + .../writable-streams.js} | 796 +++++------------- reference-implementation/lib/helpers.js | 244 +----- reference-implementation/lib/index.js | 20 +- reference-implementation/lib/utils.js | 13 - .../lib/webidl-helpers.js | 202 +++++ 21 files changed, 714 insertions(+), 871 deletions(-) create mode 100644 reference-implementation/lib/QueuingStrategy.webidl create mode 100644 reference-implementation/lib/UnderlyingSink.webidl create mode 100644 reference-implementation/lib/WritableStream-impl.js create mode 100644 reference-implementation/lib/WritableStream.webidl create mode 100644 reference-implementation/lib/WritableStreamDefaultController-impl.js create mode 100644 reference-implementation/lib/WritableStreamDefaultController.webidl create mode 100644 reference-implementation/lib/WritableStreamDefaultWriter-impl.js create mode 100644 reference-implementation/lib/WritableStreamDefaultWriter.webidl create mode 100644 reference-implementation/lib/abstract-ops/internal-methods.js create mode 100644 reference-implementation/lib/abstract-ops/miscellaneous.js rename reference-implementation/lib/{ => abstract-ops}/queue-with-sizes.js (83%) create mode 100644 reference-implementation/lib/abstract-ops/queuing-strategy.js rename reference-implementation/lib/{writable-stream.js => abstract-ops/writable-streams.js} (53%) delete mode 100644 reference-implementation/lib/utils.js create mode 100644 reference-implementation/lib/webidl-helpers.js diff --git a/Makefile b/Makefile index 4443a981f..60a5dd76a 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,18 @@ SHELL=/bin/bash -o pipefail .PHONY: local remote deploy review remote: index.bs - curl https://api.csswg.org/bikeshed/ -f -F file=@index.bs > index.html -F md-Text-Macro="COMMIT-SHA LOCAL COPY" + @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \ + --output index.html \ + --write-out "%{http_code}" \ + --header "Accept: text/plain, text/html" \ + -F die-on=warning \ + -F md-Text-Macro="COMMIT-SHA LOCAL COPY" \ + -F file=@index.bs) && \ + [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \ + echo ""; cat index.html; echo ""; \ + rm -f index.html; \ + exit 22 \ + ); local: index.bs bikeshed spec index.bs index.html --md-Text-Macro="COMMIT-SHA LOCAL COPY" diff --git a/index.bs b/index.bs index 4943746bf..302dbbf57 100644 --- a/index.bs +++ b/index.bs @@ -3756,7 +3756,7 @@ as such the counterpart internal methods are used polymorphically. id="ws-default-controller-private-error">\[[ErrorSteps]]()</dfn> implements the [$WritableStreamController/[[ErrorSteps]]$] contract. It performs the following steps: - 1. Perform ! ResetQueue([=this=]). + 1. Perform ! [$ResetQueue$]([=this=]). </div> <h3 id="ws-all-abstract-ops">Abstract operations</h3> @@ -4392,6 +4392,15 @@ The following abstract operations support the implementation of the [$WritableStreamDefaultControllerError$](|controller|, |error|). </div> +<div algorithm> + <dfn abstract-op lt="WritableStreamDefaultControllerGetBackpressure" + id="writable-stream-default-controller-get-backpressure">WritableStreamDefaultControllerGetBackpressure(|controller|)</dfn> + performs the following steps: + + 1. Let |desiredSize| be ! [$WritableStreamDefaultControllerGetDesiredSize$](|controller|). + 1. Return true if |desiredSize| ≤ 0, or false otherwise. +</div> + <div algorithm> <dfn abstract-op lt="WritableStreamDefaultControllerGetChunkSize" id="writable-stream-default-controller-get-chunk-size">WritableStreamDefaultControllerGetChunkSize(|controller|, @@ -4406,15 +4415,6 @@ The following abstract operations support the implementation of the 1. Return |returnValue|.\[[Value]]. </div> -<div algorithm> - <dfn abstract-op lt="WritableStreamDefaultControllerGetBackpressure" - id="writable-stream-default-controller-get-backpressure">WritableStreamDefaultControllerGetBackpressure(|controller|)</dfn> - performs the following steps: - - 1. Let |desiredSize| be ! [$WritableStreamDefaultControllerGetDesiredSize$](|controller|). - 1. Return true if |desiredSize| ≤ 0, or false otherwise. -</div> - <div algorithm> <dfn abstract-op lt="WritableStreamDefaultControllerGetDesiredSize" id="writable-stream-default-controller-get-desired-size">WritableStreamDefaultControllerGetDesiredSize(|controller|)</dfn> @@ -5549,8 +5549,8 @@ trillions of chunks are enqueued.) following steps: 1. Assert: |container| has \[[queue]] and \[[queueTotalSize]] internal slots. - 1. If ! [$IsNonNegativeNumber$](|v|) is false, throw a {{RangeError}} exception. - 1. If |v| is +∞, throw a {{RangeError}} exception. + 1. If ! [$IsNonNegativeNumber$](|size|) is false, throw a {{RangeError}} exception. + 1. If |size| is +∞, throw a {{RangeError}} exception. 1. [=list/Append=] Record {\[[value]]: |value|, \[[size]]: |size|} to |container|.\[[queue]]. 1. Set |container|.\[[queueTotalSize]] to |container|.\[[queueTotalSize]] + |size|. </div> diff --git a/reference-implementation/.eslintrc.json b/reference-implementation/.eslintrc.json index f3486a456..b74da9cf5 100644 --- a/reference-implementation/.eslintrc.json +++ b/reference-implementation/.eslintrc.json @@ -3,19 +3,15 @@ "env": { "node": true, "browser": true, - "es6": true + "es2020": true }, "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2020 }, "globals": { - "ReadableStream": false, - "WritableStream": false, - "TransformStream": false, - "ByteLengthQueuingStrategy": false, - "CountQueuingStrategy": false, "GCController": false, - "gc": false + "gc": false, + "globalThis": false }, "rules": { // Possible errors diff --git a/reference-implementation/compile-idl.js b/reference-implementation/compile-idl.js index e6db4f4f4..f5c81d480 100644 --- a/reference-implementation/compile-idl.js +++ b/reference-implementation/compile-idl.js @@ -10,7 +10,8 @@ const output = path.resolve(__dirname, './generated'); mkdirSync(output, { recursive: true }); const transformer = new Transformer({ - implSuffix: '-impl' + implSuffix: '-impl', + suppressErrors: true // until https://github.com/jsdom/webidl2js/pull/123 lands }); transformer.addSource(input, input); diff --git a/reference-implementation/lib/QueuingStrategy.webidl b/reference-implementation/lib/QueuingStrategy.webidl new file mode 100644 index 000000000..aac307292 --- /dev/null +++ b/reference-implementation/lib/QueuingStrategy.webidl @@ -0,0 +1,6 @@ +dictionary QueuingStrategy { + unrestricted double highWaterMark; + QueuingStrategySize size; +}; + +callback QueuingStrategySize = unrestricted double (optional any chunk); diff --git a/reference-implementation/lib/UnderlyingSink.webidl b/reference-implementation/lib/UnderlyingSink.webidl new file mode 100644 index 000000000..5b6b1d271 --- /dev/null +++ b/reference-implementation/lib/UnderlyingSink.webidl @@ -0,0 +1,12 @@ +dictionary UnderlyingSink { + WritableStreamStartCallback start; + WritableStreamWriteCallback write; + WritableStreamCloseCallback close; + WritableStreamAbortCallback abort; + any type; +}; + +callback WritableStreamStartCallback = Promise<void> (WritableStreamDefaultController controller); +callback WritableStreamWriteCallback = Promise<void> (WritableStreamDefaultController controller, optional any chunk); +callback WritableStreamCloseCallback = Promise<void> (); +callback WritableStreamAbortCallback = Promise<void> (optional any reason); diff --git a/reference-implementation/lib/WritableStream-impl.js b/reference-implementation/lib/WritableStream-impl.js new file mode 100644 index 000000000..b12b00055 --- /dev/null +++ b/reference-implementation/lib/WritableStream-impl.js @@ -0,0 +1,56 @@ +'use strict'; +const { promiseRejectedWith } = require('./webidl-helpers.js'); + +const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/queuing-strategy.js'); +const aos = require('./abstract-ops/writable-streams.js'); + +const UnderlyingSink = require('../generated/UnderlyingSink.js'); + +exports.implementation = class WritableStreamImpl { + constructor(globalObject, [underlyingSink, strategy]) { + if (underlyingSink === undefined) { + underlyingSink = null; + } + const underlyingSinkDict = UnderlyingSink.convert(underlyingSink); + if ('type' in underlyingSinkDict) { + throw new RangeError('Invalid type is specified'); + } + + aos.InitializeWritableStream(this); + + const sizeAlgorithm = ExtractSizeAlgorithm(strategy); + const highWaterMark = ExtractHighWaterMark(strategy, 1); + + aos.SetUpWritableStreamDefaultControllerFromUnderlyingSink( + this, underlyingSink, underlyingSinkDict, highWaterMark, sizeAlgorithm + ); + } + + get locked() { + return aos.IsWritableStreamLocked(this); + } + + abort(reason) { + if (aos.IsWritableStreamLocked(this) === true) { + return promiseRejectedWith(new TypeError('Cannot abort a stream that already has a writer')); + } + + return aos.WritableStreamAbort(this, reason); + } + + close() { + if (aos.IsWritableStreamLocked(this) === true) { + return promiseRejectedWith(new TypeError('Cannot close a stream that already has a writer')); + } + + if (aos.WritableStreamCloseQueuedOrInFlight(this) === true) { + return promiseRejectedWith(new TypeError('Cannot close an already-closing stream')); + } + + return aos.WritableStreamClose(this); + } + + getWriter() { + return aos.AcquireWritableStreamDefaultWriter(this); + } +}; diff --git a/reference-implementation/lib/WritableStream.webidl b/reference-implementation/lib/WritableStream.webidl new file mode 100644 index 000000000..1cea8a8b0 --- /dev/null +++ b/reference-implementation/lib/WritableStream.webidl @@ -0,0 +1,10 @@ +[Exposed=(Window,Worker,Worklet)] +interface WritableStream { + constructor(optional object underlyingSink, optional QueuingStrategy strategy = {}); + + readonly attribute boolean locked; + + Promise<void> abort(optional any reason); + Promise<void> close(); + WritableStreamDefaultWriter getWriter(); +}; diff --git a/reference-implementation/lib/WritableStreamDefaultController-impl.js b/reference-implementation/lib/WritableStreamDefaultController-impl.js new file mode 100644 index 000000000..3b9d8c3df --- /dev/null +++ b/reference-implementation/lib/WritableStreamDefaultController-impl.js @@ -0,0 +1,28 @@ +'use strict'; +const aos = require('./abstract-ops/writable-streams.js'); +const { AbortSteps, ErrorSteps } = require('./abstract-ops/internal-methods.js'); +const { ResetQueue } = require('./abstract-ops/queue-with-sizes.js'); + +exports.implementation = class WritableStreamDefaultControllerImpl { + error(e) { + const state = this._controlledWritableStream._state; + + if (state !== 'writable') { + // The stream is closed, errored or will be soon. The sink can't do anything useful if it gets an error here, so + // just treat it as a no-op. + return; + } + + aos.WritableStreamDefaultControllerError(this, e); + } + + [AbortSteps](reason) { + const result = this._abortAlgorithm(reason); + aos.WritableStreamDefaultControllerClearAlgorithms(this); + return result; + } + + [ErrorSteps]() { + ResetQueue(this); + } +}; diff --git a/reference-implementation/lib/WritableStreamDefaultController.webidl b/reference-implementation/lib/WritableStreamDefaultController.webidl new file mode 100644 index 000000000..b127a7ab4 --- /dev/null +++ b/reference-implementation/lib/WritableStreamDefaultController.webidl @@ -0,0 +1,4 @@ +[Exposed=(Window,Worker,Worklet)] +interface WritableStreamDefaultController { + void error(optional any e); +}; diff --git a/reference-implementation/lib/WritableStreamDefaultWriter-impl.js b/reference-implementation/lib/WritableStreamDefaultWriter-impl.js new file mode 100644 index 000000000..9d64ef0af --- /dev/null +++ b/reference-implementation/lib/WritableStreamDefaultWriter-impl.js @@ -0,0 +1,74 @@ +'use strict'; +const assert = require('assert'); + +const { promiseRejectedWith } = require('./webidl-helpers.js'); + +const aos = require('./abstract-ops/writable-streams.js'); + +exports.implementation = class WritableStreamDefaultWriterImpl { + constructor(globalObject, [stream]) { + aos.SetUpWritableStreamDefaultWriter(this, stream); + } + + get closed() { + return this._closedPromise; + } + + get desiredSize() { + if (this._ownerWritableStream === undefined) { + throw defaultWriterLockException('desiredSize'); + } + + return aos.WritableStreamDefaultWriterGetDesiredSize(this); + } + + get ready() { + return this._readyPromise; + } + + abort(reason) { + if (this._ownerWritableStream === undefined) { + return promiseRejectedWith(defaultWriterLockException('abort')); + } + + return aos.WritableStreamDefaultWriterAbort(this, reason); + } + + close() { + const stream = this._ownerWritableStream; + + if (stream === undefined) { + return promiseRejectedWith(defaultWriterLockException('close')); + } + + if (aos.WritableStreamCloseQueuedOrInFlight(stream) === true) { + return promiseRejectedWith(new TypeError('Cannot close an already-closing stream')); + } + + return aos.WritableStreamDefaultWriterClose(this); + } + + releaseLock() { + const stream = this._ownerWritableStream; + + if (stream === undefined) { + return; + } + + assert(stream._writer !== undefined); + + aos.WritableStreamDefaultWriterRelease(this); + } + + write(chunk) { + if (this._ownerWritableStream === undefined) { + return promiseRejectedWith(defaultWriterLockException('write to')); + } + + return aos.WritableStreamDefaultWriterWrite(this, chunk); + } +}; + +function defaultWriterLockException(name) { + return new TypeError('Cannot ' + name + ' a stream using a released writer'); +} diff --git a/reference-implementation/lib/WritableStreamDefaultWriter.webidl b/reference-implementation/lib/WritableStreamDefaultWriter.webidl new file mode 100644 index 000000000..42e0e1661 --- /dev/null +++ b/reference-implementation/lib/WritableStreamDefaultWriter.webidl @@ -0,0 +1,13 @@ +[Exposed=(Window,Worker,Worklet)] +interface WritableStreamDefaultWriter { + constructor(WritableStream stream); + + readonly attribute Promise<void> closed; + readonly attribute unrestricted double? desiredSize; + readonly attribute Promise<void> ready; + + Promise<void> abort(optional any reason); + Promise<void> close(); + void releaseLock(); + Promise<void> write(optional any chunk); +}; diff --git a/reference-implementation/lib/abstract-ops/internal-methods.js b/reference-implementation/lib/abstract-ops/internal-methods.js new file mode 100644 index 000000000..cd69a76df --- /dev/null +++ b/reference-implementation/lib/abstract-ops/internal-methods.js @@ -0,0 +1,4 @@ +'use strict'; + +exports.AbortSteps = Symbol('[[AbortSteps]]'); +exports.ErrorSteps = Symbol('[[ErrorSteps]]'); diff --git a/reference-implementation/lib/abstract-ops/miscellaneous.js b/reference-implementation/lib/abstract-ops/miscellaneous.js new file mode 100644 index 000000000..9f5ff5449 --- /dev/null +++ b/reference-implementation/lib/abstract-ops/miscellaneous.js @@ -0,0 +1,17 @@ +'use strict'; + +exports.IsNonNegativeNumber = v => { + if (typeof v !== 'number') { + return false; + } + + if (Number.isNaN(v)) { + return false; + } + + if (v < 0) { + return false; + } + + return true; +}; diff --git a/reference-implementation/lib/queue-with-sizes.js b/reference-implementation/lib/abstract-ops/queue-with-sizes.js similarity index 83% rename from reference-implementation/lib/queue-with-sizes.js rename to reference-implementation/lib/abstract-ops/queue-with-sizes.js index 328438fd8..22086caa5 100644 --- a/reference-implementation/lib/queue-with-sizes.js +++ b/reference-implementation/lib/abstract-ops/queue-with-sizes.js @@ -1,6 +1,6 @@ 'use strict'; const assert = require('assert'); -const { IsFiniteNonNegativeNumber } = require('./helpers.js'); +const { IsNonNegativeNumber } = require('./miscellaneous.js'); exports.DequeueValue = container => { assert('_queue' in container && '_queueTotalSize' in container); @@ -18,8 +18,10 @@ exports.DequeueValue = container => { exports.EnqueueValueWithSize = (container, value, size) => { assert('_queue' in container && '_queueTotalSize' in container); - size = Number(size); - if (!IsFiniteNonNegativeNumber(size)) { + if (!IsNonNegativeNumber(size)) { + throw new RangeError('Size must be a finite, non-NaN, non-negative number.'); + } + if (size === Infinity) { throw new RangeError('Size must be a finite, non-NaN, non-negative number.'); } diff --git a/reference-implementation/lib/abstract-ops/queuing-strategy.js b/reference-implementation/lib/abstract-ops/queuing-strategy.js new file mode 100644 index 000000000..9dd10d2c1 --- /dev/null +++ b/reference-implementation/lib/abstract-ops/queuing-strategy.js @@ -0,0 +1,26 @@ +'use strict'; +const { invoke } = require('../webidl-helpers.js'); + +exports.ExtractHighWaterMark = (strategy, defaultHWM) => { + if (!('highWaterMark' in strategy)) { + return defaultHWM; + } + + const { highWaterMark } = strategy; + if (Number.isNaN(highWaterMark) || highWaterMark < 0) { + throw new RangeError('Invalid highWaterMark'); + } + + return highWaterMark; +}; + +exports.ExtractSizeAlgorithm = strategy => { + if (!('size' in strategy)) { + return () => 1; + } + + return chunk => { + // TODO: manual number conversion won't be necessary when https://github.com/jsdom/webidl2js/pull/123 lands. + return Number(invoke(strategy.size, [chunk])); + }; +}; diff --git a/reference-implementation/lib/writable-stream.js b/reference-implementation/lib/abstract-ops/writable-streams.js similarity index 53% rename from reference-implementation/lib/writable-stream.js rename to reference-implementation/lib/abstract-ops/writable-streams.js index c221c9e1c..faa5de021 100644 --- a/reference-implementation/lib/writable-stream.js +++ b/reference-implementation/lib/abstract-ops/writable-streams.js @@ -1,115 +1,51 @@ 'use strict'; const assert = require('assert'); - -// Calls to verbose() are purely for debugging the reference implementation and tests. They are not part of the standard -// and do not appear in the standard text. const verbose = require('debug')('streams:writable-stream:verbose'); -const { CreateAlgorithmFromUnderlyingMethod, InvokeOrNoop, ValidateAndNormalizeHighWaterMark, IsNonNegativeNumber, - MakeSizeAlgorithmFromSizeFunction, typeIsObject, newPromise, promiseResolvedWith, promiseRejectedWith, - uponPromise, setPromiseIsHandledToTrue } = require('./helpers.js'); +const { webidlNew, promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, + rejectPromise, uponPromise, setPromiseIsHandledToTrue, stateIsPending } = require('../webidl-helpers.js'); +const { IsNonNegativeNumber } = require('./miscellaneous.js'); const { DequeueValue, EnqueueValueWithSize, PeekQueueValue, ResetQueue } = require('./queue-with-sizes.js'); +const { AbortSteps, ErrorSteps } = require('./internal-methods.js'); -const AbortSteps = Symbol('[[AbortSteps]]'); -const ErrorSteps = Symbol('[[ErrorSteps]]'); - -class WritableStream { - constructor(underlyingSink = {}, strategy = {}) { - InitializeWritableStream(this); - - const size = strategy.size; - let highWaterMark = strategy.highWaterMark; - - const type = underlyingSink.type; - - if (type !== undefined) { - throw new RangeError('Invalid type is specified'); - } - - const sizeAlgorithm = MakeSizeAlgorithmFromSizeFunction(size); - if (highWaterMark === undefined) { - highWaterMark = 1; - } - highWaterMark = ValidateAndNormalizeHighWaterMark(highWaterMark); - - SetUpWritableStreamDefaultControllerFromUnderlyingSink(this, underlyingSink, highWaterMark, sizeAlgorithm); - } - - get locked() { - if (IsWritableStream(this) === false) { - throw streamBrandCheckException('locked'); - } - - return IsWritableStreamLocked(this); - } - - abort(reason) { - if (IsWritableStream(this) === false) { - return promiseRejectedWith(streamBrandCheckException('abort')); - } - - if (IsWritableStreamLocked(this) === true) { - return promiseRejectedWith(new TypeError('Cannot abort a stream that already has a writer')); - } - - return WritableStreamAbort(this, reason); - } - - close() { - if (IsWritableStream(this) === false) { - return promiseRejectedWith(streamBrandCheckException('close')); - } - - if (IsWritableStreamLocked(this) === true) { - return promiseRejectedWith(new TypeError('Cannot close a stream that already has a writer')); - } - - if (WritableStreamCloseQueuedOrInFlight(this) === true) { - return promiseRejectedWith(new TypeError('Cannot close an already-closing stream')); - } - - return WritableStreamClose(this); - } - - getWriter() { - if (IsWritableStream(this) === false) { - throw streamBrandCheckException('getWriter'); - } - - return AcquireWritableStreamDefaultWriter(this); - } -} +const WritableStreamImpl = require('../WritableStream-impl.js'); +const WritableStreamDefaultControllerImpl = require('../WritableStreamDefaultController-impl.js'); +const WritableStreamDefaultWriterImpl = require('../WritableStreamDefaultWriter-impl.js'); -module.exports = { +Object.assign(exports, { AcquireWritableStreamDefaultWriter, - CreateWritableStream, - IsWritableStream, + InitializeWritableStream, IsWritableStreamLocked, - WritableStream, + SetUpWritableStreamDefaultControllerFromUnderlyingSink, + SetUpWritableStreamDefaultWriter, WritableStreamAbort, WritableStreamClose, - WritableStreamDefaultControllerErrorIfNeeded, - WritableStreamDefaultWriterCloseWithErrorPropagation, + WritableStreamCloseQueuedOrInFlight, + WritableStreamDefaultControllerClearAlgorithms, + WritableStreamDefaultControllerError, + WritableStreamDefaultWriterAbort, + WritableStreamDefaultWriterClose, + WritableStreamDefaultWriterGetDesiredSize, WritableStreamDefaultWriterRelease, - WritableStreamDefaultWriterWrite, - WritableStreamCloseQueuedOrInFlight -}; + WritableStreamDefaultWriterWrite +}); -// Abstract operations for the WritableStream. +// Working with writable streams function AcquireWritableStreamDefaultWriter(stream) { - return new WritableStreamDefaultWriter(stream); + const writer = webidlNew(globalThis, 'WritableStreamDefaultWriter', WritableStreamDefaultWriterImpl); + SetUpWritableStreamDefaultWriter(writer, stream); + return writer; } -// Throws if and only if startAlgorithm throws. function CreateWritableStream(startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark = 1, sizeAlgorithm = () => 1) { assert(IsNonNegativeNumber(highWaterMark) === true); - const stream = Object.create(WritableStream.prototype); + const stream = webidlNew(globalThis, 'WritableStream', WritableStreamImpl); InitializeWritableStream(stream); - const controller = Object.create(WritableStreamDefaultController.prototype); + const controller = webidlNew(globalThis, 'WritableStreamDefaultController', WritableStreamDefaultControllerImpl); SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm); @@ -129,10 +65,6 @@ function InitializeWritableStream(stream) { // variable to validate the caller. stream._writableStreamController = undefined; - // This queue is placed here instead of the writer class in order to allow for passing a writer to the next data - // producer without waiting for the queued writes to finish. - stream._writeRequests = []; - // Write requests are removed from _writeRequests when write() is called on the underlying sink. This prevents // them from being erroneously rejected on error. If a write() call is in-flight, the request is stored here. stream._inFlightWriteRequest = undefined; @@ -148,24 +80,16 @@ function InitializeWritableStream(stream) { // The promise that was returned from writer.abort(). This may also be fulfilled after the writer has detached. stream._pendingAbortRequest = undefined; + // This queue is placed here instead of the writer class in order to allow for passing a writer to the next data + // producer without waiting for the queued writes to finish. + stream._writeRequests = []; + // The backpressure signal set by the controller. stream._backpressure = false; } -function IsWritableStream(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_writableStreamController')) { - return false; - } - - return true; -} - function IsWritableStreamLocked(stream) { - assert(IsWritableStream(stream) === true); + assert(stream instanceof WritableStreamImpl.implementation); if (stream._writer === undefined) { return false; @@ -180,7 +104,7 @@ function WritableStreamAbort(stream, reason) { return promiseResolvedWith(undefined); } if (stream._pendingAbortRequest !== undefined) { - return stream._pendingAbortRequest._promise; + return stream._pendingAbortRequest.promise; } assert(state === 'writable' || state === 'erroring'); @@ -192,15 +116,8 @@ function WritableStreamAbort(stream, reason) { reason = undefined; } - const promise = newPromise((resolve, reject) => { - stream._pendingAbortRequest = { - _resolve: resolve, - _reject: reject, - _reason: reason, - _wasAlreadyErroring: wasAlreadyErroring - }; - }); - stream._pendingAbortRequest._promise = promise; + const promise = newPromise(); + stream._pendingAbortRequest = { promise, reason, wasAlreadyErroring }; if (wasAlreadyErroring === false) { WritableStreamStartErroring(stream, reason); @@ -209,6 +126,42 @@ function WritableStreamAbort(stream, reason) { return promise; } +function SetUpWritableStreamDefaultWriter(writer, stream) { + if (IsWritableStreamLocked(stream) === true) { + throw new TypeError('This stream has already been locked for exclusive writing by another writer'); + } + + writer._ownerWritableStream = stream; + stream._writer = writer; + + const state = stream._state; + + if (state === 'writable') { + if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._backpressure === true) { + writer._readyPromise = newPromise(); + } else { + writer._readyPromise = promiseResolvedWith(undefined); + } + + writer._closedPromise = newPromise(); + } else if (state === 'erroring') { + writer._readyPromise = promiseRejectedWith(stream._storedError); + setPromiseIsHandledToTrue(writer._readyPromise); + writer._closedPromise = newPromise(); + } else if (state === 'closed') { + writer._readyPromise = promiseResolvedWith(undefined); + writer._closedPromise = promiseResolvedWith(undefined); + } else { + assert(state === 'errored'); + + const storedError = stream._storedError; + writer._readyPromise = promiseRejectedWith(storedError); + setPromiseIsHandledToTrue(writer._readyPromise); + writer._closedPromise = promiseRejectedWith(storedError); + setPromiseIsHandledToTrue(writer._closedPromise); + } +} + function WritableStreamClose(stream) { const state = stream._state; if (state === 'closed' || state === 'errored') { @@ -219,18 +172,12 @@ function WritableStreamClose(stream) { assert(state === 'writable' || state === 'erroring'); assert(WritableStreamCloseQueuedOrInFlight(stream) === false); - const promise = newPromise((resolve, reject) => { - const closeRequest = { - _resolve: resolve, - _reject: reject - }; - - stream._closeRequest = closeRequest; - }); + const promise = newPromise(); + stream._closeRequest = promise; const writer = stream._writer; if (writer !== undefined && stream._backpressure === true && state === 'writable') { - defaultWriterReadyPromiseResolve(writer); + resolvePromise(writer._readyPromise, undefined); } WritableStreamDefaultControllerClose(stream._writableStreamController); @@ -238,22 +185,23 @@ function WritableStreamClose(stream) { return promise; } -// WritableStream API exposed for controllers. +// Interfacing with controllers function WritableStreamAddWriteRequest(stream) { assert(IsWritableStreamLocked(stream) === true); assert(stream._state === 'writable'); - const promise = newPromise((resolve, reject) => { - const writeRequest = { - _resolve: resolve, - _reject: reject - }; + const promise = newPromise(); + stream._writeRequests.push(promise); + return promise; +} - stream._writeRequests.push(writeRequest); - }); +function WritableStreamCloseQueuedOrInFlight(stream) { + if (stream._closeRequest === undefined && stream._inFlightCloseRequest === undefined) { + return false; + } - return promise; + return true; } function WritableStreamDealWithRejection(stream, error) { @@ -269,26 +217,6 @@ function WritableStreamDealWithRejection(stream, error) { WritableStreamFinishErroring(stream); } -function WritableStreamStartErroring(stream, reason) { - verbose('WritableStreamStartErroring(stream, %o)', reason); - assert(stream._storedError === undefined); - assert(stream._state === 'writable'); - - const controller = stream._writableStreamController; - assert(controller !== undefined); - - stream._state = 'erroring'; - stream._storedError = reason; - const writer = stream._writer; - if (writer !== undefined) { - WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); - } - - if (WritableStreamHasOperationMarkedInFlight(stream) === false && controller._started === true) { - WritableStreamFinishErroring(stream); - } -} - function WritableStreamFinishErroring(stream) { verbose('WritableStreamFinishErroring()'); assert(stream._state === 'erroring'); @@ -298,7 +226,7 @@ function WritableStreamFinishErroring(stream) { const storedError = stream._storedError; for (const writeRequest of stream._writeRequests) { - writeRequest._reject(storedError); + rejectPromise(writeRequest, storedError); } stream._writeRequests = []; @@ -310,44 +238,28 @@ function WritableStreamFinishErroring(stream) { const abortRequest = stream._pendingAbortRequest; stream._pendingAbortRequest = undefined; - if (abortRequest._wasAlreadyErroring === true) { - abortRequest._reject(storedError); + if (abortRequest.wasAlreadyErroring === true) { + rejectPromise(abortRequest.promise, storedError); WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); return; } - const promise = stream._writableStreamController[AbortSteps](abortRequest._reason); + const promise = stream._writableStreamController[AbortSteps](abortRequest.reason); uponPromise( promise, () => { - abortRequest._resolve(); + resolvePromise(abortRequest.promise); WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); }, reason => { - abortRequest._reject(reason); + rejectPromise(abortRequest.promise, reason); WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream); }); } -function WritableStreamFinishInFlightWrite(stream) { - assert(stream._inFlightWriteRequest !== undefined); - stream._inFlightWriteRequest._resolve(undefined); - stream._inFlightWriteRequest = undefined; -} - -function WritableStreamFinishInFlightWriteWithError(stream, error) { - assert(stream._inFlightWriteRequest !== undefined); - stream._inFlightWriteRequest._reject(error); - stream._inFlightWriteRequest = undefined; - - assert(stream._state === 'writable' || stream._state === 'erroring'); - - WritableStreamDealWithRejection(stream, error); -} - function WritableStreamFinishInFlightClose(stream) { assert(stream._inFlightCloseRequest !== undefined); - stream._inFlightCloseRequest._resolve(undefined); + resolvePromise(stream._inFlightCloseRequest, undefined); stream._inFlightCloseRequest = undefined; const state = stream._state; @@ -358,7 +270,7 @@ function WritableStreamFinishInFlightClose(stream) { // The error was too late to do anything, so it is ignored. stream._storedError = undefined; if (stream._pendingAbortRequest !== undefined) { - stream._pendingAbortRequest._resolve(); + resolvePromise(stream._pendingAbortRequest.promise, undefined); stream._pendingAbortRequest = undefined; } } @@ -367,7 +279,7 @@ function WritableStreamFinishInFlightClose(stream) { const writer = stream._writer; if (writer !== undefined) { - defaultWriterClosedPromiseResolve(writer); + resolvePromise(writer._closedPromise, undefined); } assert(stream._pendingAbortRequest === undefined); @@ -376,26 +288,33 @@ function WritableStreamFinishInFlightClose(stream) { function WritableStreamFinishInFlightCloseWithError(stream, error) { assert(stream._inFlightCloseRequest !== undefined); - stream._inFlightCloseRequest._reject(error); + rejectPromise(stream._inFlightCloseRequest, error); stream._inFlightCloseRequest = undefined; assert(stream._state === 'writable' || stream._state === 'erroring'); // Never execute sink abort() after sink close(). if (stream._pendingAbortRequest !== undefined) { - stream._pendingAbortRequest._reject(error); + rejectPromise(stream._pendingAbortRequest.promise, error); stream._pendingAbortRequest = undefined; } WritableStreamDealWithRejection(stream, error); } -// TODO(ricea): Fix alphabetical order. -function WritableStreamCloseQueuedOrInFlight(stream) { - if (stream._closeRequest === undefined && stream._inFlightCloseRequest === undefined) { - return false; - } +function WritableStreamFinishInFlightWrite(stream) { + assert(stream._inFlightWriteRequest !== undefined); + resolvePromise(stream._inFlightWriteRequest, undefined); + stream._inFlightWriteRequest = undefined; +} - return true; +function WritableStreamFinishInFlightWriteWithError(stream, error) { + assert(stream._inFlightWriteRequest !== undefined); + rejectPromise(stream._inFlightWriteRequest, error); + stream._inFlightWriteRequest = undefined; + + assert(stream._state === 'writable' || stream._state === 'erroring'); + + WritableStreamDealWithRejection(stream, error); } function WritableStreamHasOperationMarkedInFlight(stream) { @@ -427,16 +346,36 @@ function WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { if (stream._closeRequest !== undefined) { assert(stream._inFlightCloseRequest === undefined); - stream._closeRequest._reject(stream._storedError); + rejectPromise(stream._closeRequest, stream._storedError); stream._closeRequest = undefined; } const writer = stream._writer; if (writer !== undefined) { - defaultWriterClosedPromiseReject(writer, stream._storedError); + rejectPromise(writer._closedPromise, stream._storedError); setPromiseIsHandledToTrue(writer._closedPromise); } } +function WritableStreamStartErroring(stream, reason) { + verbose('WritableStreamStartErroring(stream, %o)', reason); + assert(stream._storedError === undefined); + assert(stream._state === 'writable'); + + const controller = stream._writableStreamController; + assert(controller !== undefined); + + stream._state = 'erroring'; + stream._storedError = reason; + const writer = stream._writer; + if (writer !== undefined) { + WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); + } + + if (WritableStreamHasOperationMarkedInFlight(stream) === false && controller._started === true) { + WritableStreamFinishErroring(stream); + } +} + function WritableStreamUpdateBackpressure(stream, backpressure) { assert(stream._state === 'writable'); assert(WritableStreamCloseQueuedOrInFlight(stream) === false); @@ -444,159 +383,19 @@ function WritableStreamUpdateBackpressure(stream, backpressure) { const writer = stream._writer; if (writer !== undefined && backpressure !== stream._backpressure) { if (backpressure === true) { - defaultWriterReadyPromiseReset(writer); + writer._readyPromise = newPromise(); } else { assert(backpressure === false); - defaultWriterReadyPromiseResolve(writer); + resolvePromise(writer._readyPromise, undefined); } } stream._backpressure = backpressure; } -class WritableStreamDefaultWriter { - constructor(stream) { - if (IsWritableStream(stream) === false) { - throw new TypeError('WritableStreamDefaultWriter can only be constructed with a WritableStream instance'); - } - if (IsWritableStreamLocked(stream) === true) { - throw new TypeError('This stream has already been locked for exclusive writing by another writer'); - } - - this._ownerWritableStream = stream; - stream._writer = this; - - const state = stream._state; - - if (state === 'writable') { - if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._backpressure === true) { - defaultWriterReadyPromiseInitialize(this); - } else { - defaultWriterReadyPromiseInitializeAsResolved(this); - } - - defaultWriterClosedPromiseInitialize(this); - } else if (state === 'erroring') { - defaultWriterReadyPromiseInitializeAsRejected(this, stream._storedError); - setPromiseIsHandledToTrue(this._readyPromise); - defaultWriterClosedPromiseInitialize(this); - } else if (state === 'closed') { - defaultWriterReadyPromiseInitializeAsResolved(this); - defaultWriterClosedPromiseInitializeAsResolved(this); - } else { - assert(state === 'errored'); - - const storedError = stream._storedError; - defaultWriterReadyPromiseInitializeAsRejected(this, storedError); - setPromiseIsHandledToTrue(this._readyPromise); - defaultWriterClosedPromiseInitializeAsRejected(this, storedError); - setPromiseIsHandledToTrue(this._closedPromise); - } - } - - get closed() { - if (IsWritableStreamDefaultWriter(this) === false) { - return promiseRejectedWith(defaultWriterBrandCheckException('closed')); - } - - return this._closedPromise; - } - - get desiredSize() { - if (IsWritableStreamDefaultWriter(this) === false) { - throw defaultWriterBrandCheckException('desiredSize'); - } - - if (this._ownerWritableStream === undefined) { - throw defaultWriterLockException('desiredSize'); - } - - return WritableStreamDefaultWriterGetDesiredSize(this); - } - - get ready() { - if (IsWritableStreamDefaultWriter(this) === false) { - return promiseRejectedWith(defaultWriterBrandCheckException('ready')); - } - - return this._readyPromise; - } - - abort(reason) { - if (IsWritableStreamDefaultWriter(this) === false) { - return promiseRejectedWith(defaultWriterBrandCheckException('abort')); - } - - if (this._ownerWritableStream === undefined) { - return promiseRejectedWith(defaultWriterLockException('abort')); - } - - return WritableStreamDefaultWriterAbort(this, reason); - } - - close() { - if (IsWritableStreamDefaultWriter(this) === false) { - return promiseRejectedWith(defaultWriterBrandCheckException('close')); - } - - const stream = this._ownerWritableStream; - - if (stream === undefined) { - return promiseRejectedWith(defaultWriterLockException('close')); - } - - if (WritableStreamCloseQueuedOrInFlight(stream) === true) { - return promiseRejectedWith(new TypeError('Cannot close an already-closing stream')); - } - - return WritableStreamDefaultWriterClose(this); - } - - releaseLock() { - if (IsWritableStreamDefaultWriter(this) === false) { - throw defaultWriterBrandCheckException('releaseLock'); - } - - const stream = this._ownerWritableStream; - - if (stream === undefined) { - return; - } - - assert(stream._writer !== undefined); - - WritableStreamDefaultWriterRelease(this); - } - - write(chunk) { - if (IsWritableStreamDefaultWriter(this) === false) { - return promiseRejectedWith(defaultWriterBrandCheckException('write')); - } - - if (this._ownerWritableStream === undefined) { - return promiseRejectedWith(defaultWriterLockException('write to')); - } - - return WritableStreamDefaultWriterWrite(this, chunk); - } -} - -// Abstract operations for the WritableStreamDefaultWriter. - -function IsWritableStreamDefaultWriter(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_ownerWritableStream')) { - return false; - } - - return true; -} +// Writers -// A client of WritableStreamDefaultWriter may use these functions directly to bypass state check. function WritableStreamDefaultWriterAbort(writer, reason) { const stream = writer._ownerWritableStream; @@ -634,20 +433,20 @@ function WritableStreamDefaultWriterCloseWithErrorPropagation(writer) { } function WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) { - if (writer._closedPromiseState === 'pending') { - defaultWriterClosedPromiseReject(writer, error); + if (stateIsPending(writer._closedPromise)) { + rejectPromise(writer._closedPromise, error); } else { - defaultWriterClosedPromiseResetToRejected(writer, error); + writer._closedPromise = promiseRejectedWith(error); } setPromiseIsHandledToTrue(writer._closedPromise); } function WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) { verbose('WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, %o)', error); - if (writer._readyPromiseState === 'pending') { - defaultWriterReadyPromiseReject(writer, error); + if (stateIsPending(writer._readyPromise)) { + rejectPromise(writer._readyPromise, error); } else { - defaultWriterReadyPromiseResetToRejected(writer, error); + writer._readyPromise = promiseRejectedWith(error); } setPromiseIsHandledToTrue(writer._readyPromise); } @@ -695,7 +494,7 @@ function WritableStreamDefaultWriterWrite(writer, chunk) { const chunkSize = WritableStreamDefaultControllerGetChunkSize(controller, chunk); if (stream !== writer._ownerWritableStream) { - return promiseRejectedWith(defaultWriterLockException('write to')); + return promiseRejectedWith(new TypeError('Cannot write to a stream using a released writer')); } const state = stream._state; @@ -718,54 +517,11 @@ function WritableStreamDefaultWriterWrite(writer, chunk) { return promise; } -class WritableStreamDefaultController { - constructor() { - throw new TypeError('WritableStreamDefaultController cannot be constructed explicitly'); - } - - error(e) { - if (IsWritableStreamDefaultController(this) === false) { - throw new TypeError( - 'WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController'); - } - const state = this._controlledWritableStream._state; - if (state !== 'writable') { - // The stream is closed, errored or will be soon. The sink can't do anything useful if it gets an error here, so - // just treat it as a no-op. - return; - } - - WritableStreamDefaultControllerError(this, e); - } - - [AbortSteps](reason) { - const result = this._abortAlgorithm(reason); - WritableStreamDefaultControllerClearAlgorithms(this); - return result; - } - - [ErrorSteps]() { - ResetQueue(this); - } -} - -// Abstract operations implementing interface required by the WritableStream. - -function IsWritableStreamDefaultController(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_controlledWritableStream')) { - return false; - } - - return true; -} +// Default controllers function SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm) { - assert(IsWritableStream(stream) === true); + assert(stream instanceof WritableStreamImpl.implementation); assert(stream._writableStreamController === undefined); controller._controlledWritableStream = stream; @@ -805,70 +561,34 @@ function SetUpWritableStreamDefaultController(stream, controller, startAlgorithm ); } -function SetUpWritableStreamDefaultControllerFromUnderlyingSink(stream, underlyingSink, highWaterMark, sizeAlgorithm) { +function SetUpWritableStreamDefaultControllerFromUnderlyingSink(stream, underlyingSink, underlyingSinkDict, + highWaterMark, sizeAlgorithm) { assert(underlyingSink !== undefined); - const controller = Object.create(WritableStreamDefaultController.prototype); - - function startAlgorithm() { - return InvokeOrNoop(underlyingSink, 'start', [controller]); - } - - const writeAlgorithm = CreateAlgorithmFromUnderlyingMethod(underlyingSink, 'write', 1, [controller]); - const closeAlgorithm = CreateAlgorithmFromUnderlyingMethod(underlyingSink, 'close', 0, []); - const abortAlgorithm = CreateAlgorithmFromUnderlyingMethod(underlyingSink, 'abort', 1, []); + const controller = webidlNew(globalThis, 'WritableStreamDefaultController', WritableStreamDefaultControllerImpl); - SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, - abortAlgorithm, highWaterMark, sizeAlgorithm); -} + let startAlgorithm = () => undefined; + let writeAlgorithm = () => promiseResolvedWith(undefined); + let closeAlgorithm = () => promiseResolvedWith(undefined); + let abortAlgorithm = () => promiseResolvedWith(undefined); -// ClearAlgorithms may be called twice. Erroring the same stream in multiple ways will often result in redundant calls. -function WritableStreamDefaultControllerClearAlgorithms(controller) { - controller._writeAlgorithm = undefined; - controller._closeAlgorithm = undefined; - controller._abortAlgorithm = undefined; - controller._strategySizeAlgorithm = undefined; -} - -function WritableStreamDefaultControllerClose(controller) { - EnqueueValueWithSize(controller, 'close', 0); - WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); -} - -function WritableStreamDefaultControllerGetChunkSize(controller, chunk) { - try { - return controller._strategySizeAlgorithm(chunk); - } catch (chunkSizeE) { - WritableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE); - return 1; + if ('start' in underlyingSinkDict) { + startAlgorithm = () => invoke(underlyingSinkDict.start, [controller], underlyingSink); } -} - -function WritableStreamDefaultControllerGetDesiredSize(controller) { - return controller._strategyHWM - controller._queueTotalSize; -} - -function WritableStreamDefaultControllerWrite(controller, chunk, chunkSize) { - const writeRecord = { chunk }; - - try { - EnqueueValueWithSize(controller, writeRecord, chunkSize); - } catch (enqueueE) { - WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueE); - return; + if ('write' in underlyingSinkDict) { + writeAlgorithm = chunk => promiseInvoke(underlyingSinkDict.write, [chunk, controller], underlyingSink); } - - const stream = controller._controlledWritableStream; - if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._state === 'writable') { - const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); - WritableStreamUpdateBackpressure(stream, backpressure); + if ('close' in underlyingSinkDict) { + closeAlgorithm = () => promiseInvoke(underlyingSinkDict.close, [], underlyingSink); + } + if ('abort' in underlyingSinkDict) { + abortAlgorithm = reason => promiseInvoke(underlyingSinkDict.abort, [reason], underlyingSink); } - WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, + abortAlgorithm, highWaterMark, sizeAlgorithm); } -// Abstract operations for the WritableStreamDefaultController. - function WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller) { verbose('WritableStreamDefaultControllerAdvanceQueueIfNeeded()'); const stream = controller._controlledWritableStream; @@ -900,12 +620,51 @@ function WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller) { } } +function WritableStreamDefaultControllerClearAlgorithms(controller) { + controller._writeAlgorithm = undefined; + controller._closeAlgorithm = undefined; + controller._abortAlgorithm = undefined; + controller._strategySizeAlgorithm = undefined; +} + +function WritableStreamDefaultControllerClose(controller) { + EnqueueValueWithSize(controller, 'close', 0); + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); +} + +function WritableStreamDefaultControllerError(controller, error) { + const stream = controller._controlledWritableStream; + + assert(stream._state === 'writable'); + + WritableStreamDefaultControllerClearAlgorithms(controller); + WritableStreamStartErroring(stream, error); +} + function WritableStreamDefaultControllerErrorIfNeeded(controller, error) { if (controller._controlledWritableStream._state === 'writable') { WritableStreamDefaultControllerError(controller, error); } } +function WritableStreamDefaultControllerGetBackpressure(controller) { + const desiredSize = WritableStreamDefaultControllerGetDesiredSize(controller); + return desiredSize <= 0; +} + +function WritableStreamDefaultControllerGetChunkSize(controller, chunk) { + try { + return controller._strategySizeAlgorithm(chunk); + } catch (chunkSizeE) { + WritableStreamDefaultControllerErrorIfNeeded(controller, chunkSizeE); + return 1; + } +} + +function WritableStreamDefaultControllerGetDesiredSize(controller) { + return controller._strategyHWM - controller._queueTotalSize; +} + function WritableStreamDefaultControllerProcessClose(controller) { const stream = controller._controlledWritableStream; @@ -959,156 +718,21 @@ function WritableStreamDefaultControllerProcessWrite(controller, chunk) { ); } -function WritableStreamDefaultControllerGetBackpressure(controller) { - const desiredSize = WritableStreamDefaultControllerGetDesiredSize(controller); - return desiredSize <= 0; -} +function WritableStreamDefaultControllerWrite(controller, chunk, chunkSize) { + const writeRecord = { chunk }; -// A client of WritableStreamDefaultController may use these functions directly to bypass state check. + try { + EnqueueValueWithSize(controller, writeRecord, chunkSize); + } catch (enqueueE) { + WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueE); + return; + } -function WritableStreamDefaultControllerError(controller, error) { const stream = controller._controlledWritableStream; + if (WritableStreamCloseQueuedOrInFlight(stream) === false && stream._state === 'writable') { + const backpressure = WritableStreamDefaultControllerGetBackpressure(controller); + WritableStreamUpdateBackpressure(stream, backpressure); + } - assert(stream._state === 'writable'); - - WritableStreamDefaultControllerClearAlgorithms(controller); - WritableStreamStartErroring(stream, error); -} - -// Helper functions for the WritableStream. - -function streamBrandCheckException(name) { - return new TypeError(`WritableStream.prototype.${name} can only be used on a WritableStream`); -} - -// Helper functions for the WritableStreamDefaultWriter. - -function defaultWriterBrandCheckException(name) { - return new TypeError( - `WritableStreamDefaultWriter.prototype.${name} can only be used on a WritableStreamDefaultWriter`); -} - -function defaultWriterLockException(name) { - return new TypeError('Cannot ' + name + ' a stream using a released writer'); -} - -function defaultWriterClosedPromiseInitialize(writer) { - writer._closedPromise = newPromise((resolve, reject) => { - writer._closedPromise_resolve = resolve; - writer._closedPromise_reject = reject; - writer._closedPromiseState = 'pending'; - }); -} - -function defaultWriterClosedPromiseInitializeAsRejected(writer, reason) { - writer._closedPromise = promiseRejectedWith(reason); - writer._closedPromise_resolve = undefined; - writer._closedPromise_reject = undefined; - writer._closedPromiseState = 'rejected'; -} - -function defaultWriterClosedPromiseInitializeAsResolved(writer) { - writer._closedPromise = promiseResolvedWith(undefined); - writer._closedPromise_resolve = undefined; - writer._closedPromise_reject = undefined; - writer._closedPromiseState = 'resolved'; -} - -function defaultWriterClosedPromiseReject(writer, reason) { - assert(writer._closedPromise_resolve !== undefined); - assert(writer._closedPromise_reject !== undefined); - assert(writer._closedPromiseState === 'pending'); - - writer._closedPromise_reject(reason); - writer._closedPromise_resolve = undefined; - writer._closedPromise_reject = undefined; - writer._closedPromiseState = 'rejected'; -} - -function defaultWriterClosedPromiseResetToRejected(writer, reason) { - assert(writer._closedPromise_resolve === undefined); - assert(writer._closedPromise_reject === undefined); - assert(writer._closedPromiseState !== 'pending'); - - writer._closedPromise = promiseRejectedWith(reason); - writer._closedPromiseState = 'rejected'; -} - -function defaultWriterClosedPromiseResolve(writer) { - assert(writer._closedPromise_resolve !== undefined); - assert(writer._closedPromise_reject !== undefined); - assert(writer._closedPromiseState === 'pending'); - - writer._closedPromise_resolve(undefined); - writer._closedPromise_resolve = undefined; - writer._closedPromise_reject = undefined; - writer._closedPromiseState = 'resolved'; -} - -function defaultWriterReadyPromiseInitialize(writer) { - verbose('defaultWriterReadyPromiseInitialize()'); - writer._readyPromise = newPromise((resolve, reject) => { - writer._readyPromise_resolve = resolve; - writer._readyPromise_reject = reject; - }); - writer._readyPromiseState = 'pending'; -} - -function defaultWriterReadyPromiseInitializeAsRejected(writer, reason) { - verbose('defaultWriterReadyPromiseInitializeAsRejected(writer, %o)', reason); - writer._readyPromise = promiseRejectedWith(reason); - writer._readyPromise_resolve = undefined; - writer._readyPromise_reject = undefined; - writer._readyPromiseState = 'rejected'; -} - -function defaultWriterReadyPromiseInitializeAsResolved(writer) { - verbose('defaultWriterReadyPromiseInitializeAsResolved()'); - writer._readyPromise = promiseResolvedWith(undefined); - writer._readyPromise_resolve = undefined; - writer._readyPromise_reject = undefined; - writer._readyPromiseState = 'fulfilled'; -} - -function defaultWriterReadyPromiseReject(writer, reason) { - verbose('defaultWriterReadyPromiseReject(writer, %o)', reason); - assert(writer._readyPromise_resolve !== undefined); - assert(writer._readyPromise_reject !== undefined); - - writer._readyPromise_reject(reason); - writer._readyPromise_resolve = undefined; - writer._readyPromise_reject = undefined; - writer._readyPromiseState = 'rejected'; -} - -function defaultWriterReadyPromiseReset(writer) { - verbose('defaultWriterReadyPromiseReset()'); - assert(writer._readyPromise_resolve === undefined); - assert(writer._readyPromise_reject === undefined); - - writer._readyPromise = newPromise((resolve, reject) => { - writer._readyPromise_resolve = resolve; - writer._readyPromise_reject = reject; - }); - writer._readyPromiseState = 'pending'; -} - -function defaultWriterReadyPromiseResetToRejected(writer, reason) { - verbose('defaultWriterReadyPromiseResetToRejected(writer, %o)', reason); - assert(writer._readyPromise_resolve === undefined); - assert(writer._readyPromise_reject === undefined); - - writer._readyPromise = promiseRejectedWith(reason); - writer._readyPromiseState = 'rejected'; -} - -function defaultWriterReadyPromiseResolve(writer) { - verbose('defaultWriterReadyPromiseResolve()'); - assert(writer._readyPromise_resolve !== undefined); - assert(writer._readyPromise_reject !== undefined); - - writer._readyPromise_resolve(undefined); - writer._readyPromise_resolve = undefined; - writer._readyPromise_reject = undefined; - writer._readyPromiseState = 'fulfilled'; + WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller); } diff --git a/reference-implementation/lib/helpers.js b/reference-implementation/lib/helpers.js index c853aae65..7f7db89af 100644 --- a/reference-implementation/lib/helpers.js +++ b/reference-implementation/lib/helpers.js @@ -1,20 +1,10 @@ 'use strict'; const assert = require('assert'); -const { rethrowAssertionErrorRejection } = require('./utils.js'); const isFakeDetached = Symbol('is "detached" for our purposes'); -function IsPropertyKey(argument) { - return typeof argument === 'string' || typeof argument === 'symbol'; -} - exports.typeIsObject = x => (typeof x === 'object' && x !== null) || typeof x === 'function'; -exports.createDataProperty = (o, p, v) => { - assert(exports.typeIsObject(o)); - Object.defineProperty(o, p, { value: v, writable: true, enumerable: true, configurable: true }); -}; - exports.createArrayFromList = elements => { // We use arrays to represent lists, so this is basically a no-op. // Do a slice though just in case we happen to depend on the unique-ness. @@ -25,98 +15,6 @@ exports.ArrayBufferCopy = (dest, destOffset, src, srcOffset, n) => { new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset); }; -exports.IsFiniteNonNegativeNumber = v => { - if (exports.IsNonNegativeNumber(v) === false) { - return false; - } - - if (v === Infinity) { - return false; - } - - return true; -}; - -exports.IsNonNegativeNumber = v => { - if (typeof v !== 'number') { - return false; - } - - if (Number.isNaN(v)) { - return false; - } - - if (v < 0) { - return false; - } - - return true; -}; - -function Call(F, V, args) { - if (typeof F !== 'function') { - throw new TypeError('Argument is not a function'); - } - - return Function.prototype.apply.call(F, V, args); -} - -exports.Call = Call; - -exports.CreateAlgorithmFromUnderlyingMethod = (underlyingObject, methodName, algoArgCount, extraArgs) => { - assert(underlyingObject !== undefined); - assert(IsPropertyKey(methodName)); - assert(algoArgCount === 0 || algoArgCount === 1); - assert(Array.isArray(extraArgs)); - const method = underlyingObject[methodName]; - if (method !== undefined) { - if (typeof method !== 'function') { - throw new TypeError(`${method} is not a method`); - } - switch (algoArgCount) { - case 0: { - return () => { - return PromiseCall(method, underlyingObject, extraArgs); - }; - } - - case 1: { - return arg => { - const fullArgs = [arg].concat(extraArgs); - return PromiseCall(method, underlyingObject, fullArgs); - }; - } - } - } - return () => promiseResolvedWith(undefined); -}; - -exports.InvokeOrNoop = (O, P, args) => { - assert(O !== undefined); - assert(IsPropertyKey(P)); - assert(Array.isArray(args)); - - const method = O[P]; - if (method === undefined) { - return undefined; - } - - return Call(method, O, args); -}; - -function PromiseCall(F, V, args) { - assert(typeof F === 'function'); - assert(V !== undefined); - assert(Array.isArray(args)); - try { - return promiseResolvedWith(Call(F, V, args)); - } catch (value) { - return promiseRejectedWith(value); - } -} - -exports.PromiseCall = PromiseCall; - // Not implemented correctly exports.TransferArrayBuffer = O => { assert(!exports.IsDetachedBuffer(O)); @@ -139,139 +37,13 @@ exports.IsDetachedBuffer = O => { return isFakeDetached in O; }; -exports.ValidateAndNormalizeHighWaterMark = highWaterMark => { - highWaterMark = Number(highWaterMark); - if (Number.isNaN(highWaterMark) || highWaterMark < 0) { - throw new RangeError('highWaterMark property of a queuing strategy must be non-negative and non-NaN'); - } - - return highWaterMark; -}; - -exports.MakeSizeAlgorithmFromSizeFunction = size => { - if (size === undefined) { - return () => 1; +exports.rethrowAssertionErrorRejection = e => { + // Used throughout the reference implementation, as `.catch(rethrowAssertionErrorRejection)`, to ensure any errors + // get shown. There are places in the spec where we do promise transformations and purposefully ignore or don't + // expect any errors, but assertion errors are always problematic. + if (e && e instanceof assert.AssertionError) { + setTimeout(() => { + throw e; + }, 0); } - if (typeof size !== 'function') { - throw new TypeError('size property of a queuing strategy must be a function'); - } - return chunk => size(chunk); -}; - -const originalPromise = Promise; -const originalPromiseThen = Promise.prototype.then; -const originalPromiseResolve = Promise.resolve; -const originalPromiseReject = Promise.reject; - -function newPromise(executor) { - return new originalPromise(executor); -} - -function promiseResolvedWith(value) { - return originalPromiseResolve.call(originalPromise, value); -} - -function promiseRejectedWith(reason) { - return originalPromiseReject.call(originalPromise, reason); -} - -function PerformPromiseThen(promise, onFulfilled, onRejected) { - // There doesn't appear to be any way to correctly emulate the behaviour from JavaScript, so this is just an - // approximation. - return originalPromiseThen.call(promise, onFulfilled, onRejected); -} - -function uponPromise(promise, onFulfilled, onRejected) { - PerformPromiseThen( - PerformPromiseThen(promise, onFulfilled, onRejected), - undefined, - rethrowAssertionErrorRejection - ); -} - -function uponFulfillment(promise, onFulfilled) { - uponPromise(promise, onFulfilled); -} - -function uponRejection(promise, onRejected) { - uponPromise(promise, undefined, onRejected); -} - -function transformPromiseWith(promise, fulfillmentHandler, rejectionHandler) { - return PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler); -} - -function setPromiseIsHandledToTrue(promise) { - PerformPromiseThen(promise, undefined, rethrowAssertionErrorRejection); -} - -exports.newPromise = newPromise; -exports.promiseResolvedWith = promiseResolvedWith; -exports.promiseRejectedWith = promiseRejectedWith; -exports.uponPromise = uponPromise; -exports.uponFulfillment = uponFulfillment; -exports.uponRejection = uponRejection; -exports.transformPromiseWith = transformPromiseWith; -exports.setPromiseIsHandledToTrue = setPromiseIsHandledToTrue; - -exports.WaitForAll = (promises, successSteps, failureSteps) => { - let rejected = false; - const rejectionHandler = arg => { - if (rejected === false) { - rejected = true; - failureSteps(arg); - } - }; - let index = 0; - let fulfilledCount = 0; - const total = promises.length; - const result = new Array(total); - if (total === 0) { - queueMicrotask(() => successSteps(result)); - return; - } - for (const promise of promises) { - const promiseIndex = index; - const fulfillmentHandler = arg => { - result[promiseIndex] = arg; - ++fulfilledCount; - if (fulfilledCount === total) { - successSteps(result); - } - }; - PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler); - ++index; - } -}; - -exports.WaitForAllPromise = (promises, successSteps, failureSteps = undefined) => { - let resolvePromise; - let rejectPromise; - const promise = newPromise((resolve, reject) => { - resolvePromise = resolve; - rejectPromise = reject; - }); - if (failureSteps === undefined) { - failureSteps = arg => { - throw arg; - }; - } - const successStepsWrapper = results => { - try { - const stepsResult = successSteps(results); - resolvePromise(stepsResult); - } catch (e) { - rejectPromise(e); - } - }; - const failureStepsWrapper = reason => { - try { - const stepsResult = failureSteps(reason); - resolvePromise(stepsResult); - } catch (e) { - rejectPromise(e); - } - }; - exports.WaitForAll(promises, successStepsWrapper, failureStepsWrapper); - return promise; }; diff --git a/reference-implementation/lib/index.js b/reference-implementation/lib/index.js index a5492de0e..4b2a57ab1 100644 --- a/reference-implementation/lib/index.js +++ b/reference-implementation/lib/index.js @@ -2,17 +2,15 @@ // This file is used as the entry point for browserifying the reference implementation to allow it // to run inside the wpt-runner "browser like" context. -const { ReadableStream } = require('./readable-stream.js'); -const { WritableStream } = require('./writable-stream.js'); -const { TransformStream } = require('./transform-stream.js'); -const ByteLengthQueuingStrategy = require('../generated/ByteLengthQueuingStrategy.js'); -const CountQueuingStrategy = require('../generated/CountQueuingStrategy.js'); +// const { ReadableStream } = require('./readable-stream.js'); +// const { TransformStream } = require('./transform-stream.js'); -window.ReadableStream = ReadableStream; -window.WritableStream = WritableStream; -window.TransformStream = TransformStream; -window.CountQueuingStrategy = CountQueuingStrategy; +// window.ReadableStream = ReadableStream; +// window.TransformStream = TransformStream; window.gc = gc; -ByteLengthQueuingStrategy.install(window); -CountQueuingStrategy.install(window); +require('../generated/ByteLengthQueuingStrategy.js').install(window); +require('../generated/CountQueuingStrategy.js').install(window); +require('../generated/WritableStream.js').install(window); +require('../generated/WritableStreamDefaultController.js').install(window); +require('../generated/WritableStreamDefaultWriter.js').install(window); diff --git a/reference-implementation/lib/utils.js b/reference-implementation/lib/utils.js deleted file mode 100644 index d27388396..000000000 --- a/reference-implementation/lib/utils.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; -const assert = require('assert'); - -exports.rethrowAssertionErrorRejection = e => { - // Used throughout the reference implementation, as `.catch(rethrowAssertionErrorRejection)`, to ensure any errors - // get shown. There are places in the spec where we do promise transformations and purposefully ignore or don't - // expect any errors, but assertion errors are always problematic. - if (e && e instanceof assert.AssertionError) { - setTimeout(() => { - throw e; - }, 0); - } -}; diff --git a/reference-implementation/lib/webidl-helpers.js b/reference-implementation/lib/webidl-helpers.js new file mode 100644 index 000000000..1c88b8e4a --- /dev/null +++ b/reference-implementation/lib/webidl-helpers.js @@ -0,0 +1,202 @@ +'use strict'; +const utils = require('../generated/utils.js'); +const { rethrowAssertionErrorRejection } = require('./helpers.js'); + +// https://heycam.github.io/webidl/#new +// https://github.com/jsdom/webidl2js/issues/193 +exports.webidlNew = (globalObject, typeName, implModule) => { + if (globalObject[utils.ctorRegistrySymbol] === undefined) { + throw new Error('Internal error: invalid global object'); + } + + const ctor = globalObject[utils.ctorRegistrySymbol][typeName]; + if (ctor === undefined) { + throw new Error(`Internal error: constructor ${typeName} is not installed on the passed global object`); + } + + const obj = Object.create(ctor.prototype); + Object.defineProperty(obj, utils.implSymbol, { + value: Object.create(implModule.implementation.prototype), + configurable: true + }); + obj[utils.implSymbol][utils.wrapperSymbol] = obj; + return obj[utils.implSymbol]; +}; + +const originalPromise = Promise; +const originalPromiseThen = Promise.prototype.then; +const originalPromiseResolve = Promise.resolve; +const originalPromiseReject = Promise.reject; + +const promiseSideTable = new WeakMap(); + +// A specialization of https://heycam.github.io/webidl/#invoke-a-callback-function +// Can be replaced when https://github.com/jsdom/webidl2js/pull/123 lands. +exports.promiseInvoke = (func, args, thisArg) => { + args = args.map(utils.tryWrapperForImpl); + try { + return Promise.resolve(Reflect.apply(func, thisArg, args)); + } catch (e) { + return Promise.reject(e); + } +}; + +// A specialization of https://heycam.github.io/webidl/#invoke-a-callback-function +// Can be replaced when https://github.com/jsdom/webidl2js/pull/123 lands. +exports.invoke = (func, args, thisArg) => { + args = args.map(utils.tryWrapperForImpl); + return Reflect.apply(func, thisArg, args); +}; + +// https://heycam.github.io/webidl/#a-new-promise +function newPromise() { + // The stateIsPending tracking only works if we never resolve the promises with other promises. + // In this spec, that happens to be true for the promises in question; they are always resolved with undefined. + const sideData = { stateIsPending: true }; + const promise = new originalPromise((resolve, reject) => { + sideData.resolve = resolve; + sideData.reject = reject; + }); + + promiseSideTable.set(promise, sideData); + return promise; +} + +// https://heycam.github.io/webidl/#resolve +function resolvePromise(p, value) { + promiseSideTable.get(p).resolve(value); + promiseSideTable.get(p).stateIsPending = false; +} + +// https://heycam.github.io/webidl/#reject +function rejectPromise(p, reason) { + promiseSideTable.get(p).reject(reason); + promiseSideTable.get(p).stateIsPending = false; +} + +// https://heycam.github.io/webidl/#a-promise-resolved-with +function promiseResolvedWith(value) { + const promise = originalPromiseResolve.call(originalPromise, value); + promiseSideTable.set(promise, { stateIsPending: false }); + return promise; +} + +// https://heycam.github.io/webidl/#a-promise-rejected-with +function promiseRejectedWith(reason) { + const promise = originalPromiseReject.call(originalPromise, reason); + promiseSideTable.set(promise, { stateIsPending: false }); + return promise; +} + +function PerformPromiseThen(promise, onFulfilled, onRejected) { + // There doesn't appear to be any way to correctly emulate the behaviour from JavaScript, so this is just an + // approximation. + return originalPromiseThen.call(promise, onFulfilled, onRejected); +} + +// https://heycam.github.io/webidl/#dfn-perform-steps-once-promise-is-settled +function uponPromise(promise, onFulfilled, onRejected) { + PerformPromiseThen( + PerformPromiseThen(promise, onFulfilled, onRejected), + undefined, + rethrowAssertionErrorRejection + ); +} + +function uponFulfillment(promise, onFulfilled) { + uponPromise(promise, onFulfilled); +} + +function uponRejection(promise, onRejected) { + uponPromise(promise, undefined, onRejected); +} + +function transformPromiseWith(promise, fulfillmentHandler, rejectionHandler) { + return PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler); +} + +function setPromiseIsHandledToTrue(promise) { + PerformPromiseThen(promise, undefined, rethrowAssertionErrorRejection); +} + +function stateIsPending(promise) { + return promiseSideTable.get(promise).stateIsPending; +} + +Object.assign(exports, { + newPromise, + resolvePromise, + rejectPromise, + promiseResolvedWith, + promiseRejectedWith, + uponPromise, + uponFulfillment, + uponRejection, + transformPromiseWith, + setPromiseIsHandledToTrue, + stateIsPending +}); + +// https://heycam.github.io/webidl/#wait-for-all +exports.WaitForAll = (promises, successSteps, failureSteps) => { + let rejected = false; + const rejectionHandler = arg => { + if (rejected === false) { + rejected = true; + failureSteps(arg); + } + }; + let index = 0; + let fulfilledCount = 0; + const total = promises.length; + const result = new Array(total); + if (total === 0) { + queueMicrotask(() => successSteps(result)); + return; + } + for (const promise of promises) { + const promiseIndex = index; + const fulfillmentHandler = arg => { + result[promiseIndex] = arg; + ++fulfilledCount; + if (fulfilledCount === total) { + successSteps(result); + } + }; + PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler); + ++index; + } +}; + +// https://heycam.github.io/webidl/#waiting-for-all-promise +exports.WaitForAllPromise = (promises, successSteps, failureSteps = undefined) => { + let resolveP; + let rejectP; + const promise = newPromise((resolve, reject) => { + resolveP = resolve; + rejectP = reject; + }); + if (failureSteps === undefined) { + failureSteps = arg => { + throw arg; + }; + } + const successStepsWrapper = results => { + try { + const stepsResult = successSteps(results); + resolveP(stepsResult); + } catch (e) { + rejectP(e); + } + }; + const failureStepsWrapper = reason => { + try { + const stepsResult = failureSteps(reason); + resolveP(stepsResult); + } catch (e) { + rejectP(e); + } + }; + exports.WaitForAll(promises, successStepsWrapper, failureStepsWrapper); + return promise; +}; From d916d2869347afee33973590e0354c83c6e80efd Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Thu, 16 Apr 2020 15:17:08 -0400 Subject: [PATCH 07/30] Converted readable streams But tests are failing all over the place so there are probably some dumb mistakes in the conversion still --- index.bs | 213 ++- .../lib/ReadableByteStreamController-impl.js | 129 ++ .../lib/ReadableByteStreamController.webidl | 9 + .../lib/ReadableStream-impl.js | 97 ++ .../lib/ReadableStream.webidl | 34 + .../lib/ReadableStreamBYOBReader-impl.js | 46 + .../lib/ReadableStreamBYOBReader.webidl | 10 + .../lib/ReadableStreamBYOBRequest-impl.js | 30 + .../lib/ReadableStreamBYOBRequest.webidl | 7 + .../ReadableStreamDefaultController-impl.js | 60 + .../ReadableStreamDefaultController.webidl | 8 + .../lib/ReadableStreamDefaultReader-impl.js | 46 + .../lib/ReadableStreamDefaultReader.webidl | 10 + .../lib/UnderlyingSink.webidl | 2 +- .../lib/UnderlyingSource.webidl | 15 + .../lib/WritableStream-impl.js | 2 +- .../WritableStreamDefaultController-impl.js | 1 + .../lib/WritableStreamDefaultWriter-impl.js | 3 +- .../ecmascript.js} | 17 +- .../lib/abstract-ops/internal-methods.js | 2 + .../lib/abstract-ops/queuing-strategy.js | 2 +- .../readable-streams.js} | 1407 ++++------------- .../lib/abstract-ops/writable-streams.js | 9 +- .../lib/helpers/miscellaneous.js | 15 + .../{webidl-helpers.js => helpers/webidl.js} | 12 +- reference-implementation/lib/index.js | 12 +- 26 files changed, 946 insertions(+), 1252 deletions(-) create mode 100644 reference-implementation/lib/ReadableByteStreamController-impl.js create mode 100644 reference-implementation/lib/ReadableByteStreamController.webidl create mode 100644 reference-implementation/lib/ReadableStream-impl.js create mode 100644 reference-implementation/lib/ReadableStream.webidl create mode 100644 reference-implementation/lib/ReadableStreamBYOBReader-impl.js create mode 100644 reference-implementation/lib/ReadableStreamBYOBReader.webidl create mode 100644 reference-implementation/lib/ReadableStreamBYOBRequest-impl.js create mode 100644 reference-implementation/lib/ReadableStreamBYOBRequest.webidl create mode 100644 reference-implementation/lib/ReadableStreamDefaultController-impl.js create mode 100644 reference-implementation/lib/ReadableStreamDefaultController.webidl create mode 100644 reference-implementation/lib/ReadableStreamDefaultReader-impl.js create mode 100644 reference-implementation/lib/ReadableStreamDefaultReader.webidl create mode 100644 reference-implementation/lib/UnderlyingSource.webidl rename reference-implementation/lib/{helpers.js => abstract-ops/ecmascript.js} (58%) rename reference-implementation/lib/{readable-stream.js => abstract-ops/readable-streams.js} (51%) create mode 100644 reference-implementation/lib/helpers/miscellaneous.js rename reference-implementation/lib/{webidl-helpers.js => helpers/webidl.js} (94%) diff --git a/index.bs b/index.bs index 302dbbf57..46df094a7 100644 --- a/index.bs +++ b/index.bs @@ -528,8 +528,8 @@ the [=underlying source=]. Such objects can contain any of the following propert <xmp class="idl"> dictionary UnderlyingSource { - ReadableStreamControllerCallback start; - ReadableStreamControllerCallback pull; + ReadableStreamStartCallback start; + ReadableStreamPullCallback pull; ReadableStreamCancelCallback cancel; ReadableStreamType type; [EnforceRange] unsigned long long autoAllocateChunkSize; @@ -537,7 +537,8 @@ dictionary UnderlyingSource { typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController; -callback ReadableStreamControllerCallback = Promise<void> (ReadableStreamController controller); +callback ReadableStreamStartCallback = any (ReadableStreamController controller); +callback ReadableStreamPullCallback = Promise<void> (ReadableStreamController controller); callback ReadableStreamCancelCallback = Promise<void> (optional any reason); enum ReadableStreamType { "bytes" }; @@ -1660,6 +1661,8 @@ following table: for="ReadableStreamBYOBRequest">respond(|bytesWritten|)</dfn> method steps are: 1. If [=this=].\[[controller]] is undefined, throw a {{TypeError}} exception. + 1. If [$IsDetachedBuffer$]([=this=].\[[view]].\[[ArrayBuffer]]) is true, throw a {{TypeError}} + exception. 1. Perform ? [$ReadableByteStreamControllerRespond$]([=this=].\[[controller]], |bytesWritten|). </div> @@ -1781,7 +1784,6 @@ are even meant to be generally useful by other specifications. that wish to query whether or not a readable stream has ever been read from or canceled. It performs the following steps: - 1. Assert: |stream| [=implements=] {{ReadableStream}}. 1. Return |stream|.\[[disturbed]]. </div> @@ -1790,96 +1792,10 @@ are even meant to be generally useful by other specifications. export>IsReadableStreamLocked(|stream|)</dfn> is meant to be called from other specifications that wish to query whether or not a readable stream is [=locked to a reader=]. - 1. Assert: |stream| [=implements=] {{ReadableStream}}. 1. If |stream|.\[[reader]] is undefined, return false. 1. Return true. </div> -<div algorithm> - <dfn abstract-op lt="ReadableStreamTee" id="readable-stream-tee" export>ReadableStreamTee(|stream|, - |cloneForBranch2|)</dfn> is meant to be called from other specifications that wish to [=tee a - readable stream|tee=] a given readable stream. - - The second argument, |cloneForBranch2|, governs whether or not the data from the original stream - will be cloned (using HTML's [=serializable objects=] framework) before appearing in the second of - the returned branches. This is useful for scenarios where both branches are to be consumed in such - a way that they might otherwise interfere with each other, such as by [=transferable - objects|transferring=] their [=chunks=]. However, it does introduce a noticeable asymmetry between - the two branches, and limits the possible [=chunks=] to serializable ones. [[!HTML]] - - <p class="note">In this standard ReadableStreamTee is always called with |cloneForBranch2| set to - false; other specifications pass true. - - It performs the following steps: - - 1. Assert: |stream| [=implements=] {{ReadableStream}}. - 1. Assert: |cloneForBranch2| is a boolean. - 1. Let |reader| be ? [$AcquireReadableStreamDefaultReader$](|stream|). - 1. Let |reading| be false. - 1. Let |canceled1| be false. - 1. Let |canceled2| be false. - 1. Let |reason1| be undefined. - 1. Let |reason2| be undefined. - 1. Let |branch1| be undefined. - 1. Let |branch2| be undefined. - 1. Let |cancelPromise| be [=a new promise=]. - 1. Let |pullAlgorithm| be the following steps: - 1. If |reading| is true, return [=a promise resolved with=] undefined. - 1. Set |reading| to true. - 1. Let |readPromise| be the result of [=reacting=] to ! - [$ReadableStreamDefaultReaderRead$](|reader|) with the following fulfillment steps given the - argument |result|: - 1. Set |reading| to false. - 1. Assert: [$Type$](|result|) is Object. - 1. Let |done| be ! [$Get$](|result|, "`done`"). - 1. Assert: [$Type$](|done|) is Boolean. - 1. If |done| is true, - 1. If |canceled1| is false, - 1. Perform ! [$ReadableStreamDefaultControllerClose$](|branch1|.\[[readableStreamController]]). - 1. If |canceled2| is false, - 1. Perform ! - [$ReadableStreamDefaultControllerClose$](|branch2|.\[[readableStreamController]]). - 1. Return. - 1. Let |value| be ! [$Get$](|result|, "`value`"). - 1. Let |value1| and |value2| be |value|. - 1. If |canceled2| is false and |cloneForBranch2| is true, set |value2| to ? - [$StructuredDeserialize$](? [$StructuredSerialize$](|value2|), [=the current Realm=]). - 1. If |canceled1| is false, perform ? - [$ReadableStreamDefaultControllerEnqueue$](|branch1|.\[[readableStreamController]], - |value1|). - 1. If |canceled2| is false, perform ? - ReadableStreamDefaultControllerEnqueue(|branch2|.\[[readableStreamController]], |value2|). - 1. Set |readPromise|.\[[PromiseIsHandled]] to true. - 1. Return [=a promise resolved with=] undefined. - 1. Let |cancel1Algorithm| be the following steps, taking a |reason| argument: - 1. Set |canceled1| to true. - 1. Set |reason1| to |reason|. - 1. If |canceled2| is true, - 1. Let |compositeReason| be ! [$CreateArrayFromList$](« |reason1|, |reason2| »). - 1. Let |cancelResult| be ! [$ReadableStreamCancel$](|stream|, |compositeReason|). - 1. [=Resolve=] |cancelPromise| with |cancelResult|. - 1. Return |cancelPromise|. - 1. Let |cancel2Algorithm| be the following steps, taking a |reason| argument: - 1. Set |canceled2| to true. - 1. Set |reason2| to |reason|. - 1. If |canceled1| is true, - 1. Let |compositeReason| be ! [$CreateArrayFromList$](« |reason1|, |reason2| »). - 1. Let |cancelResult| be ! [$ReadableStreamCancel$](|stream|, |compositeReason|). - 1. [=Resolve=] |cancelPromise| with |cancelResult|. - 1. Return |cancelPromise|. - 1. Let |startAlgorithm| be an algorithm that returns undefined. - 1. Set |branch1| to ! [$CreateReadableStream$](|startAlgorithm|, |pullAlgorithm|, - |cancel1Algorithm|). - 1. Set |branch2| to ! [$CreateReadableStream$](|startAlgorithm|, |pullAlgorithm|, - |cancel2Algorithm|). - 1. [=Upon rejection=] of |reader|.\[[closedPromise]] with reason |r|, - 1. Perform ! [$ReadableStreamDefaultControllerError$](|branch1|.\[[readableStreamController]], - |r|). - 1. Perform ! [$ReadableStreamDefaultControllerError$](|branch2|.\[[readableStreamController]], - |r|). - 1. Return « |branch1|, |branch2| ». -</div> - <div algorithm="ReadableStreamPipeTo"> <dfn abstract-op lt="ReadableStreamPipeTo" id="readable-stream-pipe-to" export>ReadableStreamPipeTo(|source|, |dest|, |preventClose|, |preventAbort|, |preventCancel|[, @@ -2005,6 +1921,91 @@ promises), which usually would require specifying a realm for the created object of the locking, none of these objects can be observed by author code. As such, the realm used to create them does not matter. +<div algorithm> + <dfn abstract-op lt="ReadableStreamTee" id="readable-stream-tee" export>ReadableStreamTee(|stream|, + |cloneForBranch2|)</dfn> is meant to be called from other specifications that wish to [=tee a + readable stream|tee=] a given readable stream. + + The second argument, |cloneForBranch2|, governs whether or not the data from the original stream + will be cloned (using HTML's [=serializable objects=] framework) before appearing in the second of + the returned branches. This is useful for scenarios where both branches are to be consumed in such + a way that they might otherwise interfere with each other, such as by [=transferable + objects|transferring=] their [=chunks=]. However, it does introduce a noticeable asymmetry between + the two branches, and limits the possible [=chunks=] to serializable ones. [[!HTML]] + + <p class="note">In this standard ReadableStreamTee is always called with |cloneForBranch2| set to + false; other specifications pass true. + + It performs the following steps: + + 1. Assert: |stream| [=implements=] {{ReadableStream}}. + 1. Assert: |cloneForBranch2| is a boolean. + 1. Let |reader| be ? [$AcquireReadableStreamDefaultReader$](|stream|). + 1. Let |reading| be false. + 1. Let |canceled1| be false. + 1. Let |canceled2| be false. + 1. Let |reason1| be undefined. + 1. Let |reason2| be undefined. + 1. Let |branch1| be undefined. + 1. Let |branch2| be undefined. + 1. Let |cancelPromise| be [=a new promise=]. + 1. Let |pullAlgorithm| be the following steps: + 1. If |reading| is true, return [=a promise resolved with=] undefined. + 1. Set |reading| to true. + 1. Let |readPromise| be the result of [=reacting=] to ! + [$ReadableStreamDefaultReaderRead$](|reader|) with the following fulfillment steps given the + argument |result|: + 1. Set |reading| to false. + 1. Assert: [$Type$](|result|) is Object. + 1. Let |done| be ! [$Get$](|result|, "`done`"). + 1. Assert: [$Type$](|done|) is Boolean. + 1. If |done| is true, + 1. If |canceled1| is false, + 1. Perform ! [$ReadableStreamDefaultControllerClose$](|branch1|.\[[readableStreamController]]). + 1. If |canceled2| is false, + 1. Perform ! + [$ReadableStreamDefaultControllerClose$](|branch2|.\[[readableStreamController]]). + 1. Return. + 1. Let |value| be ! [$Get$](|result|, "`value`"). + 1. Let |value1| and |value2| be |value|. + 1. If |canceled2| is false and |cloneForBranch2| is true, set |value2| to ? + [$StructuredDeserialize$](? [$StructuredSerialize$](|value2|), [=the current Realm=]). + 1. If |canceled1| is false, perform ? + [$ReadableStreamDefaultControllerEnqueue$](|branch1|.\[[readableStreamController]], + |value1|). + 1. If |canceled2| is false, perform ? + ReadableStreamDefaultControllerEnqueue(|branch2|.\[[readableStreamController]], |value2|). + 1. Set |readPromise|.\[[PromiseIsHandled]] to true. + 1. Return [=a promise resolved with=] undefined. + 1. Let |cancel1Algorithm| be the following steps, taking a |reason| argument: + 1. Set |canceled1| to true. + 1. Set |reason1| to |reason|. + 1. If |canceled2| is true, + 1. Let |compositeReason| be ! [$CreateArrayFromList$](« |reason1|, |reason2| »). + 1. Let |cancelResult| be ! [$ReadableStreamCancel$](|stream|, |compositeReason|). + 1. [=Resolve=] |cancelPromise| with |cancelResult|. + 1. Return |cancelPromise|. + 1. Let |cancel2Algorithm| be the following steps, taking a |reason| argument: + 1. Set |canceled2| to true. + 1. Set |reason2| to |reason|. + 1. If |canceled1| is true, + 1. Let |compositeReason| be ! [$CreateArrayFromList$](« |reason1|, |reason2| »). + 1. Let |cancelResult| be ! [$ReadableStreamCancel$](|stream|, |compositeReason|). + 1. [=Resolve=] |cancelPromise| with |cancelResult|. + 1. Return |cancelPromise|. + 1. Let |startAlgorithm| be an algorithm that returns undefined. + 1. Set |branch1| to ! [$CreateReadableStream$](|startAlgorithm|, |pullAlgorithm|, + |cancel1Algorithm|). + 1. Set |branch2| to ! [$CreateReadableStream$](|startAlgorithm|, |pullAlgorithm|, + |cancel2Algorithm|). + 1. [=Upon rejection=] of |reader|.\[[closedPromise]] with reason |r|, + 1. Perform ! [$ReadableStreamDefaultControllerError$](|branch1|.\[[readableStreamController]], + |r|). + 1. Perform ! [$ReadableStreamDefaultControllerError$](|branch2|.\[[readableStreamController]], + |r|). + 1. Return « |branch1|, |branch2| ». +</div> + <h4 id="rs-abstract-ops-used-by-controllers">Interfacing with controllers</h4> In terms of specification factoring, the way that the {{ReadableStream}} class encapsulates the @@ -2046,8 +2047,7 @@ the {{ReadableStream}}'s public API. 1. Assert: |stream|.\[[reader]] [=implements=] {{ReadableStreamBYOBReader}}. 1. Assert: |stream|.\[[state]] is "`readable"` or `"closed`". 1. Let |promise| be [=a new promise=]. - 1. Let |readIntoRequest| be Record {\[[promise]]: |promise|}. - 1. [=list/Append=] |readIntoRequest| to |stream|.\[[reader]].\[[readIntoRequests]]. + 1. [=list/Append=] |promise| to |stream|.\[[reader]].\[[readIntoRequests]]. 1. Return |promise|. </div> @@ -2059,8 +2059,7 @@ the {{ReadableStream}}'s public API. 1. Assert: |stream|.\[[reader]] [=implements=] {{ReadableStreamDefaultReader}}. 1. Assert: |stream|.\[[state]] is "`readable"`. 1. Let |promise| be [=a new promise=]. - 1. Let |readIntoRequest| be Record {\[[promise]]: |promise|}. - 1. [=list/Append=] |readIntoRequest| to |stream|.\[[reader]].\[[readRequests]]. + 1. [=list/Append=] |promise| to |stream|.\[[reader]].\[[readRequests]]. 1. Return |promise|. </div> @@ -2087,10 +2086,10 @@ the {{ReadableStream}}'s public API. 1. Set |stream|.\[[state]] to "`closed`". 1. Let |reader| be |stream|.\[[reader]]. 1. If |reader| is undefined, return. - 1. If ! IsReadableStreamDefaultReader(|reader|) is true, + 1. If |reader| [=implements=] {{ReadableStreamDefaultReader}}, 1. [=list/For each=] |readRequest| of |reader|.\[[readRequests]], - 1. [=Resolve=] |readRequest|.\[[promise]] with ! [$ReadableStreamCreateReadResult$](undefined, - true, |reader|.\[[forAuthorCode]]). + 1. [=Resolve=] |readRequest| with ! [$ReadableStreamCreateReadResult$](undefined, true, + |reader|.\[[forAuthorCode]]). 1. Set |reader|.\[[readRequests]] to an empty [=list=]. 1. [=Resolve=] |reader|.\[[closedPromise]] with undefined. @@ -2141,7 +2140,6 @@ the {{ReadableStream}}'s public API. <dfn abstract-op lt="ReadableStreamError" id="readable-stream-error">ReadableStreamError(|stream|, |e|)</dfn> performs the following steps: - 1. Assert: |stream|.\[[reader]] [=implements=] {{ReadableStreamDefaultReader}}. 1. Assert: |stream|.\[[state]] is "`readable`". 1. Set |stream|.\[[state]] to "`errored`". 1. Set |stream|.\[[storedError]] to |e|. @@ -2149,12 +2147,12 @@ the {{ReadableStream}}'s public API. 1. If |reader| is undefined, return. 1. If |reader| [=implements=] {{ReadableStreamDefaultReader}}, 1. [=list/For each=] |readRequest| of |reader|.\[[readRequests]], - 1. [=Reject=] |readRequest|.\[[promise]] with |e|. + 1. [=Reject=] |readRequest| with |e|. 1. Set |reader|.\[[readRequests]] to a new empty [=list=]. 1. Otherwise, 1. Assert: |reader| [=implements=] {{ReadableStreamBYOBReader}}. 1. [=list/For each=] |readIntoRequest| of |reader|.\[[readIntoRequests]], - 1. [=Reject=] |readIntoRequest|.\[[promise]] with |e|. + 1. [=Reject=] |readIntoRequest| with |e|. 1. Set |reader|.\[[readIntoRequests]] to a new empty [=list=]. 1. [=Reject=] |reader|.\[[closedPromise]] with |e|. 1. Set |reader|.\[[closedPromise]].\[[PromiseIsHandled]] to true. @@ -2166,9 +2164,10 @@ the {{ReadableStream}}'s public API. |chunk|, |done|)</dfn> performs the following steps: 1. Let |reader| be |stream|.\[[reader]]. + 1. Assert: |reader|.\[[readIntoRequests]] is not [=list/is empty|empty=]. 1. Let |readIntoRequest| be |reader|.\[[readIntoRequests]][0]. 1. [=list/Remove=] |readIntoRequest| from |reader|.\[[readIntoRequests]]. - 1. [=Resolve=] |readIntoRequest|.\[[promise]] with ! [$ReadableStreamCreateReadResult$](|chunk|, + 1. [=Resolve=] |readIntoRequest| with ! [$ReadableStreamCreateReadResult$](|chunk|, |done|, |reader|.\[[forAuthorCode]]). </div> @@ -2178,10 +2177,11 @@ the {{ReadableStream}}'s public API. |done|)</dfn> performs the following steps: 1. Let |reader| be |stream|.\[[reader]]. + 1. Assert: |reader|.\[[readRequests]] is not [=list/is empty|empty=]. 1. Let |readRequest| be |reader|.\[[readRequests]][0]. 1. [=list/Remove=] |readRequest| from |reader|.\[[readRequests]]. - 1. [=Resolve=] |readRequest|.\[[promise]] with ! [$ReadableStreamCreateReadResult$](|chunk|, - |done|, |reader|.\[[forAuthorCode]]). + 1. [=Resolve=] |readRequest| with ! [$ReadableStreamCreateReadResult$](|chunk|, |done|, + |reader|.\[[forAuthorCode]]). </div> <div algorithm> @@ -2406,7 +2406,7 @@ The following abstract operations support the implementation of the <div algorithm> <dfn abstract-op lt="ReadableStreamDefaultControllerEnqueue" id="readable-stream-default-controller-enqueue" - export>ReadableStreamDefaultControllerClose(|controller|, |chunk|)</dfn> can be called by other + export>ReadableStreamDefaultControllerEnqueue(|controller|, |chunk|)</dfn> can be called by other specifications that wish to enqueue [=chunks=] in a readable stream, in the same way a developer would enqueue chunks using the stream's associated controller object. Specifications should <em>not</em> do this to streams or controllers they did not create. @@ -2501,8 +2501,7 @@ The following abstract operations support the implementation of the 1. Assert: |stream|.\[[readableStreamController]] is undefined. 1. Set |controller|.\[[controlledReadableStream]] to |stream|. - 1. Set |controller|.\[[queue]] and |controller|.\[[queueTotalSize]] to undefined, then perform ! - [$ResetQueue$](|controller|). + 1. Perform ! [$ResetQueue$](|controller|). 1. Set |controller|.\[[started]], |controller|.\[[closeRequested]], |controller|.\[[pullAgain]], and |controller|.\[[pulling]] to false. 1. Set |controller|.\[[strategySizeAlgorithm]] to |sizeAlgorithm| and |controller|.\[[strategyHWM]] @@ -3258,7 +3257,7 @@ dictionary UnderlyingSink { any type; }; -callback WritableStreamStartCallback = Promise<void> (WritableStreamDefaultController controller); +callback WritableStreamStartCallback = any (WritableStreamDefaultController controller); callback WritableStreamWriteCallback = Promise<void> (WritableStreamDefaultController controller, optional any chunk); callback WritableStreamCloseCallback = Promise<void> (); callback WritableStreamAbortCallback = Promise<void> (optional any reason); diff --git a/reference-implementation/lib/ReadableByteStreamController-impl.js b/reference-implementation/lib/ReadableByteStreamController-impl.js new file mode 100644 index 000000000..4fd8a3497 --- /dev/null +++ b/reference-implementation/lib/ReadableByteStreamController-impl.js @@ -0,0 +1,129 @@ +'use strict'; +const assert = require('assert'); + +const { webidlNew, promiseResolvedWith, promiseRejectedWith } = require('./helpers/webidl.js'); +const { IsDetachedBuffer } = require('./abstract-ops/ecmascript.js'); +const { CancelSteps, PullSteps } = require('./abstract-ops/internal-methods.js'); +const { ResetQueue } = require('./abstract-ops/queue-with-sizes.js'); +const aos = require('./abstract-ops/readable-streams.js'); + +const ReadableStreamBYOBRequestImpl = require('./ReadableStreamBYOBRequest-impl.js'); + +exports.implementation = class ReadableByteStreamControllerImpl { + get byobRequest() { + if (this._byobRequest === undefined && this._pendingPullIntos.length > 0) { + const firstDescriptor = this._pendingPullIntos[0]; + const view = new Uint8Array(firstDescriptor.buffer, + firstDescriptor.byteOffset + firstDescriptor.bytesFilled, + firstDescriptor.byteLength - firstDescriptor.bytesFilled); + + const byobRequest = webidlNew(globalThis, 'ReadableStreamBYOBRequest', ReadableStreamBYOBRequestImpl); + byobRequest._controller = this; + byobRequest._view = view; + this._byobRequest = byobRequest; + } + + return this._byobRequest; + } + + get desiredSize() { + return aos.ReadableByteStreamControllerGetDesiredSize(this); + } + + close() { + if (this._closeRequested === true) { + throw new TypeError('The stream has already been closed; do not close it again!'); + } + + const state = this._controlledReadableByteStream._state; + if (state !== 'readable') { + throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`); + } + + aos.ReadableByteStreamControllerClose(this); + } + + enqueue(chunk) { + if (this._closeRequested === true) { + throw new TypeError('stream is closed or draining'); + } + + const state = this._controlledReadableByteStream._state; + if (state !== 'readable') { + throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`); + } + + if (!ArrayBuffer.isView(chunk)) { + throw new TypeError('You can only enqueue array buffer views when using a ReadableByteStreamController'); + } + + if (IsDetachedBuffer(chunk.buffer) === true) { + throw new TypeError('Cannot enqueue a view onto a detached ArrayBuffer'); + } + + aos.ReadableByteStreamControllerEnqueue(this, chunk); + } + + error(e) { + aos.ReadableByteStreamControllerError(this, e); + } + + [CancelSteps](reason) { + if (this._pendingPullIntos.length > 0) { + const firstDescriptor = this._pendingPullIntos[0]; + firstDescriptor.bytesFilled = 0; + } + + ResetQueue(this); + + const result = this._cancelAlgorithm(reason); + aos.ReadableByteStreamControllerClearAlgorithms(this); + return result; + } + + [PullSteps]() { + const stream = this._controlledReadableByteStream; + assert(aos.ReadableStreamHasDefaultReader(stream) === true); + + if (this._queueTotalSize > 0) { + assert(aos.ReadableStreamGetNumReadRequests(stream) === 0); + + const entry = this._queue.shift(); + this._queueTotalSize -= entry.byteLength; + + aos.ReadableByteStreamControllerHandleQueueDrain(this); + + const view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength); + + return promiseResolvedWith(aos.ReadableStreamCreateReadResult(view, false, stream._reader._forAuthorCode)); + } + + const autoAllocateChunkSize = this._autoAllocateChunkSize; + if (autoAllocateChunkSize !== undefined) { + let buffer; + try { + buffer = new ArrayBuffer(autoAllocateChunkSize); + } catch (bufferE) { + return promiseRejectedWith(bufferE); + } + + const pullIntoDescriptor = { + buffer, + byteOffset: 0, + byteLength: autoAllocateChunkSize, + bytesFilled: 0, + elementSize: 1, + ctor: Uint8Array, + readerType: 'default' + }; + + this._pendingPullIntos.push(pullIntoDescriptor); + } + + const promise = aos.ReadableStreamAddReadRequest(stream); + + aos.ReadableByteStreamControllerCallPullIfNeeded(this); + + return promise; + } +}; diff --git a/reference-implementation/lib/ReadableByteStreamController.webidl b/reference-implementation/lib/ReadableByteStreamController.webidl new file mode 100644 index 000000000..7705c12fb --- /dev/null +++ b/reference-implementation/lib/ReadableByteStreamController.webidl @@ -0,0 +1,9 @@ +[Exposed=(Window,Worker,Worklet)] +interface ReadableByteStreamController { + readonly attribute ReadableStreamBYOBRequest byobRequest; + readonly attribute unrestricted double? desiredSize; + + void close(); + void enqueue(ArrayBufferView chunk); + void error(optional any e); +}; diff --git a/reference-implementation/lib/ReadableStream-impl.js b/reference-implementation/lib/ReadableStream-impl.js new file mode 100644 index 000000000..c6487702f --- /dev/null +++ b/reference-implementation/lib/ReadableStream-impl.js @@ -0,0 +1,97 @@ +'use strict'; +const assert = require('assert'); + +const { promiseRejectedWith, setPromiseIsHandledToTrue } = require('./helpers/webidl.js'); +const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/queuing-strategy.js'); +const aos = require('./abstract-ops/readable-streams.js'); +const wsAOs = require('./abstract-ops/writable-streams.js'); + +const UnderlyingSource = require('../generated/UnderlyingSource.js'); + +exports.implementation = class ReadableStreamImpl { + constructor(globalObject, [underlyingSource, strategy]) { + if (underlyingSource === undefined) { + underlyingSource = null; + } + const underlyingSourceDict = UnderlyingSource.convert(underlyingSource); + + aos.InitializeReadableStream(this); + + if (underlyingSourceDict.type === 'bytes') { + if ('size' in strategy) { + throw new RangeError('The strategy for a byte stream cannot have a size function'); + } + + const highWaterMark = ExtractHighWaterMark(strategy, 0); + aos.SetUpReadableByteStreamControllerFromUnderlyingSource( + this, underlyingSource, underlyingSourceDict, highWaterMark + ); + } else { + assert(!('type' in underlyingSourceDict)); + const sizeAlgorithm = ExtractSizeAlgorithm(strategy); + const highWaterMark = ExtractHighWaterMark(strategy, 1); + aos.SetUpReadableStreamDefaultControllerFromUnderlyingSource( + this, underlyingSource, underlyingSourceDict, highWaterMark, sizeAlgorithm + ); + } + } + + get locked() { + return aos.IsReadableStreamLocked(this); + } + + cancel(reason) { + if (aos.IsReadableStreamLocked(this) === true) { + return promiseRejectedWith(new TypeError('Cannot cancel a stream that already has a reader')); + } + + return aos.ReadableStreamCancel(this, reason); + } + + getReader(options) { + if (!('mode' in options)) { + return aos.AcquireReadableStreamDefaultReader(this, true); + } + + assert(options.mode === 'byob'); + return aos.AcquireReadableStreamBYOBReader(this, true); + } + + pipeThrough(transform, options) { + if (aos.IsReadableStreamLocked(this) === true) { + throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream'); + } + if (wsAOs.IsWritableStreamLocked(transform.writable) === true) { + throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream'); + } + + const promise = aos.ReadableStreamPipeTo( + this, transform.writable, options.preventClose, options.preventAbort, options.preventCancel, options.signal + ); + + setPromiseIsHandledToTrue(promise); + + return transform.readable; + } + + pipeTo(destination, options) { + if (aos.IsReadableStreamLocked(this) === true) { + return promiseRejectedWith( + new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream') + ); + } + if (wsAOs.IsWritableStreamLocked(destination) === true) { + return promiseRejectedWith( + new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream') + ); + } + + return aos.ReadableStreamPipeTo( + this, destination, options.preventClose, options.preventAbort, options.preventCancel, options.signal + ); + } + + tee() { + return aos.ReadableStreamTee(this, false); + } +}; diff --git a/reference-implementation/lib/ReadableStream.webidl b/reference-implementation/lib/ReadableStream.webidl new file mode 100644 index 000000000..67ce71741 --- /dev/null +++ b/reference-implementation/lib/ReadableStream.webidl @@ -0,0 +1,34 @@ +[Exposed=(Window,Worker,Worklet)] +interface ReadableStream { + constructor(optional object underlyingSource, optional QueuingStrategy strategy = {}); + + readonly attribute boolean locked; + + Promise<void> cancel(optional any reason); + ReadableStreamReader getReader(optional ReadableStreamGetReaderOptions options = {}); + ReadableStream pipeThrough(ReadableWritablePair transform, optional StreamPipeOptions options = {}); + Promise<void> pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); + sequence<ReadableStream> tee(); + + // TODO: async iterator +}; + +typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader; + +enum ReadableStreamReaderMode { "byob" }; + +dictionary ReadableStreamGetReaderOptions { + ReadableStreamReaderMode mode; +}; + +dictionary ReadableWritablePair { + required ReadableStream readable; + required WritableStream writable; +}; + +dictionary StreamPipeOptions { + boolean preventClose = false; + boolean preventAbort = false; + boolean preventCancel = false; + AbortSignal signal; +}; diff --git a/reference-implementation/lib/ReadableStreamBYOBReader-impl.js b/reference-implementation/lib/ReadableStreamBYOBReader-impl.js new file mode 100644 index 000000000..690d61959 --- /dev/null +++ b/reference-implementation/lib/ReadableStreamBYOBReader-impl.js @@ -0,0 +1,46 @@ +'use strict'; + +const { promiseRejectedWith } = require('./helpers/webidl.js'); +const aos = require('./abstract-ops/readable-streams.js'); + +exports.implementation = class ReadableStreamBYOBReaderImpl { + constructor(globalObject, [stream]) { + aos.SetUpReadableStreamBYOBReader(this, stream); + } + + get closed() { + return this._closedPromise; + } + + cancel(reason) { + if (this._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('cancel')); + } + + return aos.ReadableStreamReaderGenericCancel(this, reason); + } + + read(view) { + if (view.byteLength === 0) { + return promiseRejectedWith(new TypeError('view must have non-zero byteLength')); + } + + return aos.ReadableStreamBYOBReaderRead(this, view); + } + + releaseLock() { + if (this._ownerReadableStream === undefined) { + return; + } + + if (this._readIntoRequests.length > 0) { + throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled'); + } + + aos.ReadableStreamReaderGenericRelease(this); + } +}; + +function readerLockException(name) { + return new TypeError('Cannot ' + name + ' a stream using a released reader'); +} diff --git a/reference-implementation/lib/ReadableStreamBYOBReader.webidl b/reference-implementation/lib/ReadableStreamBYOBReader.webidl new file mode 100644 index 000000000..8ac1b05a5 --- /dev/null +++ b/reference-implementation/lib/ReadableStreamBYOBReader.webidl @@ -0,0 +1,10 @@ +[Exposed=(Window,Worker,Worklet)] +interface ReadableStreamBYOBReader { + constructor(ReadableStream stream); + + readonly attribute Promise<void> closed; + + Promise<void> cancel(optional any reason); + Promise<any> read(ArrayBufferView view); + void releaseLock(); +}; diff --git a/reference-implementation/lib/ReadableStreamBYOBRequest-impl.js b/reference-implementation/lib/ReadableStreamBYOBRequest-impl.js new file mode 100644 index 000000000..2c7980c81 --- /dev/null +++ b/reference-implementation/lib/ReadableStreamBYOBRequest-impl.js @@ -0,0 +1,30 @@ +'use strict'; + +const { IsDetachedBuffer } = require('./abstract-ops/ecmascript.js'); +const aos = require('./abstract-ops/readable-streams.js'); + +exports.implementation = class ReadableStreamBYOBRequestImpl { + get view() { + return this._view; + } + + respond(bytesWritten) { + if (this._controller === undefined) { + throw new TypeError('This BYOB request has been invalidated'); + } + + if (IsDetachedBuffer(this._view.buffer) === true) { + throw new TypeError('The BYOB request\'s buffer has been detached and so cannot be used as a response'); + } + + aos.ReadableByteStreamControllerRespond(this._controller, bytesWritten); + } + + respondWithNewView(view) { + if (this._controller === undefined) { + throw new TypeError('This BYOB request has been invalidated'); + } + + aos.ReadableByteStreamControllerRespondWithNewView(this._controller, view); + } +}; diff --git a/reference-implementation/lib/ReadableStreamBYOBRequest.webidl b/reference-implementation/lib/ReadableStreamBYOBRequest.webidl new file mode 100644 index 000000000..e7950727d --- /dev/null +++ b/reference-implementation/lib/ReadableStreamBYOBRequest.webidl @@ -0,0 +1,7 @@ +[Exposed=(Window,Worker,Worklet)] +interface ReadableStreamBYOBRequest { + readonly attribute ArrayBufferView view; + + void respond([EnforceRange] unsigned long long bytesWritten); + void respondWithNewView(ArrayBufferView view); +}; diff --git a/reference-implementation/lib/ReadableStreamDefaultController-impl.js b/reference-implementation/lib/ReadableStreamDefaultController-impl.js new file mode 100644 index 000000000..fdd747e7e --- /dev/null +++ b/reference-implementation/lib/ReadableStreamDefaultController-impl.js @@ -0,0 +1,60 @@ +'use strict'; + +const { promiseResolvedWith } = require('./helpers/webidl.js'); +const { CancelSteps, PullSteps } = require('./abstract-ops/internal-methods.js'); +const { DequeueValue, ResetQueue } = require('./abstract-ops/queue-with-sizes.js'); +const aos = require('./abstract-ops/readable-streams.js'); + +exports.implementation = class ReadableStreamDefaultControllerImpl { + get desiredSize() { + return aos.ReadableStreamDefaultControllerGetDesiredSize(this); + } + + close() { + if (aos.ReadableStreamDefaultControllerCanCloseOrEnqueue(this) === false) { + throw new TypeError('The stream is not in a state that permits close'); + } + + aos.ReadableStreamDefaultControllerClose(this); + } + + enqueue(chunk) { + if (aos.ReadableStreamDefaultControllerCanCloseOrEnqueue(this) === false) { + throw new TypeError('The stream is not in a state that permits enqueue'); + } + + return aos.ReadableStreamDefaultControllerEnqueue(this, chunk); + } + + error(e) { + aos.ReadableStreamDefaultControllerError(this, e); + } + + [CancelSteps](reason) { + ResetQueue(this); + const result = this._cancelAlgorithm(reason); + aos.ReadableStreamDefaultControllerClearAlgorithms(this); + return result; + } + + [PullSteps]() { + const stream = this._controlledReadableStream; + + if (this._queue.length > 0) { + const chunk = DequeueValue(this); + + if (this._closeRequested === true && this._queue.length === 0) { + aos.ReadableStreamDefaultControllerClearAlgorithms(this); + aos.ReadableStreamClose(stream); + } else { + aos.ReadableStreamDefaultControllerCallPullIfNeeded(this); + } + + return promiseResolvedWith(aos.ReadableStreamCreateReadResult(chunk, false, stream._reader._forAuthorCode)); + } + + const pendingPromise = aos.ReadableStreamAddReadRequest(stream); + aos.ReadableStreamDefaultControllerCallPullIfNeeded(this); + return pendingPromise; + } +}; diff --git a/reference-implementation/lib/ReadableStreamDefaultController.webidl b/reference-implementation/lib/ReadableStreamDefaultController.webidl new file mode 100644 index 000000000..23809d0a8 --- /dev/null +++ b/reference-implementation/lib/ReadableStreamDefaultController.webidl @@ -0,0 +1,8 @@ +[Exposed=(Window,Worker,Worklet)] +interface ReadableStreamDefaultController { + readonly attribute unrestricted double? desiredSize; + + void close(); + void enqueue(optional any chunk); + void error(optional any e); +}; diff --git a/reference-implementation/lib/ReadableStreamDefaultReader-impl.js b/reference-implementation/lib/ReadableStreamDefaultReader-impl.js new file mode 100644 index 000000000..e4bb857c5 --- /dev/null +++ b/reference-implementation/lib/ReadableStreamDefaultReader-impl.js @@ -0,0 +1,46 @@ +'use strict'; + +const { promiseRejectedWith } = require('./helpers/webidl.js'); +const aos = require('./abstract-ops/readable-streams.js'); + +exports.implementation = class ReadableStreamDefaultReaderImpl { + constructor(globalObject, [stream]) { + aos.SetUpReadableStreamDefaultReader(this, stream); + } + + get closed() { + return this._closedPromise; + } + + cancel(reason) { + if (this._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('cancel')); + } + + return aos.ReadableStreamReaderGenericCancel(this, reason); + } + + read() { + if (this._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('read from')); + } + + return aos.ReadableStreamDefaultReaderRead(this); + } + + releaseLock() { + if (this._ownerReadableStream === undefined) { + return; + } + + if (this._readRequests.length > 0) { + throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled'); + } + + aos.ReadableStreamReaderGenericRelease(this); + } +}; + +function readerLockException(name) { + return new TypeError('Cannot ' + name + ' a stream using a released reader'); +} diff --git a/reference-implementation/lib/ReadableStreamDefaultReader.webidl b/reference-implementation/lib/ReadableStreamDefaultReader.webidl new file mode 100644 index 000000000..f750cb2f7 --- /dev/null +++ b/reference-implementation/lib/ReadableStreamDefaultReader.webidl @@ -0,0 +1,10 @@ +[Exposed=(Window,Worker,Worklet)] +interface ReadableStreamDefaultReader { + constructor(ReadableStream stream); + + readonly attribute Promise<void> closed; + + Promise<void> cancel(optional any reason); + Promise<any> read(); + void releaseLock(); +}; diff --git a/reference-implementation/lib/UnderlyingSink.webidl b/reference-implementation/lib/UnderlyingSink.webidl index 5b6b1d271..ae6ecf1ad 100644 --- a/reference-implementation/lib/UnderlyingSink.webidl +++ b/reference-implementation/lib/UnderlyingSink.webidl @@ -6,7 +6,7 @@ dictionary UnderlyingSink { any type; }; -callback WritableStreamStartCallback = Promise<void> (WritableStreamDefaultController controller); +callback WritableStreamStartCallback = any (WritableStreamDefaultController controller); callback WritableStreamWriteCallback = Promise<void> (WritableStreamDefaultController controller, optional any chunk); callback WritableStreamCloseCallback = Promise<void> (); callback WritableStreamAbortCallback = Promise<void> (optional any reason); diff --git a/reference-implementation/lib/UnderlyingSource.webidl b/reference-implementation/lib/UnderlyingSource.webidl new file mode 100644 index 000000000..5d1b679e8 --- /dev/null +++ b/reference-implementation/lib/UnderlyingSource.webidl @@ -0,0 +1,15 @@ +dictionary UnderlyingSource { + ReadableStreamStartCallback start; + ReadableStreamPullCallback pull; + ReadableStreamCancelCallback cancel; + ReadableStreamType type; + [EnforceRange] unsigned long long autoAllocateChunkSize; +}; + +typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController; + +callback ReadableStreamStartCallback = any (ReadableStreamController controller); +callback ReadableStreamPullCallback = Promise<void> (ReadableStreamController controller); +callback ReadableStreamCancelCallback = Promise<void> (optional any reason); + + enum ReadableStreamType { "bytes" }; diff --git a/reference-implementation/lib/WritableStream-impl.js b/reference-implementation/lib/WritableStream-impl.js index b12b00055..7d23802d4 100644 --- a/reference-implementation/lib/WritableStream-impl.js +++ b/reference-implementation/lib/WritableStream-impl.js @@ -1,6 +1,6 @@ 'use strict'; -const { promiseRejectedWith } = require('./webidl-helpers.js'); +const { promiseRejectedWith } = require('./helpers/webidl.js'); const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/queuing-strategy.js'); const aos = require('./abstract-ops/writable-streams.js'); diff --git a/reference-implementation/lib/WritableStreamDefaultController-impl.js b/reference-implementation/lib/WritableStreamDefaultController-impl.js index 3b9d8c3df..17a865ee2 100644 --- a/reference-implementation/lib/WritableStreamDefaultController-impl.js +++ b/reference-implementation/lib/WritableStreamDefaultController-impl.js @@ -1,4 +1,5 @@ 'use strict'; + const aos = require('./abstract-ops/writable-streams.js'); const { AbortSteps, ErrorSteps } = require('./abstract-ops/internal-methods.js'); const { ResetQueue } = require('./abstract-ops/queue-with-sizes.js'); diff --git a/reference-implementation/lib/WritableStreamDefaultWriter-impl.js b/reference-implementation/lib/WritableStreamDefaultWriter-impl.js index 9d64ef0af..689bae1bc 100644 --- a/reference-implementation/lib/WritableStreamDefaultWriter-impl.js +++ b/reference-implementation/lib/WritableStreamDefaultWriter-impl.js @@ -1,8 +1,7 @@ 'use strict'; const assert = require('assert'); -const { promiseRejectedWith } = require('./webidl-helpers.js'); - +const { promiseRejectedWith } = require('./helpers/webidl.js'); const aos = require('./abstract-ops/writable-streams.js'); exports.implementation = class WritableStreamDefaultWriterImpl { diff --git a/reference-implementation/lib/helpers.js b/reference-implementation/lib/abstract-ops/ecmascript.js similarity index 58% rename from reference-implementation/lib/helpers.js rename to reference-implementation/lib/abstract-ops/ecmascript.js index 7f7db89af..636e352da 100644 --- a/reference-implementation/lib/helpers.js +++ b/reference-implementation/lib/abstract-ops/ecmascript.js @@ -3,15 +3,13 @@ const assert = require('assert'); const isFakeDetached = Symbol('is "detached" for our purposes'); -exports.typeIsObject = x => (typeof x === 'object' && x !== null) || typeof x === 'function'; - -exports.createArrayFromList = elements => { +exports.CreateArrayFromList = elements => { // We use arrays to represent lists, so this is basically a no-op. // Do a slice though just in case we happen to depend on the unique-ness. return elements.slice(); }; -exports.ArrayBufferCopy = (dest, destOffset, src, srcOffset, n) => { +exports.CopyDataBlockBytes = (dest, destOffset, src, srcOffset, n) => { new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset); }; @@ -36,14 +34,3 @@ exports.TransferArrayBuffer = O => { exports.IsDetachedBuffer = O => { return isFakeDetached in O; }; - -exports.rethrowAssertionErrorRejection = e => { - // Used throughout the reference implementation, as `.catch(rethrowAssertionErrorRejection)`, to ensure any errors - // get shown. There are places in the spec where we do promise transformations and purposefully ignore or don't - // expect any errors, but assertion errors are always problematic. - if (e && e instanceof assert.AssertionError) { - setTimeout(() => { - throw e; - }, 0); - } -}; diff --git a/reference-implementation/lib/abstract-ops/internal-methods.js b/reference-implementation/lib/abstract-ops/internal-methods.js index cd69a76df..1debfa6a7 100644 --- a/reference-implementation/lib/abstract-ops/internal-methods.js +++ b/reference-implementation/lib/abstract-ops/internal-methods.js @@ -2,3 +2,5 @@ exports.AbortSteps = Symbol('[[AbortSteps]]'); exports.ErrorSteps = Symbol('[[ErrorSteps]]'); +exports.CancelSteps = Symbol('[[CancelSteps]]'); +exports.PullSteps = Symbol('[[PullSteps]]'); diff --git a/reference-implementation/lib/abstract-ops/queuing-strategy.js b/reference-implementation/lib/abstract-ops/queuing-strategy.js index 9dd10d2c1..b7d4af6be 100644 --- a/reference-implementation/lib/abstract-ops/queuing-strategy.js +++ b/reference-implementation/lib/abstract-ops/queuing-strategy.js @@ -1,5 +1,5 @@ 'use strict'; -const { invoke } = require('../webidl-helpers.js'); +const { invoke } = require('../helpers/webidl.js'); exports.ExtractHighWaterMark = (strategy, defaultHWM) => { if (!('highWaterMark' in strategy)) { diff --git a/reference-implementation/lib/readable-stream.js b/reference-implementation/lib/abstract-ops/readable-streams.js similarity index 51% rename from reference-implementation/lib/readable-stream.js rename to reference-implementation/lib/abstract-ops/readable-streams.js index c44dfaa72..a7f035c9d 100644 --- a/reference-implementation/lib/readable-stream.js +++ b/reference-implementation/lib/abstract-ops/readable-streams.js @@ -1,273 +1,89 @@ 'use strict'; - const assert = require('assert'); -const { ArrayBufferCopy, CreateAlgorithmFromUnderlyingMethod, IsFiniteNonNegativeNumber, InvokeOrNoop, - IsDetachedBuffer, TransferArrayBuffer, ValidateAndNormalizeHighWaterMark, IsNonNegativeNumber, - MakeSizeAlgorithmFromSizeFunction, createArrayFromList, typeIsObject, WaitForAllPromise, - newPromise, promiseResolvedWith, promiseRejectedWith, uponPromise, uponFulfillment, uponRejection, - transformPromiseWith, setPromiseIsHandledToTrue } = require('./helpers.js'); -const { DequeueValue, EnqueueValueWithSize, ResetQueue } = require('./queue-with-sizes.js'); -const { AcquireWritableStreamDefaultWriter, IsWritableStream, IsWritableStreamLocked, - WritableStreamAbort, WritableStreamDefaultWriterCloseWithErrorPropagation, - WritableStreamDefaultWriterRelease, WritableStreamDefaultWriterWrite, WritableStreamCloseQueuedOrInFlight } = - require('./writable-stream.js'); - -const CancelSteps = Symbol('[[CancelSteps]]'); -const PullSteps = Symbol('[[PullSteps]]'); - -class ReadableStream { - constructor(underlyingSource = {}, strategy = {}) { - InitializeReadableStream(this); - - const size = strategy.size; - let highWaterMark = strategy.highWaterMark; - - const type = underlyingSource.type; - const typeString = String(type); - if (typeString === 'bytes') { - if (size !== undefined) { - throw new RangeError('The strategy for a byte stream cannot have a size function'); - } - - if (highWaterMark === undefined) { - highWaterMark = 0; - } - highWaterMark = ValidateAndNormalizeHighWaterMark(highWaterMark); - - SetUpReadableByteStreamControllerFromUnderlyingSource(this, underlyingSource, highWaterMark); - } else if (type === undefined) { - const sizeAlgorithm = MakeSizeAlgorithmFromSizeFunction(size); - - if (highWaterMark === undefined) { - highWaterMark = 1; - } - highWaterMark = ValidateAndNormalizeHighWaterMark(highWaterMark); - - SetUpReadableStreamDefaultControllerFromUnderlyingSource(this, underlyingSource, highWaterMark, sizeAlgorithm); - } else { - throw new RangeError('Invalid type is specified'); - } - } - - get locked() { - if (IsReadableStream(this) === false) { - throw streamBrandCheckException('locked'); - } - - return IsReadableStreamLocked(this); - } - - cancel(reason) { - if (IsReadableStream(this) === false) { - return promiseRejectedWith(streamBrandCheckException('cancel')); - } - - if (IsReadableStreamLocked(this) === true) { - return promiseRejectedWith(new TypeError('Cannot cancel a stream that already has a reader')); - } - - return ReadableStreamCancel(this, reason); - } - - getReader({ mode } = {}) { - if (IsReadableStream(this) === false) { - throw streamBrandCheckException('getReader'); - } - - if (mode === undefined) { - return AcquireReadableStreamDefaultReader(this, true); - } - - mode = String(mode); - - if (mode === 'byob') { - return AcquireReadableStreamBYOBReader(this, true); - } - - throw new RangeError('Invalid mode is specified'); - } - - pipeThrough({ writable, readable }, { preventClose, preventAbort, preventCancel, signal } = {}) { - if (IsReadableStream(this) === false) { - throw streamBrandCheckException('pipeThrough'); - } - - if (IsWritableStream(writable) === false) { - throw new TypeError('writable argument to pipeThrough must be a WritableStream'); - } - - if (IsReadableStream(readable) === false) { - throw new TypeError('readable argument to pipeThrough must be a ReadableStream'); - } - - preventClose = Boolean(preventClose); - preventAbort = Boolean(preventAbort); - preventCancel = Boolean(preventCancel); - - if (signal !== undefined && !isAbortSignal(signal)) { - throw new TypeError('ReadableStream.prototype.pipeThrough\'s signal option must be an AbortSignal'); - } - - if (IsReadableStreamLocked(this) === true) { - throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream'); - } - if (IsWritableStreamLocked(writable) === true) { - throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream'); - } - - const promise = ReadableStreamPipeTo(this, writable, preventClose, preventAbort, preventCancel, signal); - - setPromiseIsHandledToTrue(promise); - - return readable; - } - - pipeTo(dest, { preventClose, preventAbort, preventCancel, signal } = {}) { - if (IsReadableStream(this) === false) { - return promiseRejectedWith(streamBrandCheckException('pipeTo')); - } - if (IsWritableStream(dest) === false) { - return promiseRejectedWith( - new TypeError('ReadableStream.prototype.pipeTo\'s first argument must be a WritableStream')); - } - - preventClose = Boolean(preventClose); - preventAbort = Boolean(preventAbort); - preventCancel = Boolean(preventCancel); - - if (signal !== undefined && !isAbortSignal(signal)) { - return promiseRejectedWith( - new TypeError('ReadableStream.prototype.pipeTo\'s signal option must be an AbortSignal') - ); - } - - if (IsReadableStreamLocked(this) === true) { - return promiseRejectedWith( - new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream') - ); - } - if (IsWritableStreamLocked(dest) === true) { - return promiseRejectedWith( - new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream') - ); - } - - return ReadableStreamPipeTo(this, dest, preventClose, preventAbort, preventCancel, signal); - } - - tee() { - if (IsReadableStream(this) === false) { - throw streamBrandCheckException('tee'); - } - - const branches = ReadableStreamTee(this, false); - return createArrayFromList(branches); - } - getIterator({ preventCancel = false } = {}) { - if (IsReadableStream(this) === false) { - throw streamBrandCheckException('getIterator'); - } - const reader = AcquireReadableStreamDefaultReader(this); - const iterator = Object.create(ReadableStreamAsyncIteratorPrototype); - iterator._asyncIteratorReader = reader; - iterator._preventCancel = Boolean(preventCancel); - return iterator; - } -} - -const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype); -const ReadableStreamAsyncIteratorPrototype = Object.setPrototypeOf({ - next() { - if (IsReadableStreamAsyncIterator(this) === false) { - return promiseRejectedWith(streamAsyncIteratorBrandCheckException('next')); - } - const reader = this._asyncIteratorReader; - if (reader._ownerReadableStream === undefined) { - return promiseRejectedWith(readerLockException('iterate')); - } - return transformPromiseWith(ReadableStreamDefaultReaderRead(reader), result => { - assert(typeIsObject(result)); - const done = result.done; - assert(typeof done === 'boolean'); - if (done) { - ReadableStreamReaderGenericRelease(reader); - } - const value = result.value; - return ReadableStreamCreateReadResult(value, done, true); - }); - }, - - return(value) { - if (IsReadableStreamAsyncIterator(this) === false) { - return promiseRejectedWith(streamAsyncIteratorBrandCheckException('next')); - } - const reader = this._asyncIteratorReader; - if (reader._ownerReadableStream === undefined) { - return promiseRejectedWith(readerLockException('finish iterating')); - } - if (reader._readRequests.length > 0) { - return promiseRejectedWith(new TypeError( - 'Tried to release a reader lock when that reader has pending read() calls un-settled')); - } - if (this._preventCancel === false) { - const result = ReadableStreamReaderGenericCancel(reader, value); - ReadableStreamReaderGenericRelease(reader); - return transformPromiseWith(result, () => ReadableStreamCreateReadResult(value, true, true)); - } - ReadableStreamReaderGenericRelease(reader); - return promiseResolvedWith(ReadableStreamCreateReadResult(value, true, true)); - } -}, AsyncIteratorPrototype); -Object.defineProperty(ReadableStreamAsyncIteratorPrototype, 'next', { enumerable: false }); -Object.defineProperty(ReadableStreamAsyncIteratorPrototype, 'return', { enumerable: false }); - -Object.defineProperty(ReadableStream.prototype, Symbol.asyncIterator, { - value: ReadableStream.prototype.getIterator, - enumerable: false, - writable: true, - configurable: true -}); - -module.exports = { +const { webidlNew, promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, + rejectPromise, uponPromise, setPromiseIsHandledToTrue, waitForAllPromise, transformPromiseWith, uponFulfillment, + uponRejection } = require('../helpers/webidl.js'); +const { typeIsObject } = require('../helpers/miscellaneous.js'); +const { CopyDataBlockBytes, CreateArrayFromList, TransferArrayBuffer } = require('./ecmascript.js'); +const { IsNonNegativeNumber } = require('./miscellaneous.js'); +const { EnqueueValueWithSize, ResetQueue } = require('./queue-with-sizes.js'); +const { AcquireWritableStreamDefaultWriter, IsWritableStreamLocked, WritableStreamAbort, + WritableStreamDefaultWriterCloseWithErrorPropagation, WritableStreamDefaultWriterRelease, + WritableStreamDefaultWriterWrite, WritableStreamCloseQueuedOrInFlight } = require('./writable-streams.js'); +const { CancelSteps, PullSteps } = require('./internal-methods.js'); + +const ReadableByteStreamControllerImpl = require('../ReadableByteStreamController-impl.js'); +const ReadableStreamBYOBReaderImpl = require('../ReadableStreamBYOBReader-impl.js'); +const ReadableStreamDefaultReaderImpl = require('../ReadableStreamDefaultReader-impl.js'); +const ReadableStreamDefaultControllerImpl = require('../ReadableStreamDefaultController-impl.js'); +const ReadableStreamImpl = require('../ReadableStream-impl.js'); +const WritableStreamImpl = require('../WritableStream-impl.js'); + +Object.assign(exports, { AcquireReadableStreamBYOBReader, AcquireReadableStreamDefaultReader, - CreateReadableByteStream, - CreateReadableStream, - ReadableStream, - IsReadableStreamDisturbed, + InitializeReadableStream, + IsReadableStreamLocked, + ReadableByteStreamControllerCallPullIfNeeded, + ReadableByteStreamControllerClearAlgorithms, + ReadableByteStreamControllerClose, + ReadableByteStreamControllerEnqueue, + ReadableByteStreamControllerError, + ReadableByteStreamControllerGetDesiredSize, + ReadableByteStreamControllerHandleQueueDrain, + ReadableByteStreamControllerRespond, + ReadableByteStreamControllerRespondWithNewView, + ReadableStreamAddReadRequest, + ReadableStreamBYOBReaderRead, + ReadableStreamCancel, + ReadableStreamClose, + ReadableStreamCreateReadResult, + ReadableStreamDefaultControllerCallPullIfNeeded, + ReadableStreamDefaultControllerCanCloseOrEnqueue, + ReadableStreamDefaultControllerClearAlgorithms, ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue, ReadableStreamDefaultControllerError, ReadableStreamDefaultControllerGetDesiredSize, ReadableStreamDefaultControllerHasBackpressure, - ReadableStreamDefaultControllerCanCloseOrEnqueue -}; + ReadableStreamDefaultReaderRead, + ReadableStreamGetNumReadRequests, + ReadableStreamHasDefaultReader, + ReadableStreamPipeTo, + ReadableStreamReaderGenericCancel, + ReadableStreamReaderGenericRelease, + ReadableStreamTee, + SetUpReadableByteStreamControllerFromUnderlyingSource, + SetUpReadableStreamBYOBReader, + SetUpReadableStreamDefaultControllerFromUnderlyingSource, + SetUpReadableStreamDefaultReader +}); -// Abstract operations for the ReadableStream. +// Working with readable streams function AcquireReadableStreamBYOBReader(stream, forAuthorCode = false) { - const reader = new ReadableStreamBYOBReader(stream); + const reader = webidlNew(globalThis, 'ReadableStreamBYOBReader', ReadableStreamBYOBReaderImpl); + SetUpReadableStreamBYOBReader(reader, stream); reader._forAuthorCode = forAuthorCode; return reader; } function AcquireReadableStreamDefaultReader(stream, forAuthorCode = false) { - const reader = new ReadableStreamDefaultReader(stream); + const reader = webidlNew(globalThis, 'ReadableStreamDefaultReader', ReadableStreamDefaultReaderImpl); + SetUpReadableStreamDefaultReader(reader, stream); reader._forAuthorCode = forAuthorCode; return reader; } -// Throws if and only if startAlgorithm throws. function CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark = 1, sizeAlgorithm = () => 1) { assert(IsNonNegativeNumber(highWaterMark) === true); - const stream = Object.create(ReadableStream.prototype); + const stream = webidlNew(globalThis, 'ReadableStream', ReadableStreamImpl); InitializeReadableStream(stream); - const controller = Object.create(ReadableStreamDefaultController.prototype); - + const controller = webidlNew(globalThis, 'ReadableStreamDefaultController', ReadableStreamDefaultControllerImpl); SetUpReadableStreamDefaultController( stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm ); @@ -275,25 +91,7 @@ function CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, hi return stream; } -// Throws if and only if startAlgorithm throws. -function CreateReadableByteStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark = 0, - autoAllocateChunkSize = undefined) { - assert(IsNonNegativeNumber(highWaterMark) === true); - if (autoAllocateChunkSize !== undefined) { - assert(Number.isInteger(autoAllocateChunkSize) === true); - assert(autoAllocateChunkSize > 0); - } - - const stream = Object.create(ReadableStream.prototype); - InitializeReadableStream(stream); - - const controller = Object.create(ReadableByteStreamController.prototype); - - SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, - autoAllocateChunkSize); - - return stream; -} +// CreateReadableByteStream is not implemented since it is only meant for external specs. function InitializeReadableStream(stream) { stream._state = 'readable'; @@ -302,27 +100,9 @@ function InitializeReadableStream(stream) { stream._disturbed = false; } -function IsReadableStream(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_readableStreamController')) { - return false; - } - - return true; -} - -function IsReadableStreamDisturbed(stream) { - assert(IsReadableStream(stream) === true); - - return stream._disturbed; -} +// IsReadableStreamDisturbed is not implemented since it is only meant for external specs. function IsReadableStreamLocked(stream) { - assert(IsReadableStream(stream) === true); - if (stream._reader === undefined) { return false; } @@ -330,25 +110,13 @@ function IsReadableStreamLocked(stream) { return true; } -function IsReadableStreamAsyncIterator(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_asyncIteratorReader')) { - return false; - } - - return true; -} - function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventCancel, signal) { - assert(IsReadableStream(source) === true); - assert(IsWritableStream(dest) === true); + assert(source instanceof ReadableStreamImpl.implementation); + assert(dest instanceof WritableStreamImpl.implementation); assert(typeof preventClose === 'boolean'); assert(typeof preventAbort === 'boolean'); assert(typeof preventCancel === 'boolean'); - assert(signal === undefined || isAbortSignal(signal)); + assert(signal === undefined || signal.constructor.name === 'AbortSignal'); assert(IsReadableStreamLocked(source) === false); assert(IsWritableStreamLocked(dest) === false); @@ -384,7 +152,7 @@ function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventC return promiseResolvedWith(undefined); }); } - shutdownWithAction(() => WaitForAllPromise(actions.map(action => action()), results => results), true, error); + shutdownWithAction(() => waitForAllPromise(actions.map(action => action()), results => results), true, error); }; if (signal.aborted === true) { @@ -546,7 +314,7 @@ function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventC } function ReadableStreamTee(stream, cloneForBranch2) { - assert(IsReadableStream(stream) === true); + assert(stream instanceof ReadableStreamImpl.implementation); assert(typeof cloneForBranch2 === 'boolean'); const reader = AcquireReadableStreamDefaultReader(stream); @@ -616,7 +384,7 @@ function ReadableStreamTee(stream, cloneForBranch2) { canceled1 = true; reason1 = reason; if (canceled2 === true) { - const compositeReason = createArrayFromList([reason1, reason2]); + const compositeReason = CreateArrayFromList([reason1, reason2]); const cancelResult = ReadableStreamCancel(stream, compositeReason); resolveCancelPromise(cancelResult); } @@ -627,7 +395,7 @@ function ReadableStreamTee(stream, cloneForBranch2) { canceled2 = true; reason2 = reason; if (canceled1 === true) { - const compositeReason = createArrayFromList([reason1, reason2]); + const compositeReason = CreateArrayFromList([reason1, reason2]); const cancelResult = ReadableStreamCancel(stream, compositeReason); resolveCancelPromise(cancelResult); } @@ -647,37 +415,23 @@ function ReadableStreamTee(stream, cloneForBranch2) { return [branch1, branch2]; } -// ReadableStream API exposed for controllers. +// Interfacing with controllers function ReadableStreamAddReadIntoRequest(stream) { - assert(IsReadableStreamBYOBReader(stream._reader) === true); + assert(stream._reader instanceof ReadableStreamBYOBReaderImpl.implementation); assert(stream._state === 'readable' || stream._state === 'closed'); - const promise = newPromise((resolve, reject) => { - const readIntoRequest = { - _resolve: resolve, - _reject: reject - }; - - stream._reader._readIntoRequests.push(readIntoRequest); - }); - + const promise = newPromise(); + stream._reader._readIntoRequests.push(promise); return promise; } function ReadableStreamAddReadRequest(stream) { - assert(IsReadableStreamDefaultReader(stream._reader) === true); + assert(stream._reader instanceof ReadableStreamDefaultReaderImpl.implementation); assert(stream._state === 'readable'); - const promise = newPromise((resolve, reject) => { - const readRequest = { - _resolve: resolve, - _reject: reject - }; - - stream._reader._readRequests.push(readRequest); - }); - + const promise = newPromise(); + stream._reader._readRequests.push(promise); return promise; } @@ -708,14 +462,14 @@ function ReadableStreamClose(stream) { return; } - if (IsReadableStreamDefaultReader(reader) === true) { - for (const { _resolve } of reader._readRequests) { - _resolve(ReadableStreamCreateReadResult(undefined, true, reader._forAuthorCode)); + if (reader instanceof ReadableStreamDefaultReaderImpl.implementation) { + for (const readRequest of reader._readRequests) { + resolvePromise(readRequest, ReadableStreamCreateReadResult(undefined, true, reader._forAuthorCode)); } reader._readRequests = []; } - defaultReaderClosedPromiseResolve(reader); + resolvePromise(reader._closedPromise, undefined); } function ReadableStreamCreateReadResult(value, done, forAuthorCode) { @@ -731,7 +485,6 @@ function ReadableStreamCreateReadResult(value, done, forAuthorCode) { } function ReadableStreamError(stream, e) { - assert(IsReadableStream(stream) === true); assert(stream._state === 'readable'); stream._state = 'errored'; @@ -743,23 +496,23 @@ function ReadableStreamError(stream, e) { return; } - if (IsReadableStreamDefaultReader(reader) === true) { + if (reader instanceof ReadableStreamDefaultReaderImpl.implementation) { for (const readRequest of reader._readRequests) { - readRequest._reject(e); + rejectPromise(readRequest, e); } reader._readRequests = []; } else { - assert(IsReadableStreamBYOBReader(reader)); + assert(reader instanceof ReadableStreamBYOBReaderImpl.implementation); for (const readIntoRequest of reader._readIntoRequests) { - readIntoRequest._reject(e); + rejectPromise(readIntoRequest, e); } reader._readIntoRequests = []; } - defaultReaderClosedPromiseReject(reader, e); + rejectPromise(reader._closedPromise, e); setPromiseIsHandledToTrue(reader._closedPromise); } @@ -769,7 +522,7 @@ function ReadableStreamFulfillReadIntoRequest(stream, chunk, done) { assert(reader._readIntoRequests.length > 0); const readIntoRequest = reader._readIntoRequests.shift(); - readIntoRequest._resolve(ReadableStreamCreateReadResult(chunk, done, reader._forAuthorCode)); + resolvePromise(readIntoRequest, ReadableStreamCreateReadResult(chunk, done, reader._forAuthorCode)); } function ReadableStreamFulfillReadRequest(stream, chunk, done) { @@ -778,7 +531,7 @@ function ReadableStreamFulfillReadRequest(stream, chunk, done) { assert(reader._readRequests.length > 0); const readRequest = reader._readRequests.shift(); - readRequest._resolve(ReadableStreamCreateReadResult(chunk, done, reader._forAuthorCode)); + resolvePromise(readRequest, ReadableStreamCreateReadResult(chunk, done, reader._forAuthorCode)); } function ReadableStreamGetNumReadIntoRequests(stream) { @@ -796,11 +549,11 @@ function ReadableStreamHasBYOBReader(stream) { return false; } - if (IsReadableStreamBYOBReader(reader) === false) { - return false; + if (reader instanceof ReadableStreamBYOBReaderImpl.implementation) { + return true; } - return true; + return false; } function ReadableStreamHasDefaultReader(stream) { @@ -810,182 +563,19 @@ function ReadableStreamHasDefaultReader(stream) { return false; } - if (IsReadableStreamDefaultReader(reader) === false) { - return false; + if (reader instanceof ReadableStreamDefaultReaderImpl.implementation) { + return true; } - return true; + return false; } // Readers -class ReadableStreamDefaultReader { - constructor(stream) { - if (IsReadableStream(stream) === false) { - throw new TypeError('ReadableStreamDefaultReader can only be constructed with a ReadableStream instance'); - } - if (IsReadableStreamLocked(stream) === true) { - throw new TypeError('This stream has already been locked for exclusive reading by another reader'); - } - - ReadableStreamReaderGenericInitialize(this, stream); - - this._readRequests = []; - } - - get closed() { - if (IsReadableStreamDefaultReader(this) === false) { - return promiseRejectedWith(defaultReaderBrandCheckException('closed')); - } - - return this._closedPromise; - } - - cancel(reason) { - if (IsReadableStreamDefaultReader(this) === false) { - return promiseRejectedWith(defaultReaderBrandCheckException('cancel')); - } - - if (this._ownerReadableStream === undefined) { - return promiseRejectedWith(readerLockException('cancel')); - } - - return ReadableStreamReaderGenericCancel(this, reason); - } - - read() { - if (IsReadableStreamDefaultReader(this) === false) { - return promiseRejectedWith(defaultReaderBrandCheckException('read')); - } - - if (this._ownerReadableStream === undefined) { - return promiseRejectedWith(readerLockException('read from')); - } - - return ReadableStreamDefaultReaderRead(this); - } - - releaseLock() { - if (IsReadableStreamDefaultReader(this) === false) { - throw defaultReaderBrandCheckException('releaseLock'); - } - - if (this._ownerReadableStream === undefined) { - return; - } - - if (this._readRequests.length > 0) { - throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled'); - } - - ReadableStreamReaderGenericRelease(this); - } -} - -class ReadableStreamBYOBReader { - constructor(stream) { - if (!IsReadableStream(stream)) { - throw new TypeError('ReadableStreamBYOBReader can only be constructed with a ReadableStream instance given a ' + - 'byte source'); - } - if (IsReadableByteStreamController(stream._readableStreamController) === false) { - throw new TypeError('Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte ' + - 'source'); - } - if (IsReadableStreamLocked(stream)) { - throw new TypeError('This stream has already been locked for exclusive reading by another reader'); - } - - ReadableStreamReaderGenericInitialize(this, stream); - - this._readIntoRequests = []; - } - - get closed() { - if (!IsReadableStreamBYOBReader(this)) { - return promiseRejectedWith(byobReaderBrandCheckException('closed')); - } - - return this._closedPromise; - } - - cancel(reason) { - if (!IsReadableStreamBYOBReader(this)) { - return promiseRejectedWith(byobReaderBrandCheckException('cancel')); - } - - if (this._ownerReadableStream === undefined) { - return promiseRejectedWith(readerLockException('cancel')); - } - - return ReadableStreamReaderGenericCancel(this, reason); - } - - read(view) { - if (!IsReadableStreamBYOBReader(this)) { - return promiseRejectedWith(byobReaderBrandCheckException('read')); - } - - if (this._ownerReadableStream === undefined) { - return promiseRejectedWith(readerLockException('read from')); - } - - if (!ArrayBuffer.isView(view)) { - return promiseRejectedWith(new TypeError('view must be an array buffer view')); - } - - if (IsDetachedBuffer(view.buffer) === true) { - return promiseRejectedWith(new TypeError('Cannot read into a view onto a detached ArrayBuffer')); - } - - if (view.byteLength === 0) { - return promiseRejectedWith(new TypeError('view must have non-zero byteLength')); - } - - return ReadableStreamBYOBReaderRead(this, view); - } - - releaseLock() { - if (!IsReadableStreamBYOBReader(this)) { - throw byobReaderBrandCheckException('releaseLock'); - } - - if (this._ownerReadableStream === undefined) { - return; - } - - if (this._readIntoRequests.length > 0) { - throw new TypeError('Tried to release a reader lock when that reader has pending read() calls un-settled'); - } - - ReadableStreamReaderGenericRelease(this); - } -} - -// Abstract operations for the readers. - -function IsReadableStreamBYOBReader(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_readIntoRequests')) { - return false; - } - - return true; -} - -function IsReadableStreamDefaultReader(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_readRequests')) { - return false; - } - - return true; +function ReadableStreamReaderGenericCancel(reader, reason) { + const stream = reader._ownerReadableStream; + assert(stream !== undefined); + return ReadableStreamCancel(stream, reason); } function ReadableStreamReaderGenericInitialize(reader, stream) { @@ -994,38 +584,30 @@ function ReadableStreamReaderGenericInitialize(reader, stream) { stream._reader = reader; if (stream._state === 'readable') { - defaultReaderClosedPromiseInitialize(reader); + reader._closedPromise = newPromise(); } else if (stream._state === 'closed') { - defaultReaderClosedPromiseInitializeAsResolved(reader); + reader._closedPromise = promiseResolvedWith(undefined); } else { assert(stream._state === 'errored'); - defaultReaderClosedPromiseInitializeAsRejected(reader, stream._storedError); + reader._closedPromise = promiseRejectedWith(stream._storedError); setPromiseIsHandledToTrue(reader._closedPromise); } } -// A client of ReadableStreamDefaultReader and ReadableStreamBYOBReader may use these functions directly to bypass state -// check. - -function ReadableStreamReaderGenericCancel(reader, reason) { - const stream = reader._ownerReadableStream; - assert(stream !== undefined); - return ReadableStreamCancel(stream, reason); -} - function ReadableStreamReaderGenericRelease(reader) { assert(reader._ownerReadableStream !== undefined); assert(reader._ownerReadableStream._reader === reader); if (reader._ownerReadableStream._state === 'readable') { - defaultReaderClosedPromiseReject( - reader, - new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness')); + rejectPromise( + reader._closedPromise, + new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness') + ); } else { - defaultReaderClosedPromiseResetToRejected( - reader, - new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness')); + reader._closedPromise = promiseRejectedWith( + new TypeError('Reader was released and can no longer be used to monitor the stream\'s closedness') + ); } setPromiseIsHandledToTrue(reader._closedPromise); @@ -1044,7 +626,6 @@ function ReadableStreamBYOBReaderRead(reader, view) { return promiseRejectedWith(stream._storedError); } - // Controllers must implement this. return ReadableByteStreamControllerPullInto(stream._readableStreamController, view); } @@ -1068,110 +649,46 @@ function ReadableStreamDefaultReaderRead(reader) { return stream._readableStreamController[PullSteps](); } -// Controllers - -class ReadableStreamDefaultController { - constructor() { - throw new TypeError(); +function SetUpReadableStreamBYOBReader(reader, stream) { + if (IsReadableStreamLocked(stream) === true) { + throw new TypeError('This stream has already been locked for exclusive reading by another reader'); } - get desiredSize() { - if (IsReadableStreamDefaultController(this) === false) { - throw defaultControllerBrandCheckException('desiredSize'); - } - - return ReadableStreamDefaultControllerGetDesiredSize(this); + if (!(stream._readableStreamController instanceof ReadableByteStreamControllerImpl.implementation)) { + throw new TypeError('Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source'); } - close() { - if (IsReadableStreamDefaultController(this) === false) { - throw defaultControllerBrandCheckException('close'); - } + ReadableStreamReaderGenericInitialize(reader, stream); - if (ReadableStreamDefaultControllerCanCloseOrEnqueue(this) === false) { - throw new TypeError('The stream is not in a state that permits close'); - } + reader._readIntoRequests = []; +} - ReadableStreamDefaultControllerClose(this); +function SetUpReadableStreamDefaultReader(reader, stream) { + if (IsReadableStreamLocked(stream) === true) { + throw new TypeError('This stream has already been locked for exclusive reading by another reader'); } - enqueue(chunk) { - if (IsReadableStreamDefaultController(this) === false) { - throw defaultControllerBrandCheckException('enqueue'); - } - - if (ReadableStreamDefaultControllerCanCloseOrEnqueue(this) === false) { - throw new TypeError('The stream is not in a state that permits enqueue'); - } + ReadableStreamReaderGenericInitialize(reader, stream); - return ReadableStreamDefaultControllerEnqueue(this, chunk); - } + reader._readRequests = []; +} - error(e) { - if (IsReadableStreamDefaultController(this) === false) { - throw defaultControllerBrandCheckException('error'); - } +// Default controllers - ReadableStreamDefaultControllerError(this, e); +function ReadableStreamDefaultControllerCallPullIfNeeded(controller) { + const shouldPull = ReadableStreamDefaultControllerShouldCallPull(controller); + if (shouldPull === false) { + return; } - [CancelSteps](reason) { - ResetQueue(this); - const result = this._cancelAlgorithm(reason); - ReadableStreamDefaultControllerClearAlgorithms(this); - return result; + if (controller._pulling === true) { + controller._pullAgain = true; + return; } - [PullSteps]() { - const stream = this._controlledReadableStream; + assert(controller._pullAgain === false); - if (this._queue.length > 0) { - const chunk = DequeueValue(this); - - if (this._closeRequested === true && this._queue.length === 0) { - ReadableStreamDefaultControllerClearAlgorithms(this); - ReadableStreamClose(stream); - } else { - ReadableStreamDefaultControllerCallPullIfNeeded(this); - } - - return promiseResolvedWith(ReadableStreamCreateReadResult(chunk, false, stream._reader._forAuthorCode)); - } - - const pendingPromise = ReadableStreamAddReadRequest(stream); - ReadableStreamDefaultControllerCallPullIfNeeded(this); - return pendingPromise; - } -} - -// Abstract operations for the ReadableStreamDefaultController. - -function IsReadableStreamDefaultController(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_controlledReadableStream')) { - return false; - } - - return true; -} - -function ReadableStreamDefaultControllerCallPullIfNeeded(controller) { - const shouldPull = ReadableStreamDefaultControllerShouldCallPull(controller); - if (shouldPull === false) { - return; - } - - if (controller._pulling === true) { - controller._pullAgain = true; - return; - } - - assert(controller._pullAgain === false); - - controller._pulling = true; + controller._pulling = true; const pullPromise = controller._pullAlgorithm(); uponPromise( @@ -1291,7 +808,6 @@ function ReadableStreamDefaultControllerGetDesiredSize(controller) { return controller._strategyHWM - controller._queueTotalSize; } -// This is used in the implementation of TransformStream. function ReadableStreamDefaultControllerHasBackpressure(controller) { if (ReadableStreamDefaultControllerShouldCallPull(controller) === true) { return false; @@ -1316,6 +832,7 @@ function SetUpReadableStreamDefaultController( controller._controlledReadableStream = stream; + // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly. controller._queue = undefined; controller._queueTotalSize = undefined; ResetQueue(controller); @@ -1350,245 +867,32 @@ function SetUpReadableStreamDefaultController( ); } -function SetUpReadableStreamDefaultControllerFromUnderlyingSource(stream, underlyingSource, highWaterMark, - sizeAlgorithm) { +function SetUpReadableStreamDefaultControllerFromUnderlyingSource( + stream, underlyingSource, underlyingSourceDict, highWaterMark, sizeAlgorithm) { assert(underlyingSource !== undefined); - const controller = Object.create(ReadableStreamDefaultController.prototype); - - function startAlgorithm() { - return InvokeOrNoop(underlyingSource, 'start', [controller]); - } - - const pullAlgorithm = CreateAlgorithmFromUnderlyingMethod(underlyingSource, 'pull', 0, [controller]); - const cancelAlgorithm = CreateAlgorithmFromUnderlyingMethod(underlyingSource, 'cancel', 1, []); + const controller = webidlNew(globalThis, 'ReadableStreamDefaultController', ReadableStreamDefaultControllerImpl); - SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, - highWaterMark, sizeAlgorithm); -} + let startAlgorithm = () => undefined; + let pullAlgorithm = () => promiseResolvedWith(undefined); + let cancelAlgorithm = () => promiseResolvedWith(undefined); -class ReadableStreamBYOBRequest { - constructor() { - throw new TypeError('ReadableStreamBYOBRequest cannot be used directly'); + if ('start' in underlyingSourceDict) { + startAlgorithm = () => invoke(underlyingSourceDict.start, [controller], underlyingSource); } - - get view() { - if (IsReadableStreamBYOBRequest(this) === false) { - throw byobRequestBrandCheckException('view'); - } - - return this._view; + if ('pull' in underlyingSourceDict) { + pullAlgorithm = () => promiseInvoke(underlyingSourceDict.pull, [controller], underlyingSource); } - - respond(bytesWritten) { - if (IsReadableStreamBYOBRequest(this) === false) { - throw byobRequestBrandCheckException('respond'); - } - - if (this._associatedReadableByteStreamController === undefined) { - throw new TypeError('This BYOB request has been invalidated'); - } - - if (IsDetachedBuffer(this._view.buffer) === true) { - throw new TypeError('The BYOB request\'s buffer has been detached and so cannot be used as a response'); - } - - ReadableByteStreamControllerRespond(this._associatedReadableByteStreamController, bytesWritten); + if ('cancel' in underlyingSourceDict) { + cancelAlgorithm = reason => promiseInvoke(underlyingSourceDict.cancel, [reason], underlyingSource); } - respondWithNewView(view) { - if (IsReadableStreamBYOBRequest(this) === false) { - throw byobRequestBrandCheckException('respond'); - } - - if (this._associatedReadableByteStreamController === undefined) { - throw new TypeError('This BYOB request has been invalidated'); - } - - if (!ArrayBuffer.isView(view)) { - throw new TypeError('You can only respond with array buffer views'); - } - - if (IsDetachedBuffer(view.buffer) === true) { - throw new TypeError('The supplied view\'s buffer has been detached and so cannot be used as a response'); - } - - ReadableByteStreamControllerRespondWithNewView(this._associatedReadableByteStreamController, view); - } -} - -class ReadableByteStreamController { - constructor() { - throw new TypeError('ReadableByteStreamController constructor cannot be used directly'); - } - - get byobRequest() { - if (IsReadableByteStreamController(this) === false) { - throw byteStreamControllerBrandCheckException('byobRequest'); - } - - if (this._byobRequest === undefined && this._pendingPullIntos.length > 0) { - const firstDescriptor = this._pendingPullIntos[0]; - const view = new Uint8Array(firstDescriptor.buffer, - firstDescriptor.byteOffset + firstDescriptor.bytesFilled, - firstDescriptor.byteLength - firstDescriptor.bytesFilled); - - const byobRequest = Object.create(ReadableStreamBYOBRequest.prototype); - SetUpReadableStreamBYOBRequest(byobRequest, this, view); - this._byobRequest = byobRequest; - } - - return this._byobRequest; - } - - get desiredSize() { - if (IsReadableByteStreamController(this) === false) { - throw byteStreamControllerBrandCheckException('desiredSize'); - } - - return ReadableByteStreamControllerGetDesiredSize(this); - } - - close() { - if (IsReadableByteStreamController(this) === false) { - throw byteStreamControllerBrandCheckException('close'); - } - - if (this._closeRequested === true) { - throw new TypeError('The stream has already been closed; do not close it again!'); - } - - const state = this._controlledReadableByteStream._state; - if (state !== 'readable') { - throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`); - } - - ReadableByteStreamControllerClose(this); - } - - enqueue(chunk) { - if (IsReadableByteStreamController(this) === false) { - throw byteStreamControllerBrandCheckException('enqueue'); - } - - if (this._closeRequested === true) { - throw new TypeError('stream is closed or draining'); - } - - const state = this._controlledReadableByteStream._state; - if (state !== 'readable') { - throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`); - } - - if (!ArrayBuffer.isView(chunk)) { - throw new TypeError('You can only enqueue array buffer views when using a ReadableByteStreamController'); - } - - if (IsDetachedBuffer(chunk.buffer) === true) { - throw new TypeError('Cannot enqueue a view onto a detached ArrayBuffer'); - } - - ReadableByteStreamControllerEnqueue(this, chunk); - } - - error(e) { - if (IsReadableByteStreamController(this) === false) { - throw byteStreamControllerBrandCheckException('error'); - } - - ReadableByteStreamControllerError(this, e); - } - - [CancelSteps](reason) { - if (this._pendingPullIntos.length > 0) { - const firstDescriptor = this._pendingPullIntos[0]; - firstDescriptor.bytesFilled = 0; - } - - ResetQueue(this); - - const result = this._cancelAlgorithm(reason); - ReadableByteStreamControllerClearAlgorithms(this); - return result; - } - - [PullSteps]() { - const stream = this._controlledReadableByteStream; - assert(ReadableStreamHasDefaultReader(stream) === true); - - if (this._queueTotalSize > 0) { - assert(ReadableStreamGetNumReadRequests(stream) === 0); - - const entry = this._queue.shift(); - this._queueTotalSize -= entry.byteLength; - - ReadableByteStreamControllerHandleQueueDrain(this); - - let view; - try { - view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength); - } catch (viewE) { - return promiseRejectedWith(viewE); - } - - return promiseResolvedWith(ReadableStreamCreateReadResult(view, false, stream._reader._forAuthorCode)); - } - - const autoAllocateChunkSize = this._autoAllocateChunkSize; - if (autoAllocateChunkSize !== undefined) { - let buffer; - try { - buffer = new ArrayBuffer(autoAllocateChunkSize); - } catch (bufferE) { - return promiseRejectedWith(bufferE); - } - - const pullIntoDescriptor = { - buffer, - byteOffset: 0, - byteLength: autoAllocateChunkSize, - bytesFilled: 0, - elementSize: 1, - ctor: Uint8Array, - readerType: 'default' - }; - - this._pendingPullIntos.push(pullIntoDescriptor); - } - - const promise = ReadableStreamAddReadRequest(stream); - - ReadableByteStreamControllerCallPullIfNeeded(this); - - return promise; - } -} - -// Abstract operations for the ReadableByteStreamController. - -function IsReadableByteStreamController(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_controlledReadableByteStream')) { - return false; - } - - return true; + SetUpReadableStreamDefaultController( + stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm + ); } -function IsReadableStreamBYOBRequest(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_associatedReadableByteStreamController')) { - return false; - } - - return true; -} +// Byte stream controllers function ReadableByteStreamControllerCallPullIfNeeded(controller) { const shouldPull = ReadableByteStreamControllerShouldCallPull(controller); @@ -1623,11 +927,43 @@ function ReadableByteStreamControllerCallPullIfNeeded(controller) { ); } +function ReadableByteStreamControllerClearAlgorithms(controller) { + controller._pullAlgorithm = undefined; + controller._cancelAlgorithm = undefined; +} + function ReadableByteStreamControllerClearPendingPullIntos(controller) { ReadableByteStreamControllerInvalidateBYOBRequest(controller); controller._pendingPullIntos = []; } +function ReadableByteStreamControllerClose(controller) { + const stream = controller._controlledReadableStream; + + if (controller._closeRequest === true || stream._state !== 'readable') { + return; + } + + if (controller._queueTotalSize > 0) { + controller._closeRequested = true; + + return; + } + + if (controller._pendingPullIntos.length > 0) { + const firstPendingPullInto = controller._pendingPullIntos[0]; + if (firstPendingPullInto.bytesFilled > 0) { + const e = new TypeError('Insufficient bytes to fill elements in the given buffer'); + ReadableByteStreamControllerError(controller, e); + + throw e; + } + } + + ReadableByteStreamControllerClearAlgorithms(controller); + ReadableStreamClose(stream); +} + function ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor) { assert(stream._state !== 'errored'); @@ -1657,11 +993,65 @@ function ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescripto pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, bytesFilled / elementSize); } +function ReadableByteStreamControllerEnqueue(controller, chunk) { + const stream = controller._controlledReadableStream; + + if (controller._closeRequest === true || stream._state !== 'readable') { + return; + } + + const buffer = chunk.buffer; + const byteOffset = chunk.byteOffset; + const byteLength = chunk.byteLength; + const transferredBuffer = TransferArrayBuffer(buffer); + + if (ReadableStreamHasDefaultReader(stream) === true) { + if (ReadableStreamGetNumReadRequests(stream) === 0) { + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + } else { + assert(controller._queue.length === 0); + + const transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength); + ReadableStreamFulfillReadRequest(stream, transferredView, false); + } + } else if (ReadableStreamHasBYOBReader(stream) === true) { + // TODO: Ideally in this branch detaching should happen only if the buffer is not consumed fully. + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); + } else { + assert(IsReadableStreamLocked(stream) === false); + ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); + } + + ReadableByteStreamControllerCallPullIfNeeded(controller); +} + function ReadableByteStreamControllerEnqueueChunkToQueue(controller, buffer, byteOffset, byteLength) { controller._queue.push({ buffer, byteOffset, byteLength }); controller._queueTotalSize += byteLength; } +function ReadableByteStreamControllerError(controller, e) { + const stream = controller._controlledReadableStream; + + if (stream._state !== 'readable') { + return; + } + + ReadableByteStreamControllerClearPendingPullIntos(controller); + + ResetQueue(controller); + ReadableByteStreamControllerClearAlgorithms(controller); + ReadableStreamError(stream, e); +} + +function ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, size, pullIntoDescriptor) { + assert(controller._pendingPullIntos.length === 0 || controller._pendingPullIntos[0] === pullIntoDescriptor); + + ReadableByteStreamControllerInvalidateBYOBRequest(controller); + pullIntoDescriptor.bytesFilled += size; +} + function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) { const elementSize = pullIntoDescriptor.elementSize; @@ -1687,7 +1077,7 @@ function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, const bytesToCopy = Math.min(totalBytesToCopyRemaining, headOfQueue.byteLength); const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; - ArrayBufferCopy(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy); + CopyDataBlockBytes(pullIntoDescriptor.buffer, destStart, headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy); if (headOfQueue.byteLength === bytesToCopy) { queue.shift(); @@ -1711,19 +1101,26 @@ function ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, return ready; } -function ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, size, pullIntoDescriptor) { - assert(controller._pendingPullIntos.length === 0 || controller._pendingPullIntos[0] === pullIntoDescriptor); +function ReadableByteStreamControllerGetDesiredSize(controller) { + const stream = controller._controlledReadableStream; + const state = stream._state; - ReadableByteStreamControllerInvalidateBYOBRequest(controller); - pullIntoDescriptor.bytesFilled += size; + if (state === 'errored') { + return null; + } + if (state === 'closed') { + return 0; + } + + return controller._strategyHWM - controller._queueTotalSize; } function ReadableByteStreamControllerHandleQueueDrain(controller) { - assert(controller._controlledReadableByteStream._state === 'readable'); + assert(controller._controlledReadableStream._state === 'readable'); if (controller._queueTotalSize === 0 && controller._closeRequested === true) { ReadableByteStreamControllerClearAlgorithms(controller); - ReadableStreamClose(controller._controlledReadableByteStream); + ReadableStreamClose(controller._controlledReadableStream); } else { ReadableByteStreamControllerCallPullIfNeeded(controller); } @@ -1734,7 +1131,7 @@ function ReadableByteStreamControllerInvalidateBYOBRequest(controller) { return; } - controller._byobRequest._associatedReadableByteStreamController = undefined; + controller._byobRequest._controller = undefined; controller._byobRequest._view = undefined; controller._byobRequest = undefined; } @@ -1753,7 +1150,7 @@ function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(contro ReadableByteStreamControllerShiftPendingPullInto(controller); ReadableByteStreamControllerCommitPullIntoDescriptor( - controller._controlledReadableByteStream, + controller._controlledReadableStream, pullIntoDescriptor ); } @@ -1761,7 +1158,7 @@ function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(contro } function ReadableByteStreamControllerPullInto(controller, view) { - const stream = controller._controlledReadableByteStream; + const stream = controller._controlledReadableStream; let elementSize = 1; if (view.constructor !== DataView) { @@ -1822,12 +1219,18 @@ function ReadableByteStreamControllerPullInto(controller, view) { return promise; } +function ReadableByteStreamControllerRespond(controller, bytesWritten) { + assert(controller._pendingPullIntos.length > 0); + + ReadableByteStreamControllerRespondInternal(controller, bytesWritten); +} + function ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor) { firstDescriptor.buffer = TransferArrayBuffer(firstDescriptor.buffer); assert(firstDescriptor.bytesFilled === 0); - const stream = controller._controlledReadableByteStream; + const stream = controller._controlledReadableStream; if (ReadableStreamHasBYOBReader(stream) === true) { while (ReadableStreamGetNumReadIntoRequests(stream) > 0) { const pullIntoDescriptor = ReadableByteStreamControllerShiftPendingPullInto(controller); @@ -1859,7 +1262,7 @@ function ReadableByteStreamControllerRespondInReadableState(controller, bytesWri pullIntoDescriptor.buffer = TransferArrayBuffer(pullIntoDescriptor.buffer); pullIntoDescriptor.bytesFilled -= remainderSize; - ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableByteStream, pullIntoDescriptor); + ReadableByteStreamControllerCommitPullIntoDescriptor(controller._controlledReadableStream, pullIntoDescriptor); ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); } @@ -1867,7 +1270,7 @@ function ReadableByteStreamControllerRespondInReadableState(controller, bytesWri function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) { const firstDescriptor = controller._pendingPullIntos[0]; - const stream = controller._controlledReadableByteStream; + const stream = controller._controlledReadableStream; if (stream._state === 'closed') { if (bytesWritten !== 0) { @@ -1884,6 +1287,23 @@ function ReadableByteStreamControllerRespondInternal(controller, bytesWritten) { ReadableByteStreamControllerCallPullIfNeeded(controller); } +function ReadableByteStreamControllerRespondWithNewView(controller, view) { + assert(controller._pendingPullIntos.length > 0); + + const firstDescriptor = controller._pendingPullIntos[0]; + + if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) { + throw new RangeError('The region specified by view does not match byobRequest'); + } + if (firstDescriptor.byteLength !== view.byteLength) { + throw new RangeError('The buffer of view has different capacity than byobRequest'); + } + + firstDescriptor.buffer = view.buffer; + + ReadableByteStreamControllerRespondInternal(controller, view.byteLength); +} + function ReadableByteStreamControllerShiftPendingPullInto(controller) { const descriptor = controller._pendingPullIntos.shift(); ReadableByteStreamControllerInvalidateBYOBRequest(controller); @@ -1891,7 +1311,7 @@ function ReadableByteStreamControllerShiftPendingPullInto(controller) { } function ReadableByteStreamControllerShouldCallPull(controller) { - const stream = controller._controlledReadableByteStream; + const stream = controller._controlledReadableStream; if (stream._state !== 'readable') { return false; @@ -1922,127 +1342,6 @@ function ReadableByteStreamControllerShouldCallPull(controller) { return false; } -function ReadableByteStreamControllerClearAlgorithms(controller) { - controller._pullAlgorithm = undefined; - controller._cancelAlgorithm = undefined; -} - -function ReadableByteStreamControllerClose(controller) { - const stream = controller._controlledReadableByteStream; - - if (controller._closeRequest === true || stream._state !== 'readable') { - return; - } - - if (controller._queueTotalSize > 0) { - controller._closeRequested = true; - - return; - } - - if (controller._pendingPullIntos.length > 0) { - const firstPendingPullInto = controller._pendingPullIntos[0]; - if (firstPendingPullInto.bytesFilled > 0) { - const e = new TypeError('Insufficient bytes to fill elements in the given buffer'); - ReadableByteStreamControllerError(controller, e); - - throw e; - } - } - - ReadableByteStreamControllerClearAlgorithms(controller); - ReadableStreamClose(stream); -} - -function ReadableByteStreamControllerEnqueue(controller, chunk) { - const stream = controller._controlledReadableByteStream; - - if (controller._closeRequest === true || stream._state !== 'readable') { - return; - } - - const buffer = chunk.buffer; - const byteOffset = chunk.byteOffset; - const byteLength = chunk.byteLength; - const transferredBuffer = TransferArrayBuffer(buffer); - - if (ReadableStreamHasDefaultReader(stream) === true) { - if (ReadableStreamGetNumReadRequests(stream) === 0) { - ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); - } else { - assert(controller._queue.length === 0); - - const transferredView = new Uint8Array(transferredBuffer, byteOffset, byteLength); - ReadableStreamFulfillReadRequest(stream, transferredView, false); - } - } else if (ReadableStreamHasBYOBReader(stream) === true) { - // TODO: Ideally in this branch detaching should happen only if the buffer is not consumed fully. - ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); - ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller); - } else { - assert(IsReadableStreamLocked(stream) === false); - ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength); - } - - ReadableByteStreamControllerCallPullIfNeeded(controller); -} - -function ReadableByteStreamControllerError(controller, e) { - const stream = controller._controlledReadableByteStream; - - if (stream._state !== 'readable') { - return; - } - - ReadableByteStreamControllerClearPendingPullIntos(controller); - - ResetQueue(controller); - ReadableByteStreamControllerClearAlgorithms(controller); - ReadableStreamError(stream, e); -} - -function ReadableByteStreamControllerGetDesiredSize(controller) { - const stream = controller._controlledReadableByteStream; - const state = stream._state; - - if (state === 'errored') { - return null; - } - if (state === 'closed') { - return 0; - } - - return controller._strategyHWM - controller._queueTotalSize; -} - -function ReadableByteStreamControllerRespond(controller, bytesWritten) { - bytesWritten = Number(bytesWritten); - if (IsFiniteNonNegativeNumber(bytesWritten) === false) { - throw new RangeError('bytesWritten must be a finite'); - } - - assert(controller._pendingPullIntos.length > 0); - - ReadableByteStreamControllerRespondInternal(controller, bytesWritten); -} - -function ReadableByteStreamControllerRespondWithNewView(controller, view) { - assert(controller._pendingPullIntos.length > 0); - - const firstDescriptor = controller._pendingPullIntos[0]; - - if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset) { - throw new RangeError('The region specified by view does not match byobRequest'); - } - if (firstDescriptor.byteLength !== view.byteLength) { - throw new RangeError('The buffer of view has different capacity than byobRequest'); - } - - firstDescriptor.buffer = view.buffer; - - ReadableByteStreamControllerRespondInternal(controller, view.byteLength); -} - function SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize) { assert(stream._readableStreamController === undefined); @@ -2051,7 +1350,7 @@ function SetUpReadableByteStreamController(stream, controller, startAlgorithm, p assert(autoAllocateChunkSize > 0); } - controller._controlledReadableByteStream = stream; + controller._controlledReadableStream = stream; controller._pullAgain = false; controller._pulling = false; @@ -2065,7 +1364,7 @@ function SetUpReadableByteStreamController(stream, controller, startAlgorithm, p controller._closeRequested = false; controller._started = false; - controller._strategyHWM = ValidateAndNormalizeHighWaterMark(highWaterMark); + controller._strategyHWM = highWaterMark; controller._pullAlgorithm = pullAlgorithm; controller._cancelAlgorithm = cancelAlgorithm; @@ -2093,145 +1392,27 @@ function SetUpReadableByteStreamController(stream, controller, startAlgorithm, p ); } -function SetUpReadableByteStreamControllerFromUnderlyingSource(stream, underlyingByteSource, highWaterMark) { - assert(underlyingByteSource !== undefined); +function SetUpReadableByteStreamControllerFromUnderlyingSource( + stream, underlyingSource, underlyingSourceDict, highWaterMark) { + const controller = webidlNew(globalThis, 'ReadableByteStreamController', ReadableByteStreamControllerImpl); - const controller = Object.create(ReadableByteStreamController.prototype); + let startAlgorithm = () => undefined; + let pullAlgorithm = () => promiseResolvedWith(undefined); + let cancelAlgorithm = () => promiseResolvedWith(undefined); - function startAlgorithm() { - return InvokeOrNoop(underlyingByteSource, 'start', [controller]); + if ('start' in underlyingSourceDict) { + startAlgorithm = () => invoke(underlyingSourceDict.start, [controller], underlyingSource); } - - const pullAlgorithm = CreateAlgorithmFromUnderlyingMethod(underlyingByteSource, 'pull', 0, [controller]); - const cancelAlgorithm = CreateAlgorithmFromUnderlyingMethod(underlyingByteSource, 'cancel', 1, []); - - let autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize; - if (autoAllocateChunkSize !== undefined) { - autoAllocateChunkSize = Number(autoAllocateChunkSize); - if (Number.isInteger(autoAllocateChunkSize) === false || autoAllocateChunkSize <= 0) { - throw new RangeError('autoAllocateChunkSize must be a positive integer'); - } + if ('pull' in underlyingSourceDict) { + pullAlgorithm = () => promiseInvoke(underlyingSourceDict.pull, [controller], underlyingSource); } - - SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, - autoAllocateChunkSize); -} - -function SetUpReadableStreamBYOBRequest(request, controller, view) { - assert(IsReadableByteStreamController(controller) === true); - assert(typeof view === 'object'); - assert(ArrayBuffer.isView(view) === true); - assert(IsDetachedBuffer(view.buffer) === false); - request._associatedReadableByteStreamController = controller; - request._view = view; -} - -// Helper functions for the ReadableStream. - -function isAbortSignal(value) { - if (typeof value !== 'object' || value === null) { - return false; + if ('cancel' in underlyingSourceDict) { + cancelAlgorithm = reason => promiseInvoke(underlyingSourceDict.cancel, [reason], underlyingSource); } - // Use the brand check to distinguish a real AbortSignal from a fake one. - const aborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted').get; - try { - aborted.call(value); - return true; - } catch (e) { - return false; - } -} - -function streamBrandCheckException(name) { - return new TypeError(`ReadableStream.prototype.${name} can only be used on a ReadableStream`); -} - -function streamAsyncIteratorBrandCheckException(name) { - return new TypeError(`ReadableStreamAsyncIterator.${name} can only be used on a ReadableSteamAsyncIterator`); -} - -// Helper functions for the readers. - -function readerLockException(name) { - return new TypeError('Cannot ' + name + ' a stream using a released reader'); -} - -// Helper functions for the ReadableStreamDefaultReader. - -function defaultReaderBrandCheckException(name) { - return new TypeError( - `ReadableStreamDefaultReader.prototype.${name} can only be used on a ReadableStreamDefaultReader`); -} - -function defaultReaderClosedPromiseInitialize(reader) { - reader._closedPromise = newPromise((resolve, reject) => { - reader._closedPromise_resolve = resolve; - reader._closedPromise_reject = reject; - }); -} - -function defaultReaderClosedPromiseInitializeAsRejected(reader, reason) { - reader._closedPromise = promiseRejectedWith(reason); - reader._closedPromise_resolve = undefined; - reader._closedPromise_reject = undefined; -} - -function defaultReaderClosedPromiseInitializeAsResolved(reader) { - reader._closedPromise = promiseResolvedWith(undefined); - reader._closedPromise_resolve = undefined; - reader._closedPromise_reject = undefined; -} - -function defaultReaderClosedPromiseReject(reader, reason) { - assert(reader._closedPromise_resolve !== undefined); - assert(reader._closedPromise_reject !== undefined); - - reader._closedPromise_reject(reason); - reader._closedPromise_resolve = undefined; - reader._closedPromise_reject = undefined; -} - -function defaultReaderClosedPromiseResetToRejected(reader, reason) { - assert(reader._closedPromise_resolve === undefined); - assert(reader._closedPromise_reject === undefined); - - reader._closedPromise = promiseRejectedWith(reason); -} - -function defaultReaderClosedPromiseResolve(reader) { - assert(reader._closedPromise_resolve !== undefined); - assert(reader._closedPromise_reject !== undefined); + const autoAllocateChunkSize = underlyingSourceDict.autoAllocateChunkSize; - reader._closedPromise_resolve(undefined); - reader._closedPromise_resolve = undefined; - reader._closedPromise_reject = undefined; -} - -// Helper functions for the ReadableStreamDefaultReader. - -function byobReaderBrandCheckException(name) { - return new TypeError( - `ReadableStreamBYOBReader.prototype.${name} can only be used on a ReadableStreamBYOBReader`); -} - -// Helper functions for the ReadableStreamDefaultController. - -function defaultControllerBrandCheckException(name) { - return new TypeError( - `ReadableStreamDefaultController.prototype.${name} can only be used on a ReadableStreamDefaultController`); -} - -// Helper functions for the ReadableStreamBYOBRequest. - -function byobRequestBrandCheckException(name) { - return new TypeError( - `ReadableStreamBYOBRequest.prototype.${name} can only be used on a ReadableStreamBYOBRequest`); -} - -// Helper functions for the ReadableByteStreamController. - -function byteStreamControllerBrandCheckException(name) { - return new TypeError( - `ReadableByteStreamController.prototype.${name} can only be used on a ReadableByteStreamController`); + SetUpReadableByteStreamController( + stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize + ); } diff --git a/reference-implementation/lib/abstract-ops/writable-streams.js b/reference-implementation/lib/abstract-ops/writable-streams.js index faa5de021..8751879fe 100644 --- a/reference-implementation/lib/abstract-ops/writable-streams.js +++ b/reference-implementation/lib/abstract-ops/writable-streams.js @@ -3,7 +3,7 @@ const assert = require('assert'); const verbose = require('debug')('streams:writable-stream:verbose'); const { webidlNew, promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, - rejectPromise, uponPromise, setPromiseIsHandledToTrue, stateIsPending } = require('../webidl-helpers.js'); + rejectPromise, uponPromise, setPromiseIsHandledToTrue, stateIsPending } = require('../helpers/webidl.js'); const { IsNonNegativeNumber } = require('./miscellaneous.js'); const { DequeueValue, EnqueueValueWithSize, PeekQueueValue, ResetQueue } = require('./queue-with-sizes.js'); const { AbortSteps, ErrorSteps } = require('./internal-methods.js'); @@ -14,6 +14,7 @@ const WritableStreamDefaultWriterImpl = require('../WritableStreamDefaultWriter- Object.assign(exports, { AcquireWritableStreamDefaultWriter, + CreateWritableStream, InitializeWritableStream, IsWritableStreamLocked, SetUpWritableStreamDefaultControllerFromUnderlyingSink, @@ -25,6 +26,7 @@ Object.assign(exports, { WritableStreamDefaultControllerError, WritableStreamDefaultWriterAbort, WritableStreamDefaultWriterClose, + WritableStreamDefaultWriterCloseWithErrorPropagation, WritableStreamDefaultWriterGetDesiredSize, WritableStreamDefaultWriterRelease, WritableStreamDefaultWriterWrite @@ -585,8 +587,9 @@ function SetUpWritableStreamDefaultControllerFromUnderlyingSink(stream, underlyi abortAlgorithm = reason => promiseInvoke(underlyingSinkDict.abort, [reason], underlyingSink); } - SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, - abortAlgorithm, highWaterMark, sizeAlgorithm); + SetUpWritableStreamDefaultController( + stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm + ); } function WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller) { diff --git a/reference-implementation/lib/helpers/miscellaneous.js b/reference-implementation/lib/helpers/miscellaneous.js new file mode 100644 index 000000000..a042f33cf --- /dev/null +++ b/reference-implementation/lib/helpers/miscellaneous.js @@ -0,0 +1,15 @@ +'use strict'; +const assert = require('assert'); + +exports.typeIsObject = x => (typeof x === 'object' && x !== null) || typeof x === 'function'; + +exports.rethrowAssertionErrorRejection = e => { + // Used throughout the reference implementation, as `.catch(rethrowAssertionErrorRejection)`, to ensure any errors + // get shown. There are places in the spec where we do promise transformations and purposefully ignore or don't + // expect any errors, but assertion errors are always problematic. + if (e && e instanceof assert.AssertionError) { + setTimeout(() => { + throw e; + }, 0); + } +}; diff --git a/reference-implementation/lib/webidl-helpers.js b/reference-implementation/lib/helpers/webidl.js similarity index 94% rename from reference-implementation/lib/webidl-helpers.js rename to reference-implementation/lib/helpers/webidl.js index 1c88b8e4a..796aa4d1d 100644 --- a/reference-implementation/lib/webidl-helpers.js +++ b/reference-implementation/lib/helpers/webidl.js @@ -1,6 +1,6 @@ 'use strict'; -const utils = require('../generated/utils.js'); -const { rethrowAssertionErrorRejection } = require('./helpers.js'); +const utils = require('../../generated/utils.js'); +const { rethrowAssertionErrorRejection } = require('./miscellaneous.js'); // https://heycam.github.io/webidl/#new // https://github.com/jsdom/webidl2js/issues/193 @@ -138,7 +138,7 @@ Object.assign(exports, { }); // https://heycam.github.io/webidl/#wait-for-all -exports.WaitForAll = (promises, successSteps, failureSteps) => { +function waitForAll(promises, successSteps, failureSteps) { let rejected = false; const rejectionHandler = arg => { if (rejected === false) { @@ -166,10 +166,10 @@ exports.WaitForAll = (promises, successSteps, failureSteps) => { PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler); ++index; } -}; +} // https://heycam.github.io/webidl/#waiting-for-all-promise -exports.WaitForAllPromise = (promises, successSteps, failureSteps = undefined) => { +exports.waitForAllPromise = (promises, successSteps, failureSteps = undefined) => { let resolveP; let rejectP; const promise = newPromise((resolve, reject) => { @@ -197,6 +197,6 @@ exports.WaitForAllPromise = (promises, successSteps, failureSteps = undefined) = rejectP(e); } }; - exports.WaitForAll(promises, successStepsWrapper, failureStepsWrapper); + waitForAll(promises, successStepsWrapper, failureStepsWrapper); return promise; }; diff --git a/reference-implementation/lib/index.js b/reference-implementation/lib/index.js index 4b2a57ab1..d60616562 100644 --- a/reference-implementation/lib/index.js +++ b/reference-implementation/lib/index.js @@ -2,15 +2,21 @@ // This file is used as the entry point for browserifying the reference implementation to allow it // to run inside the wpt-runner "browser like" context. -// const { ReadableStream } = require('./readable-stream.js'); // const { TransformStream } = require('./transform-stream.js'); -// window.ReadableStream = ReadableStream; // window.TransformStream = TransformStream; window.gc = gc; require('../generated/ByteLengthQueuingStrategy.js').install(window); require('../generated/CountQueuingStrategy.js').install(window); + +require('../generated/ReadableStream.js').install(window); +require('../generated/ReadableStreamDefaultReader.js').install(window); +require('../generated/ReadableStreamBYOBReader.js').install(window); +require('../generated/ReadableStreamDefaultController.js').install(window); +require('../generated/ReadableByteStreamController.js').install(window); +require('../generated/ReadableStreamBYOBRequest.js').install(window); + require('../generated/WritableStream.js').install(window); -require('../generated/WritableStreamDefaultController.js').install(window); require('../generated/WritableStreamDefaultWriter.js').install(window); +require('../generated/WritableStreamDefaultController.js').install(window); From 8453d747a6f33faa09bb89192028d4774b5df041 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Fri, 17 Apr 2020 15:12:41 -0400 Subject: [PATCH 08/30] IDL and promise fixes --- .../lib/ReadableStream-impl.js | 22 ++++++++++++++++++- .../lib/abstract-ops/readable-streams.js | 6 ++--- .../lib/helpers/webidl.js | 2 +- reference-implementation/lib/index.js | 22 +++++++++---------- reference-implementation/package.json | 2 +- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/reference-implementation/lib/ReadableStream-impl.js b/reference-implementation/lib/ReadableStream-impl.js index c6487702f..826f2f785 100644 --- a/reference-implementation/lib/ReadableStream-impl.js +++ b/reference-implementation/lib/ReadableStream-impl.js @@ -6,6 +6,7 @@ const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/q const aos = require('./abstract-ops/readable-streams.js'); const wsAOs = require('./abstract-ops/writable-streams.js'); +const idlUtils = require('../generated/utils.js'); const UnderlyingSource = require('../generated/UnderlyingSource.js'); exports.implementation = class ReadableStreamImpl { @@ -75,6 +76,13 @@ exports.implementation = class ReadableStreamImpl { } pipeTo(destination, options) { + // Conversion here is needed until https://github.com/jsdom/webidl2js/issues/81 is fixed. + if ('signal' in options) { + if (!isAbortSignal(options.signal)) { + return promiseRejectedWith(new TypeError('Invalid signal argument')); + } + } + if (aos.IsReadableStreamLocked(this) === true) { return promiseRejectedWith( new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream') @@ -92,6 +100,18 @@ exports.implementation = class ReadableStreamImpl { } tee() { - return aos.ReadableStreamTee(this, false); + // Conversion here is only needed until https://github.com/jsdom/webidl2js/pull/108 gets merged. + return aos.ReadableStreamTee(this, false).map(idlUtils.wrapperForImpl); } }; + +// See pipeTo() for why this is needed. +const abortedGetter = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted').get; +function isAbortSignal(v) { + try { + abortedGetter.call(v); + return true; + } catch { + return false; + } +} diff --git a/reference-implementation/lib/abstract-ops/readable-streams.js b/reference-implementation/lib/abstract-ops/readable-streams.js index a7f035c9d..c35fef200 100644 --- a/reference-implementation/lib/abstract-ops/readable-streams.js +++ b/reference-implementation/lib/abstract-ops/readable-streams.js @@ -130,7 +130,7 @@ function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventC // This is used to keep track of the spec's requirement that we wait for ongoing writes during shutdown. let currentWrite = promiseResolvedWith(undefined); - return newPromise((resolve, reject) => { + return new Promise((resolve, reject) => { let abortAlgorithm; if (signal !== undefined) { abortAlgorithm = () => { @@ -167,7 +167,7 @@ function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventC // - Backpressure must be enforced // - Shutdown must stop all activity function pipeLoop() { - return newPromise((resolveLoop, rejectLoop) => { + return new Promise((resolveLoop, rejectLoop) => { function next(done) { if (done) { resolveLoop(); @@ -328,7 +328,7 @@ function ReadableStreamTee(stream, cloneForBranch2) { let branch2; let resolveCancelPromise; - const cancelPromise = newPromise(resolve => { + const cancelPromise = new Promise(resolve => { resolveCancelPromise = resolve; }); diff --git a/reference-implementation/lib/helpers/webidl.js b/reference-implementation/lib/helpers/webidl.js index 796aa4d1d..239b8a6fd 100644 --- a/reference-implementation/lib/helpers/webidl.js +++ b/reference-implementation/lib/helpers/webidl.js @@ -172,7 +172,7 @@ function waitForAll(promises, successSteps, failureSteps) { exports.waitForAllPromise = (promises, successSteps, failureSteps = undefined) => { let resolveP; let rejectP; - const promise = newPromise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { resolveP = resolve; rejectP = reject; }); diff --git a/reference-implementation/lib/index.js b/reference-implementation/lib/index.js index d60616562..b066f10a0 100644 --- a/reference-implementation/lib/index.js +++ b/reference-implementation/lib/index.js @@ -7,16 +7,16 @@ // window.TransformStream = TransformStream; window.gc = gc; -require('../generated/ByteLengthQueuingStrategy.js').install(window); -require('../generated/CountQueuingStrategy.js').install(window); +require('../generated/ByteLengthQueuingStrategy.js').install(window, ['Window']); +require('../generated/CountQueuingStrategy.js').install(window, ['Window']); -require('../generated/ReadableStream.js').install(window); -require('../generated/ReadableStreamDefaultReader.js').install(window); -require('../generated/ReadableStreamBYOBReader.js').install(window); -require('../generated/ReadableStreamDefaultController.js').install(window); -require('../generated/ReadableByteStreamController.js').install(window); -require('../generated/ReadableStreamBYOBRequest.js').install(window); +require('../generated/ReadableStream.js').install(window, ['Window']); +require('../generated/ReadableStreamDefaultReader.js').install(window, ['Window']); +require('../generated/ReadableStreamBYOBReader.js').install(window, ['Window']); +require('../generated/ReadableStreamDefaultController.js').install(window, ['Window']); +require('../generated/ReadableByteStreamController.js').install(window, ['Window']); +require('../generated/ReadableStreamBYOBRequest.js').install(window, ['Window']); -require('../generated/WritableStream.js').install(window); -require('../generated/WritableStreamDefaultWriter.js').install(window); -require('../generated/WritableStreamDefaultController.js').install(window); +require('../generated/WritableStream.js').install(window, ['Window']); +require('../generated/WritableStreamDefaultWriter.js').install(window, ['Window']); +require('../generated/WritableStreamDefaultController.js').install(window, ['Window']); diff --git a/reference-implementation/package.json b/reference-implementation/package.json index a7f59923a..b259192a2 100644 --- a/reference-implementation/package.json +++ b/reference-implementation/package.json @@ -18,7 +18,7 @@ "minimatch": "^3.0.4", "nyc": "^15.0.1", "opener": "^1.5.1", - "webidl2js": "^15.1.0", + "webidl2js": "^15.2.0", "wpt-runner": "^3.0.1" }, "nyc": { From 0758900fb60ef0078fab2b28ed2a8964a0a52b27 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Fri, 17 Apr 2020 16:13:58 -0400 Subject: [PATCH 09/30] Transform streams and more --- index.bs | 49 ++-- .../lib/ReadableByteStreamController-impl.js | 6 +- .../lib/ReadableStream-impl.js | 9 +- .../lib/TransformStream-impl.js | 48 ++++ .../lib/TransformStream.webidl | 9 + .../TransformStreamDefaultController-impl.js | 23 ++ .../TransformStreamDefaultController.webidl | 8 + .../lib/Transformer.webidl | 11 + .../lib/UnderlyingSink.webidl | 16 +- .../lib/UnderlyingSource.webidl | 14 +- .../lib/abstract-ops/readable-streams.js | 1 + .../transform-streams.js} | 245 +++--------------- .../lib/abstract-ops/writable-streams.js | 1 + .../lib/helpers/webidl.js | 3 +- reference-implementation/lib/index.js | 6 +- reference-implementation/web-platform-tests | 2 +- 16 files changed, 199 insertions(+), 252 deletions(-) create mode 100644 reference-implementation/lib/TransformStream-impl.js create mode 100644 reference-implementation/lib/TransformStream.webidl create mode 100644 reference-implementation/lib/TransformStreamDefaultController-impl.js create mode 100644 reference-implementation/lib/TransformStreamDefaultController.webidl create mode 100644 reference-implementation/lib/Transformer.webidl rename reference-implementation/lib/{transform-stream.js => abstract-ops/transform-streams.js} (51%) diff --git a/index.bs b/index.bs index 46df094a7..ff9da397a 100644 --- a/index.bs +++ b/index.bs @@ -528,18 +528,18 @@ the [=underlying source=]. Such objects can contain any of the following propert <xmp class="idl"> dictionary UnderlyingSource { - ReadableStreamStartCallback start; - ReadableStreamPullCallback pull; - ReadableStreamCancelCallback cancel; + UnderlyingSourceStartCallback start; + UnderlyingSourcePullCallback pull; + UnderlyingSourceCancelCallback cancel; ReadableStreamType type; [EnforceRange] unsigned long long autoAllocateChunkSize; }; typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController; -callback ReadableStreamStartCallback = any (ReadableStreamController controller); -callback ReadableStreamPullCallback = Promise<void> (ReadableStreamController controller); -callback ReadableStreamCancelCallback = Promise<void> (optional any reason); +callback UnderlyingSourceStartCallback = any (ReadableStreamController controller); +callback UnderlyingSourcePullCallback = Promise<void> (ReadableStreamController controller); +callback UnderlyingSourceCancelCallback = Promise<void> (optional any reason); enum ReadableStreamType { "bytes" }; @@ -3250,17 +3250,17 @@ the [=underlying sink=]. Such objects can contain any of the following propertie dictionary UnderlyingSink { - WritableStreamStartCallback start; - WritableStreamWriteCallback write; - WritableStreamCloseCallback close; - WritableStreamAbortCallback abort; + UnderlyingSinkStartCallback start; + UnderlyingSinkWriteCallback write; + UnderlyingSinkCloseCallback close; + UnderlyingSinkAbortCallback abort; any type; }; -callback WritableStreamStartCallback = any (WritableStreamDefaultController controller); -callback WritableStreamWriteCallback = Promise<void> (WritableStreamDefaultController controller, optional any chunk); -callback WritableStreamCloseCallback = Promise<void> (); -callback WritableStreamAbortCallback = Promise<void> (optional any reason); +callback UnderlyingSinkStartCallback = any (WritableStreamDefaultController controller); +callback UnderlyingSinkWriteCallback = Promise<void> (WritableStreamDefaultController controller, optional any chunk); +callback UnderlyingSinkCloseCallback = Promise<void> (); +callback UnderlyingSinkAbortCallback = Promise<void> (optional any reason);
@@ -3415,7 +3415,7 @@ as seen for example in [[#example-ws-no-backpressure]].

This is to allow us to add new potential types in the future, without backward-compatibility concerns. 1. Perform ! [$InitializeWritableStream$]([=this=]). - 1. Let |sizeAlgorithm| be ? [$ExtractSizeAlgorithm$](|strategy|). + 1. Let |sizeAlgorithm| be ! [$ExtractSizeAlgorithm$](|strategy|). 1. Let |highWaterMark| be ? [$ExtractHighWaterMark$](|strategy|, 1). 1. Perform ? [$SetUpWritableStreamDefaultControllerFromUnderlyingSink$]([=this=], |underlyingSink|, |underlyingSinkDict|, |highWaterMark|, |sizeAlgorithm|). @@ -4602,15 +4602,16 @@ the [=transformer=]. Such objects can contain any of the following methods:

dictionary Transformer { - TransformStreamControllerCallback start; - TransformStreamTransformCallback transform; - TransformStreamControllerCallback flush; + TransformerStartCallback start; + TransformerTransformCallback transform; + TransformerFlushCallback flush; any readableType; any writableType; }; -callback TransformStreamControllerCallback = Promise<void> (TransformStreamDefaultController controller); -callback TransformStreamTransformCallback = Promise<void> (TransformStreamDefaultController controller, optional any chunk); +callback TransformerStartCallback = any (TransformStreamDefaultController controller); +callback TransformerFlushCallback = Promise<void> (TransformStreamDefaultController controller); +callback TransformerTransformCallback = Promise<void> (TransformStreamDefaultController controller, optional any chunk);
@@ -4726,10 +4727,10 @@ side=], or to terminate or error the stream. exception. 1. If |transformerDict|["{{Transformer/writableType}}"] [=map/exists=], throw a {{RangeError}} exception. - 1. Let |writableSizeAlgorithm| be ? [$ExtractSizeAlgorithm$](|writableStrategy|). - 1. Let |writableHighWaterMark| be ? [$ExtractHighWaterMark$](|writableStrategy|, 1). - 1. Let |readableSizeAlgorithm| be ? [$ExtractSizeAlgorithm$](|readableStrategy|). 1. Let |readableHighWaterMark| be ? [$ExtractHighWaterMark$](|readableStrategy|, 0). + 1. Let |readableSizeAlgorithm| be ! [$ExtractSizeAlgorithm$](|readableStrategy|). + 1. Let |writableHighWaterMark| be ? [$ExtractHighWaterMark$](|writableStrategy|, 1). + 1. Let |writableSizeAlgorithm| be ! [$ExtractSizeAlgorithm$](|writableStrategy|). 1. Let |startPromise| be [=a new promise=]. 1. Perform ! [$InitializeTransformStream$]([=this=], |startPromise|, |writableHighWaterMark|, |writableSizeAlgorithm|, |readableHighWaterMark|, |readableSizeAlgorithm|). @@ -5155,7 +5156,7 @@ side=] of [=transform streams=]. 1. Throw |readable|.\[[storedError]].
-

Default source

+

Default sources

The following abstract operation is used to implement the [=underlying source=] for the [=readable side=] of [=transform streams=]. diff --git a/reference-implementation/lib/ReadableByteStreamController-impl.js b/reference-implementation/lib/ReadableByteStreamController-impl.js index 4fd8a3497..ee1bc76c3 100644 --- a/reference-implementation/lib/ReadableByteStreamController-impl.js +++ b/reference-implementation/lib/ReadableByteStreamController-impl.js @@ -35,7 +35,7 @@ exports.implementation = class ReadableByteStreamControllerImpl { throw new TypeError('The stream has already been closed; do not close it again!'); } - const state = this._controlledReadableByteStream._state; + const state = this._controlledReadableStream._state; if (state !== 'readable') { throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`); } @@ -48,7 +48,7 @@ exports.implementation = class ReadableByteStreamControllerImpl { throw new TypeError('stream is closed or draining'); } - const state = this._controlledReadableByteStream._state; + const state = this._controlledReadableStream._state; if (state !== 'readable') { throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`); } @@ -82,7 +82,7 @@ exports.implementation = class ReadableByteStreamControllerImpl { } [PullSteps]() { - const stream = this._controlledReadableByteStream; + const stream = this._controlledReadableStream; assert(aos.ReadableStreamHasDefaultReader(stream) === true); if (this._queueTotalSize > 0) { diff --git a/reference-implementation/lib/ReadableStream-impl.js b/reference-implementation/lib/ReadableStream-impl.js index 826f2f785..cc7b523de 100644 --- a/reference-implementation/lib/ReadableStream-impl.js +++ b/reference-implementation/lib/ReadableStream-impl.js @@ -59,6 +59,13 @@ exports.implementation = class ReadableStreamImpl { } pipeThrough(transform, options) { + // Conversion here is needed until https://github.com/jsdom/webidl2js/issues/81 is fixed. + if ('signal' in options) { + if (!isAbortSignal(options.signal)) { + return promiseRejectedWith(new TypeError('Invalid signal argument')); + } + } + if (aos.IsReadableStreamLocked(this) === true) { throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream'); } @@ -105,7 +112,7 @@ exports.implementation = class ReadableStreamImpl { } }; -// See pipeTo() for why this is needed. +// See pipeTo()/pipeThrough() for why this is needed. const abortedGetter = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted').get; function isAbortSignal(v) { try { diff --git a/reference-implementation/lib/TransformStream-impl.js b/reference-implementation/lib/TransformStream-impl.js new file mode 100644 index 000000000..2e29fdfe0 --- /dev/null +++ b/reference-implementation/lib/TransformStream-impl.js @@ -0,0 +1,48 @@ +'use strict'; + +const { newPromise, resolvePromise, invoke } = require('./helpers/webidl.js'); +const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/queuing-strategy.js'); +const aos = require('./abstract-ops/transform-streams.js'); + +const Transformer = require('../generated/Transformer.js'); + +exports.implementation = class TransformStreamImpl { + constructor(globalObject, [transformer, writableStrategy, readableStrategy]) { + if (transformer === undefined) { + transformer = null; + } + const transformerDict = Transformer.convert(transformer); + if ('readableType' in transformerDict) { + throw new RangeError('Invalid readableType specified'); + } + if ('writableType' in transformerDict) { + throw new RangeError('Invalid writableType specified'); + } + + const readableHighWaterMark = ExtractHighWaterMark(readableStrategy, 0); + const readableSizeAlgorithm = ExtractSizeAlgorithm(readableStrategy); + const writableHighWaterMark = ExtractHighWaterMark(writableStrategy, 1); + const writableSizeAlgorithm = ExtractSizeAlgorithm(writableStrategy); + + const startPromise = newPromise(); + + aos.InitializeTransformStream( + this, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm + ); + aos.SetUpTransformStreamDefaultControllerFromTransformer(this, transformer, transformerDict); + + if ('start' in transformerDict) { + resolvePromise(startPromise, invoke(transformerDict.start, [this._transformStreamController], transformer)); + } else { + resolvePromise(startPromise, undefined); + } + } + + get readable() { + return this._readable; + } + + get writable() { + return this._writable; + } +}; diff --git a/reference-implementation/lib/TransformStream.webidl b/reference-implementation/lib/TransformStream.webidl new file mode 100644 index 000000000..3d02834c7 --- /dev/null +++ b/reference-implementation/lib/TransformStream.webidl @@ -0,0 +1,9 @@ +[Exposed=(Window,Worker,Worklet)] +interface TransformStream { + constructor(optional object transformer, + optional QueuingStrategy writableStrategy = {}, + optional QueuingStrategy readableStrategy = {}); + + readonly attribute ReadableStream readable; + readonly attribute WritableStream writable; +}; diff --git a/reference-implementation/lib/TransformStreamDefaultController-impl.js b/reference-implementation/lib/TransformStreamDefaultController-impl.js new file mode 100644 index 000000000..b3e87b7e8 --- /dev/null +++ b/reference-implementation/lib/TransformStreamDefaultController-impl.js @@ -0,0 +1,23 @@ +'use strict'; + +const aos = require('./abstract-ops/transform-streams.js'); +const rsAOs = require('./abstract-ops/readable-streams.js'); + +exports.implementation = class TransformStreamDefaultController { + get desiredSize() { + const readableController = this._controlledTransformStream._readable._readableStreamController; + return rsAOs.ReadableStreamDefaultControllerGetDesiredSize(readableController); + } + + enqueue(chunk) { + aos.TransformStreamDefaultControllerEnqueue(this, chunk); + } + + error(reason) { + aos.TransformStreamDefaultControllerError(this, reason); + } + + terminate() { + aos.TransformStreamDefaultControllerTerminate(this); + } +}; diff --git a/reference-implementation/lib/TransformStreamDefaultController.webidl b/reference-implementation/lib/TransformStreamDefaultController.webidl new file mode 100644 index 000000000..8602a4edf --- /dev/null +++ b/reference-implementation/lib/TransformStreamDefaultController.webidl @@ -0,0 +1,8 @@ +[Exposed=(Window,Worker,Worklet)] +interface TransformStreamDefaultController { + readonly attribute unrestricted double? desiredSize; + + void enqueue(optional any chunk); + void error(optional any reason); + void terminate(); +}; diff --git a/reference-implementation/lib/Transformer.webidl b/reference-implementation/lib/Transformer.webidl new file mode 100644 index 000000000..06fa3a84a --- /dev/null +++ b/reference-implementation/lib/Transformer.webidl @@ -0,0 +1,11 @@ +dictionary Transformer { + TransformerStartCallback start; + TransformerTransformCallback transform; + TransformerFlushCallback flush; + any readableType; + any writableType; +}; + +callback TransformerStartCallback = any (TransformStreamDefaultController controller); +callback TransformerFlushCallback = Promise (TransformStreamDefaultController controller); +callback TransformerTransformCallback = Promise (TransformStreamDefaultController controller, optional any chunk); diff --git a/reference-implementation/lib/UnderlyingSink.webidl b/reference-implementation/lib/UnderlyingSink.webidl index ae6ecf1ad..b8287e8b0 100644 --- a/reference-implementation/lib/UnderlyingSink.webidl +++ b/reference-implementation/lib/UnderlyingSink.webidl @@ -1,12 +1,12 @@ dictionary UnderlyingSink { - WritableStreamStartCallback start; - WritableStreamWriteCallback write; - WritableStreamCloseCallback close; - WritableStreamAbortCallback abort; + UnderlyingSinkStartCallback start; + UnderlyingSinkWriteCallback write; + UnderlyingSinkCloseCallback close; + UnderlyingSinkAbortCallback abort; any type; }; -callback WritableStreamStartCallback = any (WritableStreamDefaultController controller); -callback WritableStreamWriteCallback = Promise (WritableStreamDefaultController controller, optional any chunk); -callback WritableStreamCloseCallback = Promise (); -callback WritableStreamAbortCallback = Promise (optional any reason); +callback UnderlyingSinkStartCallback = any (WritableStreamDefaultController controller); +callback UnderlyingSinkWriteCallback = Promise (WritableStreamDefaultController controller, optional any chunk); +callback UnderlyingSinkCloseCallback = Promise (); +callback UnderlyingSinkAbortCallback = Promise (optional any reason); diff --git a/reference-implementation/lib/UnderlyingSource.webidl b/reference-implementation/lib/UnderlyingSource.webidl index 5d1b679e8..a048269b5 100644 --- a/reference-implementation/lib/UnderlyingSource.webidl +++ b/reference-implementation/lib/UnderlyingSource.webidl @@ -1,15 +1,15 @@ dictionary UnderlyingSource { - ReadableStreamStartCallback start; - ReadableStreamPullCallback pull; - ReadableStreamCancelCallback cancel; + UnderlyingSourceStartCallback start; + UnderlyingSourcePullCallback pull; + UnderlyingSourceCancelCallback cancel; ReadableStreamType type; [EnforceRange] unsigned long long autoAllocateChunkSize; }; typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController; -callback ReadableStreamStartCallback = any (ReadableStreamController controller); -callback ReadableStreamPullCallback = Promise (ReadableStreamController controller); -callback ReadableStreamCancelCallback = Promise (optional any reason); +callback UnderlyingSourceStartCallback = any (ReadableStreamController controller); +callback UnderlyingSourcePullCallback = Promise (ReadableStreamController controller); +callback UnderlyingSourceCancelCallback = Promise (optional any reason); - enum ReadableStreamType { "bytes" }; +enum ReadableStreamType { "bytes" }; diff --git a/reference-implementation/lib/abstract-ops/readable-streams.js b/reference-implementation/lib/abstract-ops/readable-streams.js index c35fef200..243afe26e 100644 --- a/reference-implementation/lib/abstract-ops/readable-streams.js +++ b/reference-implementation/lib/abstract-ops/readable-streams.js @@ -23,6 +23,7 @@ const WritableStreamImpl = require('../WritableStream-impl.js'); Object.assign(exports, { AcquireReadableStreamBYOBReader, AcquireReadableStreamDefaultReader, + CreateReadableStream, InitializeReadableStream, IsReadableStreamLocked, ReadableByteStreamControllerCallPullIfNeeded, diff --git a/reference-implementation/lib/transform-stream.js b/reference-implementation/lib/abstract-ops/transform-streams.js similarity index 51% rename from reference-implementation/lib/transform-stream.js rename to reference-implementation/lib/abstract-ops/transform-streams.js index f496a7b13..a8a6f8c19 100644 --- a/reference-implementation/lib/transform-stream.js +++ b/reference-implementation/lib/abstract-ops/transform-streams.js @@ -1,110 +1,31 @@ 'use strict'; const assert = require('assert'); - -// Calls to verbose() are purely for debugging the reference implementation and tests. They are not part of the standard -// and do not appear in the standard text. const verbose = require('debug')('streams:transform-stream:verbose'); -const { InvokeOrNoop, CreateAlgorithmFromUnderlyingMethod, PromiseCall, typeIsObject, - ValidateAndNormalizeHighWaterMark, IsNonNegativeNumber, MakeSizeAlgorithmFromSizeFunction, newPromise, - promiseResolvedWith, promiseRejectedWith, transformPromiseWith } = require('./helpers.js'); -const { CreateReadableStream, ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue, - ReadableStreamDefaultControllerError, ReadableStreamDefaultControllerGetDesiredSize, - ReadableStreamDefaultControllerHasBackpressure, - ReadableStreamDefaultControllerCanCloseOrEnqueue } = require('./readable-stream.js'); -const { CreateWritableStream, WritableStreamDefaultControllerErrorIfNeeded } = require('./writable-stream.js'); - -// Class TransformStream - -class TransformStream { - constructor(transformer = {}, writableStrategy = {}, readableStrategy = {}) { - const writableSizeFunction = writableStrategy.size; - let writableHighWaterMark = writableStrategy.highWaterMark; - const readableSizeFunction = readableStrategy.size; - let readableHighWaterMark = readableStrategy.highWaterMark; - - const writableType = transformer.writableType; - - if (writableType !== undefined) { - throw new RangeError('Invalid writable type specified'); - } - - const writableSizeAlgorithm = MakeSizeAlgorithmFromSizeFunction(writableSizeFunction); - if (writableHighWaterMark === undefined) { - writableHighWaterMark = 1; - } - writableHighWaterMark = ValidateAndNormalizeHighWaterMark(writableHighWaterMark); - - const readableType = transformer.readableType; - - if (readableType !== undefined) { - throw new RangeError('Invalid readable type specified'); - } - - const readableSizeAlgorithm = MakeSizeAlgorithmFromSizeFunction(readableSizeFunction); - if (readableHighWaterMark === undefined) { - readableHighWaterMark = 0; - } - readableHighWaterMark = ValidateAndNormalizeHighWaterMark(readableHighWaterMark); - - let startPromise_resolve; - const startPromise = newPromise(resolve => { - startPromise_resolve = resolve; - }); - - InitializeTransformStream(this, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, - readableSizeAlgorithm); - SetUpTransformStreamDefaultControllerFromTransformer(this, transformer); - - const startResult = InvokeOrNoop(transformer, 'start', [this._transformStreamController]); - startPromise_resolve(startResult); - } - - get readable() { - if (IsTransformStream(this) === false) { - throw streamBrandCheckException('readable'); - } - - return this._readable; - } - - get writable() { - if (IsTransformStream(this) === false) { - throw streamBrandCheckException('writable'); - } - - return this._writable; - } -} - -// Transform Stream Abstract Operations - -function CreateTransformStream(startAlgorithm, transformAlgorithm, flushAlgorithm, writableHighWaterMark = 1, - writableSizeAlgorithm = () => 1, readableHighWaterMark = 0, - readableSizeAlgorithm = () => 1) { - assert(IsNonNegativeNumber(writableHighWaterMark)); - assert(IsNonNegativeNumber(readableHighWaterMark)); - const stream = Object.create(TransformStream.prototype); +const { webidlNew, promiseInvoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, + transformPromiseWith } = require('../helpers/webidl.js'); +const { CreateReadableStream, ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue, + ReadableStreamDefaultControllerError, ReadableStreamDefaultControllerHasBackpressure, + ReadableStreamDefaultControllerCanCloseOrEnqueue } = require('./readable-streams.js'); +const { CreateWritableStream, WritableStreamDefaultControllerErrorIfNeeded } = require('./writable-streams.js'); - let startPromise_resolve; - const startPromise = newPromise(resolve => { - startPromise_resolve = resolve; - }); +const TransformStreamImpl = require('../TransformStream-impl.js'); +const TransformStreamDefaultControllerImpl = require('../TransformStreamDefaultController-impl.js'); - InitializeTransformStream(stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, - readableSizeAlgorithm); +Object.assign(exports, { + InitializeTransformStream, + SetUpTransformStreamDefaultControllerFromTransformer, + TransformStreamDefaultControllerEnqueue, + TransformStreamDefaultControllerError, + TransformStreamDefaultControllerTerminate +}); - const controller = Object.create(TransformStreamDefaultController.prototype); +// Working with transform streams - SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm); - - const startResult = startAlgorithm(); - startPromise_resolve(startResult); - return stream; -} +// CreateTransformStream is not implemented since it is only meant for external specs. -function InitializeTransformStream(stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, - readableHighWaterMark, readableSizeAlgorithm) { +function InitializeTransformStream( + stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm) { function startAlgorithm() { return startPromise; } @@ -121,8 +42,9 @@ function InitializeTransformStream(stream, startPromise, writableHighWaterMark, return TransformStreamDefaultSinkCloseAlgorithm(stream); } - stream._writable = CreateWritableStream(startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, - writableHighWaterMark, writableSizeAlgorithm); + stream._writable = CreateWritableStream( + startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, writableHighWaterMark, writableSizeAlgorithm + ); function pullAlgorithm() { return TransformStreamDefaultSourcePullAlgorithm(stream); @@ -133,32 +55,18 @@ function InitializeTransformStream(stream, startPromise, writableHighWaterMark, return promiseResolvedWith(undefined); } - stream._readable = CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, readableHighWaterMark, - readableSizeAlgorithm); + stream._readable = CreateReadableStream( + startAlgorithm, pullAlgorithm, cancelAlgorithm, readableHighWaterMark, readableSizeAlgorithm + ); // The [[backpressure]] slot is set to undefined so that it can be initialised by TransformStreamSetBackpressure. stream._backpressure = undefined; stream._backpressureChangePromise = undefined; - stream._backpressureChangePromise_resolve = undefined; TransformStreamSetBackpressure(stream, true); - // Used by IsWritableStream() which is called by SetUpTransformStreamDefaultController(). stream._transformStreamController = undefined; } -function IsTransformStream(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_transformStreamController')) { - return false; - } - - return true; -} - -// This is a no-op if both sides are already errored. function TransformStreamError(stream, e) { verbose('TransformStreamError()'); @@ -184,73 +92,18 @@ function TransformStreamSetBackpressure(stream, backpressure) { assert(stream._backpressure !== backpressure); if (stream._backpressureChangePromise !== undefined) { - stream._backpressureChangePromise_resolve(); + resolvePromise(stream._backpressureChangePromise, undefined); } - stream._backpressureChangePromise = newPromise(resolve => { - stream._backpressureChangePromise_resolve = resolve; - }); + stream._backpressureChangePromise = newPromise(); stream._backpressure = backpressure; } -// Class TransformStreamDefaultController - -class TransformStreamDefaultController { - constructor() { - throw new TypeError('TransformStreamDefaultController instances cannot be created directly'); - } - - get desiredSize() { - if (IsTransformStreamDefaultController(this) === false) { - throw defaultControllerBrandCheckException('desiredSize'); - } - - const readableController = this._controlledTransformStream._readable._readableStreamController; - return ReadableStreamDefaultControllerGetDesiredSize(readableController); - } - - enqueue(chunk) { - if (IsTransformStreamDefaultController(this) === false) { - throw defaultControllerBrandCheckException('enqueue'); - } - - TransformStreamDefaultControllerEnqueue(this, chunk); - } - - error(reason) { - if (IsTransformStreamDefaultController(this) === false) { - throw defaultControllerBrandCheckException('error'); - } - - TransformStreamDefaultControllerError(this, reason); - } - - terminate() { - if (IsTransformStreamDefaultController(this) === false) { - throw defaultControllerBrandCheckException('terminate'); - } - - TransformStreamDefaultControllerTerminate(this); - } -} - -// Transform Stream Default Controller Abstract Operations - -function IsTransformStreamDefaultController(x) { - if (!typeIsObject(x)) { - return false; - } - - if (!Object.prototype.hasOwnProperty.call(x, '_controlledTransformStream')) { - return false; - } - - return true; -} +// Default controllers function SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm) { - assert(IsTransformStream(stream) === true); + assert(stream instanceof TransformStreamImpl.implementation); assert(stream._transformStreamController === undefined); controller._controlledTransformStream = stream; @@ -260,10 +113,10 @@ function SetUpTransformStreamDefaultController(stream, controller, transformAlgo controller._flushAlgorithm = flushAlgorithm; } -function SetUpTransformStreamDefaultControllerFromTransformer(stream, transformer) { +function SetUpTransformStreamDefaultControllerFromTransformer(stream, transformer, transformerDict) { assert(transformer !== undefined); - const controller = Object.create(TransformStreamDefaultController.prototype); + const controller = webidlNew(globalThis, 'TransformStreamDefaultController', TransformStreamDefaultControllerImpl); let transformAlgorithm = chunk => { try { @@ -273,15 +126,15 @@ function SetUpTransformStreamDefaultControllerFromTransformer(stream, transforme return promiseRejectedWith(transformResultE); } }; - const transformMethod = transformer.transform; - if (transformMethod !== undefined) { - if (typeof transformMethod !== 'function') { - throw new TypeError('transform is not a method'); - } - transformAlgorithm = chunk => PromiseCall(transformMethod, transformer, [chunk, controller]); - } - const flushAlgorithm = CreateAlgorithmFromUnderlyingMethod(transformer, 'flush', 0, [controller]); + let flushAlgorithm = () => promiseResolvedWith(undefined); + + if ('transform' in transformerDict) { + transformAlgorithm = chunk => promiseInvoke(transformerDict.transform, [chunk, controller], transformer); + } + if ('flush' in transformerDict) { + flushAlgorithm = () => promiseInvoke(transformerDict.flush, [controller], transformer); + } SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm); } @@ -343,7 +196,7 @@ function TransformStreamDefaultControllerTerminate(controller) { TransformStreamErrorWritableAndUnblockWrite(stream, error); } -// TransformStreamDefaultSink Algorithms +// Default sinks function TransformStreamDefaultSinkWriteAlgorithm(stream, chunk) { verbose('TransformStreamDefaultSinkWriteAlgorithm()'); @@ -401,7 +254,7 @@ function TransformStreamDefaultSinkCloseAlgorithm(stream) { }); } -// TransformStreamDefaultSource Algorithms +// Default sources function TransformStreamDefaultSourcePullAlgorithm(stream) { verbose('TransformStreamDefaultSourcePullAlgorithm()'); @@ -416,19 +269,3 @@ function TransformStreamDefaultSourcePullAlgorithm(stream) { // Prevent the next pull() call until there is backpressure. return stream._backpressureChangePromise; } - -module.exports = { CreateTransformStream, TransformStream }; - -// Helper functions for the TransformStreamDefaultController. - -function defaultControllerBrandCheckException(name) { - return new TypeError( - `TransformStreamDefaultController.prototype.${name} can only be used on a TransformStreamDefaultController`); -} - -// Helper functions for the TransformStream. - -function streamBrandCheckException(name) { - return new TypeError( - `TransformStream.prototype.${name} can only be used on a TransformStream`); -} diff --git a/reference-implementation/lib/abstract-ops/writable-streams.js b/reference-implementation/lib/abstract-ops/writable-streams.js index 8751879fe..4a5825145 100644 --- a/reference-implementation/lib/abstract-ops/writable-streams.js +++ b/reference-implementation/lib/abstract-ops/writable-streams.js @@ -24,6 +24,7 @@ Object.assign(exports, { WritableStreamCloseQueuedOrInFlight, WritableStreamDefaultControllerClearAlgorithms, WritableStreamDefaultControllerError, + WritableStreamDefaultControllerErrorIfNeeded, WritableStreamDefaultWriterAbort, WritableStreamDefaultWriterClose, WritableStreamDefaultWriterCloseWithErrorPropagation, diff --git a/reference-implementation/lib/helpers/webidl.js b/reference-implementation/lib/helpers/webidl.js index 239b8a6fd..72a5d0858 100644 --- a/reference-implementation/lib/helpers/webidl.js +++ b/reference-implementation/lib/helpers/webidl.js @@ -76,7 +76,8 @@ function rejectPromise(p, reason) { // https://heycam.github.io/webidl/#a-promise-resolved-with function promiseResolvedWith(value) { - const promise = originalPromiseResolve.call(originalPromise, value); + // Cannot use original Promise.resolve since that will return value itself sometimes, unlike Web IDL. + const promise = new originalPromise(resolve => resolve(value)); promiseSideTable.set(promise, { stateIsPending: false }); return promise; } diff --git a/reference-implementation/lib/index.js b/reference-implementation/lib/index.js index b066f10a0..ce8a0e54b 100644 --- a/reference-implementation/lib/index.js +++ b/reference-implementation/lib/index.js @@ -2,9 +2,6 @@ // This file is used as the entry point for browserifying the reference implementation to allow it // to run inside the wpt-runner "browser like" context. -// const { TransformStream } = require('./transform-stream.js'); - -// window.TransformStream = TransformStream; window.gc = gc; require('../generated/ByteLengthQueuingStrategy.js').install(window, ['Window']); @@ -20,3 +17,6 @@ require('../generated/ReadableStreamBYOBRequest.js').install(window, ['Window']) require('../generated/WritableStream.js').install(window, ['Window']); require('../generated/WritableStreamDefaultWriter.js').install(window, ['Window']); require('../generated/WritableStreamDefaultController.js').install(window, ['Window']); + +require('../generated/TransformStream.js').install(window, ['Window']); +require('../generated/TransformStreamDefaultController.js').install(window, ['Window']); diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index 55d456135..c3ee3b7fc 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit 55d4561356b8dea58d251755431ec9646f521d98 +Subproject commit c3ee3b7fc723e30afd02f5bc2fd02f389f5aca99 From 533f58c75d0b9b926ec75ac0149b6e9bd1f7bde8 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Tue, 21 Apr 2020 13:14:32 -0400 Subject: [PATCH 10/30] Uniformly check for zero-length buffers --- index.bs | 14 ++++++++++++-- .../lib/ReadableByteStreamController-impl.js | 15 +++++++-------- .../lib/ReadableStreamBYOBReader-impl.js | 7 +++++++ .../lib/ReadableStreamBYOBRequest-impl.js | 11 +++++++++++ 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/index.bs b/index.bs index ff9da397a..b867fc9ac 100644 --- a/index.bs +++ b/index.bs @@ -541,7 +541,7 @@ callback UnderlyingSourceStartCallback = any (ReadableStreamController controlle callback UnderlyingSourcePullCallback = Promise (ReadableStreamController controller); callback UnderlyingSourceCancelCallback = Promise (optional any reason); - enum ReadableStreamType { "bytes" }; +enum ReadableStreamType { "bytes" };
@@ -1162,9 +1162,11 @@ following table: The read(|view|) method steps are: + 1. If |view|.\[[ByteLength]] is 0, return [=a promise rejected with=] a {{TypeError}} exception. + 1. If |view|.\[[ViewedArrayBuffer]].\[[ArrayBufferByteLength]] is 0, return [=a promise rejected + with=] a {{TypeError}} exception. 1. If [=this=].\[[ownerReadableStream]] is undefined, return [=a promise rejected with=] a {{TypeError}} exception. - 1. If |view|.\[[ByteLength]] is 0, return [=a promise rejected with=] a {{TypeError}} exception. 1. Return ! [$ReadableStreamBYOBReaderRead$]([=this=], |view|).
@@ -1521,6 +1523,9 @@ following table: The enqueue(|chunk|) method steps are: + 1. If |chunk|.\[[ByteLength]] is 0, throw a {{TypeError}} exception. + 1. If |chunk|.\[[ViewedArrayBuffer]].\[[ArrayBufferByteLength]] is 0, throw a {{TypeError}} + exception. 1. If [=this=].\[[closeRequested]] is true, throw a {{TypeError}} exception. 1. If [=this=].\[[controlledReadableStream]].\[[state]] is not "`readable`", throw a {{TypeError}} exception. @@ -1663,6 +1668,8 @@ following table: 1. If [=this=].\[[controller]] is undefined, throw a {{TypeError}} exception. 1. If [$IsDetachedBuffer$]([=this=].\[[view]].\[[ArrayBuffer]]) is true, throw a {{TypeError}} exception. + 1. Assert: [=this=].\[[view]].\[[ByteLength]] > 0. + 1. Assert: [=this=].\[[view]].\[[ViewedArrayBuffer]].\[[ByeLength]] > 0. 1. Perform ? [$ReadableByteStreamControllerRespond$]([=this=].\[[controller]], |bytesWritten|).
@@ -1670,6 +1677,9 @@ following table: The respondWithNewView(|view|) method steps are: + 1. If |view|.\[[ByteLength]] is 0, throw a {{TypeError}} exception. + 1. If |view|.\[[ViewedArrayBuffer]].\[[ArrayBufferByteLength]] is 0, throw a {{TypeError}} + exception. 1. If [=this=].\[[controller]] is undefined, throw a {{TypeError}} exception. 1. Return ? [$ReadableByteStreamControllerRespondWithNewView$]([=this=].\[[controller]], |view|).
diff --git a/reference-implementation/lib/ReadableByteStreamController-impl.js b/reference-implementation/lib/ReadableByteStreamController-impl.js index ee1bc76c3..c59b493d9 100644 --- a/reference-implementation/lib/ReadableByteStreamController-impl.js +++ b/reference-implementation/lib/ReadableByteStreamController-impl.js @@ -44,6 +44,13 @@ exports.implementation = class ReadableByteStreamControllerImpl { } enqueue(chunk) { + if (chunk.byteLength === 0) { + throw new TypeError('chunk must have non-zero byteLength'); + } + if (chunk.buffer.byteLength === 0) { + throw new TypeError('chunk\'s buffer must have non-zero byteLength'); + } + if (this._closeRequested === true) { throw new TypeError('stream is closed or draining'); } @@ -53,14 +60,6 @@ exports.implementation = class ReadableByteStreamControllerImpl { throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`); } - if (!ArrayBuffer.isView(chunk)) { - throw new TypeError('You can only enqueue array buffer views when using a ReadableByteStreamController'); - } - - if (IsDetachedBuffer(chunk.buffer) === true) { - throw new TypeError('Cannot enqueue a view onto a detached ArrayBuffer'); - } - aos.ReadableByteStreamControllerEnqueue(this, chunk); } diff --git a/reference-implementation/lib/ReadableStreamBYOBReader-impl.js b/reference-implementation/lib/ReadableStreamBYOBReader-impl.js index 690d61959..cc7fc32d1 100644 --- a/reference-implementation/lib/ReadableStreamBYOBReader-impl.js +++ b/reference-implementation/lib/ReadableStreamBYOBReader-impl.js @@ -24,6 +24,13 @@ exports.implementation = class ReadableStreamBYOBReaderImpl { if (view.byteLength === 0) { return promiseRejectedWith(new TypeError('view must have non-zero byteLength')); } + if (view.buffer.byteLength === 0) { + return promiseRejectedWith(new TypeError('view\'s buffer must have non-zero byteLength')); + } + + if (this._ownerReadableStream === undefined) { + return promiseRejectedWith(readerLockException('cancel')); + } return aos.ReadableStreamBYOBReaderRead(this, view); } diff --git a/reference-implementation/lib/ReadableStreamBYOBRequest-impl.js b/reference-implementation/lib/ReadableStreamBYOBRequest-impl.js index 2c7980c81..2e737fd9b 100644 --- a/reference-implementation/lib/ReadableStreamBYOBRequest-impl.js +++ b/reference-implementation/lib/ReadableStreamBYOBRequest-impl.js @@ -1,4 +1,5 @@ 'use strict'; +const assert = require('assert'); const { IsDetachedBuffer } = require('./abstract-ops/ecmascript.js'); const aos = require('./abstract-ops/readable-streams.js'); @@ -17,10 +18,20 @@ exports.implementation = class ReadableStreamBYOBRequestImpl { throw new TypeError('The BYOB request\'s buffer has been detached and so cannot be used as a response'); } + assert(this._view.byteLength > 0); + assert(this._view.buffer.byteLength > 0); + aos.ReadableByteStreamControllerRespond(this._controller, bytesWritten); } respondWithNewView(view) { + if (view.byteLength === 0) { + throw new TypeError('chunk must have non-zero byteLength'); + } + if (view.buffer.byteLength === 0) { + throw new TypeError('chunk\'s buffer must have non-zero byteLength'); + } + if (this._controller === undefined) { throw new TypeError('This BYOB request has been invalidated'); } From 616254dc53fa6af9c6ff4b1ee298fb1600fae68f Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Tue, 21 Apr 2020 13:38:45 -0400 Subject: [PATCH 11/30] Make byobRequest null instead of undefined for no-request --- index.bs | 14 +++++++------- .../lib/ReadableByteStreamController-impl.js | 2 +- .../lib/ReadableByteStreamController.webidl | 2 +- .../lib/abstract-ops/readable-streams.js | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/index.bs b/index.bs index b867fc9ac..9187d9da7 100644 --- a/index.bs +++ b/index.bs @@ -1369,7 +1369,7 @@ The Web IDL definition for the {{ReadableByteStreamController}} class is given a [Exposed=(Window,Worker,Worklet)] interface ReadableByteStreamController { - readonly attribute ReadableStreamBYOBRequest byobRequest; + readonly attribute ReadableStreamBYOBRequest? byobRequest; readonly attribute unrestricted double? desiredSize; void close(); @@ -1397,7 +1397,7 @@ following table: <tr> <td>\[[byobRequest]] <td class="non-normative">A {{ReadableStreamBYOBRequest}} instance representing the current BYOB - pull request, or undefined if there are no pending requests + pull request, or null if there are no pending requests <tr> <td>\[[cancelAlgorithm]] <td class="non-normative">A promise-returning algorithm, taking one argument (the cancel reason), @@ -1459,7 +1459,7 @@ following table: <dl class="domintro non-normative"> <dt><code><var ignore>byobRequest</var> = <var ignore>controller</var>.{{ReadableByteStreamController/byobRequest}}</code> <dd> - <p>Returns the current BYOB pull request. + <p>Returns the current BYOB pull request, or null if there isn't one. <dt><code><var ignore>desiredSize</var> = <var ignore>controller</var>.{{ReadableByteStreamController/desiredSize}}</code> <dd> @@ -1489,7 +1489,7 @@ following table: The <dfn id="rbs-controller-byob-request" attribute for="ReadableByteStreamController">byobRequest</dfn> attribute's getter steps are: - 1. If [=this=].\[[byobRequest]] is undefined and [=this=].\[[pendingPullIntos]] is not [=list/is + 1. If [=this=].\[[byobRequest]] is null and [=this=].\[[pendingPullIntos]] is not [=list/is empty|empty=], 1. Let |firstDescriptor| be [=this=].\[[pendingPullIntos]][0]. 1. Let |view| be ! [$Construct$]({{%Uint8Array%}}, « |firstDescriptor|.\[[buffer]], @@ -2796,10 +2796,10 @@ The following abstract operations support the implementation of the id="readable-byte-stream-controller-invalidate-byob-request">ReadableByteStreamControllerInvalidateBYOBRequest(|controller|)</dfn> performs the following steps: - 1. If |controller|.\[[byobRequest]] is undefined, return. + 1. If |controller|.\[[byobRequest]] is null, return. 1. Set |controller|.\[[byobRequest]].\[[controller]] to undefined. 1. Set |controller|.\[[byobRequest]].\[[view]] to undefined. - 1. Set |controller|.\[[byobRequest]] to undefined. + 1. Set |controller|.\[[byobRequest]] to null. </div> <div algorithm> @@ -2991,7 +2991,7 @@ The following abstract operations support the implementation of the 1. Assert: |autoAllocateChunkSize| is positive. 1. Set |controller|.\[[controlledReadableStream]] to |stream|. 1. Set |controller|.\[[pullAgain]] and |controller|.\[[pulling]] to false. - 1. Set |controller|.\[[byobRequest]] to undefined. + 1. Set |controller|.\[[byobRequest]] to null. 1. Perform ! [$ResetQueue$](|controller|). 1. Set |controller|.\[[closeRequested]] and |controller|.\[[started]] to false. 1. Set |controller|.\[[strategyHWM]] to |highWaterMark|. diff --git a/reference-implementation/lib/ReadableByteStreamController-impl.js b/reference-implementation/lib/ReadableByteStreamController-impl.js index c59b493d9..0cbb4b659 100644 --- a/reference-implementation/lib/ReadableByteStreamController-impl.js +++ b/reference-implementation/lib/ReadableByteStreamController-impl.js @@ -11,7 +11,7 @@ const ReadableStreamBYOBRequestImpl = require('./ReadableStreamBYOBRequest-impl. exports.implementation = class ReadableByteStreamControllerImpl { get byobRequest() { - if (this._byobRequest === undefined && this._pendingPullIntos.length > 0) { + if (this._byobRequest === null && this._pendingPullIntos.length > 0) { const firstDescriptor = this._pendingPullIntos[0]; const view = new Uint8Array(firstDescriptor.buffer, firstDescriptor.byteOffset + firstDescriptor.bytesFilled, diff --git a/reference-implementation/lib/ReadableByteStreamController.webidl b/reference-implementation/lib/ReadableByteStreamController.webidl index 7705c12fb..9e278fc1e 100644 --- a/reference-implementation/lib/ReadableByteStreamController.webidl +++ b/reference-implementation/lib/ReadableByteStreamController.webidl @@ -1,6 +1,6 @@ [Exposed=(Window,Worker,Worklet)] interface ReadableByteStreamController { - readonly attribute ReadableStreamBYOBRequest byobRequest; + readonly attribute ReadableStreamBYOBRequest? byobRequest; readonly attribute unrestricted double? desiredSize; void close(); diff --git a/reference-implementation/lib/abstract-ops/readable-streams.js b/reference-implementation/lib/abstract-ops/readable-streams.js index 243afe26e..eee1abd81 100644 --- a/reference-implementation/lib/abstract-ops/readable-streams.js +++ b/reference-implementation/lib/abstract-ops/readable-streams.js @@ -1128,13 +1128,13 @@ function ReadableByteStreamControllerHandleQueueDrain(controller) { } function ReadableByteStreamControllerInvalidateBYOBRequest(controller) { - if (controller._byobRequest === undefined) { + if (controller._byobRequest === null) { return; } controller._byobRequest._controller = undefined; controller._byobRequest._view = undefined; - controller._byobRequest = undefined; + controller._byobRequest = null; } function ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller) { @@ -1356,7 +1356,7 @@ function SetUpReadableByteStreamController(stream, controller, startAlgorithm, p controller._pullAgain = false; controller._pulling = false; - controller._byobRequest = undefined; + controller._byobRequest = null; // Need to set the slots so that the assert doesn't fire. In the spec the slots already exist implicitly. controller._queue = controller._queueTotalSize = undefined; From c04fd91ec18d131961466b0faae2fd8c026ba667 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Tue, 21 Apr 2020 13:42:19 -0400 Subject: [PATCH 12/30] Fix issues noted by ricea so far --- index.bs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.bs b/index.bs index 9187d9da7..05ab20d23 100644 --- a/index.bs +++ b/index.bs @@ -2138,7 +2138,7 @@ the {{ReadableStream}}'s public API. The underlying issue here is that reading from streams always uses promises for <code>{ value, done }</code> objects, even in specifications. Although it is conceivable we could rephrase all - of the internal algorithms to not use promises and note use JavaScript objects, and instead only + of the internal algorithms to not use promises and not use JavaScript objects, and instead only package up the results into promise-for-<code>{ value, done }</code> when a <code>read()</code> method is called, this would be a large undertaking, which we have not done. See <a href="https://github.com/whatwg/infra/issues/181">whatwg/infra#181</a> for more background on @@ -2317,7 +2317,7 @@ The following abstract operations support the implementation and manipulation of performs the following steps: 1. If ! [$IsReadableStreamLocked$](|stream|) is true, throw a {{TypeError}} exception. - 1. If ! |stream|.\[[readableStreamController]] does not [=implement=] + 1. If |stream|.\[[readableStreamController]] does not [=implement=] {{ReadableByteStreamController}}, throw a {{TypeError}} exception. 1. Perform ! [$ReadableStreamReaderGenericInitialize$](|reader|, |stream|). 1. Set |reader|.\[[readIntoRequests]] to a new empty [=list=]. @@ -3328,7 +3328,7 @@ callback UnderlyingSinkAbortCallback = Promise<void> (optional any reason); will error the stream, instead of letting it close successfully. Throwing an exception is treated the same as returning a rejected promise. - <dt><dfn dict-member for="UnderlyingSink" lt="abort">close(<var ignore>reason</var>)</dfn></dt> + <dt><dfn dict-member for="UnderlyingSink" lt="abort">abort(<var ignore>reason</var>)</dfn></dt> <dd> <p>A function that is called after the [=producer=] signals, via {{WritableStream/abort()|stream.abort()}} or @@ -3415,7 +3415,7 @@ as seen for example in [[#example-ws-no-backpressure]]. |strategy|)</dfn> constructor steps are: 1. If |underlyingSink| is missing, set it to null. - 1. Let |underlyingSinkDict| be |underlyingSinkDict|, [=converted to an IDL value=] of type + 1. Let |underlyingSinkDict| be |underlyingSink|, [=converted to an IDL value=] of type {{UnderlyingSink}}. <p class="note">We cannot declare the |underlyingSink| argument as having the {{UnderlyingSink}} type directly, because doing so would lose the reference to the original object. We need to From 85de416f170663d65e962ee10853a86468ce9eca Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Tue, 21 Apr 2020 17:28:13 -0400 Subject: [PATCH 13/30] Fix typo --- index.bs | 2 +- reference-implementation/web-platform-tests | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 05ab20d23..bdb0be911 100644 --- a/index.bs +++ b/index.bs @@ -4146,7 +4146,7 @@ the {{WritableStream}}'s public API. <h4 id="ws-writer-abstract-ops">Writers</h4> -The following abstract operations support the implementation and manipualtion of +The following abstract operations support the implementation and manipulation of {{WritableStreamDefaultWriter}} instances. <div algorithm> diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index c3ee3b7fc..ef7f4c143 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit c3ee3b7fc723e30afd02f5bc2fd02f389f5aca99 +Subproject commit ef7f4c1434d69075af81af855ca60f3bf4f4132d From 10a22ab0e434d25b2ae389d115c35988ae85424f Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 22 Apr 2020 12:55:52 -0400 Subject: [PATCH 14/30] Ref impl fixes --- .../lib/ReadableByteStreamController-impl.js | 7 ++- .../lib/ReadableStream-impl.js | 6 +-- .../lib/ReadableStreamBYOBReader-impl.js | 2 +- .../lib/abstract-ops/readable-streams.js | 50 +++++++++---------- .../lib/abstract-ops/transform-streams.js | 12 ++--- .../lib/abstract-ops/writable-streams.js | 22 ++++---- .../lib/helpers/webidl.js | 22 -------- reference-implementation/package.json | 2 +- 8 files changed, 50 insertions(+), 73 deletions(-) diff --git a/reference-implementation/lib/ReadableByteStreamController-impl.js b/reference-implementation/lib/ReadableByteStreamController-impl.js index 0cbb4b659..3a6c52db8 100644 --- a/reference-implementation/lib/ReadableByteStreamController-impl.js +++ b/reference-implementation/lib/ReadableByteStreamController-impl.js @@ -1,13 +1,12 @@ 'use strict'; const assert = require('assert'); -const { webidlNew, promiseResolvedWith, promiseRejectedWith } = require('./helpers/webidl.js'); -const { IsDetachedBuffer } = require('./abstract-ops/ecmascript.js'); +const { promiseResolvedWith, promiseRejectedWith } = require('./helpers/webidl.js'); const { CancelSteps, PullSteps } = require('./abstract-ops/internal-methods.js'); const { ResetQueue } = require('./abstract-ops/queue-with-sizes.js'); const aos = require('./abstract-ops/readable-streams.js'); -const ReadableStreamBYOBRequestImpl = require('./ReadableStreamBYOBRequest-impl.js'); +const ReadableStreamBYOBRequest = require('../generated/ReadableStreamBYOBRequest.js'); exports.implementation = class ReadableByteStreamControllerImpl { get byobRequest() { @@ -17,7 +16,7 @@ exports.implementation = class ReadableByteStreamControllerImpl { firstDescriptor.byteOffset + firstDescriptor.bytesFilled, firstDescriptor.byteLength - firstDescriptor.bytesFilled); - const byobRequest = webidlNew(globalThis, 'ReadableStreamBYOBRequest', ReadableStreamBYOBRequestImpl); + const byobRequest = ReadableStreamBYOBRequest.new(globalThis); byobRequest._controller = this; byobRequest._view = view; this._byobRequest = byobRequest; diff --git a/reference-implementation/lib/ReadableStream-impl.js b/reference-implementation/lib/ReadableStream-impl.js index cc7b523de..820babb55 100644 --- a/reference-implementation/lib/ReadableStream-impl.js +++ b/reference-implementation/lib/ReadableStream-impl.js @@ -59,10 +59,10 @@ exports.implementation = class ReadableStreamImpl { } pipeThrough(transform, options) { - // Conversion here is needed until https://github.com/jsdom/webidl2js/issues/81 is fixed. + // Type checking here is needed until https://github.com/jsdom/webidl2js/issues/81 is fixed. if ('signal' in options) { if (!isAbortSignal(options.signal)) { - return promiseRejectedWith(new TypeError('Invalid signal argument')); + throw new TypeError('Invalid signal argument'); } } @@ -83,7 +83,7 @@ exports.implementation = class ReadableStreamImpl { } pipeTo(destination, options) { - // Conversion here is needed until https://github.com/jsdom/webidl2js/issues/81 is fixed. + // Type checking here is needed until https://github.com/jsdom/webidl2js/issues/81 is fixed. if ('signal' in options) { if (!isAbortSignal(options.signal)) { return promiseRejectedWith(new TypeError('Invalid signal argument')); diff --git a/reference-implementation/lib/ReadableStreamBYOBReader-impl.js b/reference-implementation/lib/ReadableStreamBYOBReader-impl.js index cc7fc32d1..dd30fd5fa 100644 --- a/reference-implementation/lib/ReadableStreamBYOBReader-impl.js +++ b/reference-implementation/lib/ReadableStreamBYOBReader-impl.js @@ -29,7 +29,7 @@ exports.implementation = class ReadableStreamBYOBReaderImpl { } if (this._ownerReadableStream === undefined) { - return promiseRejectedWith(readerLockException('cancel')); + return promiseRejectedWith(readerLockException('read')); } return aos.ReadableStreamBYOBReaderRead(this, view); diff --git a/reference-implementation/lib/abstract-ops/readable-streams.js b/reference-implementation/lib/abstract-ops/readable-streams.js index eee1abd81..8a39c65ef 100644 --- a/reference-implementation/lib/abstract-ops/readable-streams.js +++ b/reference-implementation/lib/abstract-ops/readable-streams.js @@ -1,8 +1,8 @@ 'use strict'; const assert = require('assert'); -const { webidlNew, promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, - rejectPromise, uponPromise, setPromiseIsHandledToTrue, waitForAllPromise, transformPromiseWith, uponFulfillment, +const { promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, + uponPromise, setPromiseIsHandledToTrue, waitForAllPromise, transformPromiseWith, uponFulfillment, uponRejection } = require('../helpers/webidl.js'); const { typeIsObject } = require('../helpers/miscellaneous.js'); const { CopyDataBlockBytes, CreateArrayFromList, TransferArrayBuffer } = require('./ecmascript.js'); @@ -13,12 +13,12 @@ const { AcquireWritableStreamDefaultWriter, IsWritableStreamLocked, WritableStre WritableStreamDefaultWriterWrite, WritableStreamCloseQueuedOrInFlight } = require('./writable-streams.js'); const { CancelSteps, PullSteps } = require('./internal-methods.js'); -const ReadableByteStreamControllerImpl = require('../ReadableByteStreamController-impl.js'); -const ReadableStreamBYOBReaderImpl = require('../ReadableStreamBYOBReader-impl.js'); -const ReadableStreamDefaultReaderImpl = require('../ReadableStreamDefaultReader-impl.js'); -const ReadableStreamDefaultControllerImpl = require('../ReadableStreamDefaultController-impl.js'); -const ReadableStreamImpl = require('../ReadableStream-impl.js'); -const WritableStreamImpl = require('../WritableStream-impl.js'); +const ReadableByteStreamController = require('../../generated/ReadableByteStreamController.js'); +const ReadableStreamBYOBReader = require('../../generated/ReadableStreamBYOBReader.js'); +const ReadableStreamDefaultReader = require('../../generated/ReadableStreamDefaultReader.js'); +const ReadableStreamDefaultController = require('../../generated/ReadableStreamDefaultController.js'); +const ReadableStream = require('../../generated/ReadableStream.js'); +const WritableStream = require('../../generated/WritableStream.js'); Object.assign(exports, { AcquireReadableStreamBYOBReader, @@ -64,14 +64,14 @@ Object.assign(exports, { // Working with readable streams function AcquireReadableStreamBYOBReader(stream, forAuthorCode = false) { - const reader = webidlNew(globalThis, 'ReadableStreamBYOBReader', ReadableStreamBYOBReaderImpl); + const reader = ReadableStreamBYOBReader.new(globalThis); SetUpReadableStreamBYOBReader(reader, stream); reader._forAuthorCode = forAuthorCode; return reader; } function AcquireReadableStreamDefaultReader(stream, forAuthorCode = false) { - const reader = webidlNew(globalThis, 'ReadableStreamDefaultReader', ReadableStreamDefaultReaderImpl); + const reader = ReadableStreamDefaultReader.new(globalThis); SetUpReadableStreamDefaultReader(reader, stream); reader._forAuthorCode = forAuthorCode; return reader; @@ -81,10 +81,10 @@ function CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, hi sizeAlgorithm = () => 1) { assert(IsNonNegativeNumber(highWaterMark) === true); - const stream = webidlNew(globalThis, 'ReadableStream', ReadableStreamImpl); + const stream = ReadableStream.new(globalThis); InitializeReadableStream(stream); - const controller = webidlNew(globalThis, 'ReadableStreamDefaultController', ReadableStreamDefaultControllerImpl); + const controller = ReadableStreamDefaultController.new(globalThis); SetUpReadableStreamDefaultController( stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm ); @@ -112,8 +112,8 @@ function IsReadableStreamLocked(stream) { } function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventCancel, signal) { - assert(source instanceof ReadableStreamImpl.implementation); - assert(dest instanceof WritableStreamImpl.implementation); + assert(ReadableStream.isImpl(source)); + assert(WritableStream.isImpl(dest)); assert(typeof preventClose === 'boolean'); assert(typeof preventAbort === 'boolean'); assert(typeof preventCancel === 'boolean'); @@ -315,7 +315,7 @@ function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventC } function ReadableStreamTee(stream, cloneForBranch2) { - assert(stream instanceof ReadableStreamImpl.implementation); + assert(ReadableStream.isImpl(stream)); assert(typeof cloneForBranch2 === 'boolean'); const reader = AcquireReadableStreamDefaultReader(stream); @@ -419,7 +419,7 @@ function ReadableStreamTee(stream, cloneForBranch2) { // Interfacing with controllers function ReadableStreamAddReadIntoRequest(stream) { - assert(stream._reader instanceof ReadableStreamBYOBReaderImpl.implementation); + assert(ReadableStreamBYOBReader.isImpl(stream._reader)); assert(stream._state === 'readable' || stream._state === 'closed'); const promise = newPromise(); @@ -428,7 +428,7 @@ function ReadableStreamAddReadIntoRequest(stream) { } function ReadableStreamAddReadRequest(stream) { - assert(stream._reader instanceof ReadableStreamDefaultReaderImpl.implementation); + assert(ReadableStreamDefaultReader.isImpl(stream._reader)); assert(stream._state === 'readable'); const promise = newPromise(); @@ -463,7 +463,7 @@ function ReadableStreamClose(stream) { return; } - if (reader instanceof ReadableStreamDefaultReaderImpl.implementation) { + if (ReadableStreamDefaultReader.isImpl(reader)) { for (const readRequest of reader._readRequests) { resolvePromise(readRequest, ReadableStreamCreateReadResult(undefined, true, reader._forAuthorCode)); } @@ -497,14 +497,14 @@ function ReadableStreamError(stream, e) { return; } - if (reader instanceof ReadableStreamDefaultReaderImpl.implementation) { + if (ReadableStreamDefaultReader.isImpl(reader)) { for (const readRequest of reader._readRequests) { rejectPromise(readRequest, e); } reader._readRequests = []; } else { - assert(reader instanceof ReadableStreamBYOBReaderImpl.implementation); + assert(ReadableStreamBYOBReader.isImpl(reader)); for (const readIntoRequest of reader._readIntoRequests) { rejectPromise(readIntoRequest, e); @@ -550,7 +550,7 @@ function ReadableStreamHasBYOBReader(stream) { return false; } - if (reader instanceof ReadableStreamBYOBReaderImpl.implementation) { + if (ReadableStreamBYOBReader.isImpl(reader)) { return true; } @@ -564,7 +564,7 @@ function ReadableStreamHasDefaultReader(stream) { return false; } - if (reader instanceof ReadableStreamDefaultReaderImpl.implementation) { + if (ReadableStreamDefaultReader.isImpl(reader)) { return true; } @@ -655,7 +655,7 @@ function SetUpReadableStreamBYOBReader(reader, stream) { throw new TypeError('This stream has already been locked for exclusive reading by another reader'); } - if (!(stream._readableStreamController instanceof ReadableByteStreamControllerImpl.implementation)) { + if (!ReadableByteStreamController.isImpl(stream._readableStreamController)) { throw new TypeError('Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source'); } @@ -872,7 +872,7 @@ function SetUpReadableStreamDefaultControllerFromUnderlyingSource( stream, underlyingSource, underlyingSourceDict, highWaterMark, sizeAlgorithm) { assert(underlyingSource !== undefined); - const controller = webidlNew(globalThis, 'ReadableStreamDefaultController', ReadableStreamDefaultControllerImpl); + const controller = ReadableStreamDefaultController.new(globalThis); let startAlgorithm = () => undefined; let pullAlgorithm = () => promiseResolvedWith(undefined); @@ -1395,7 +1395,7 @@ function SetUpReadableByteStreamController(stream, controller, startAlgorithm, p function SetUpReadableByteStreamControllerFromUnderlyingSource( stream, underlyingSource, underlyingSourceDict, highWaterMark) { - const controller = webidlNew(globalThis, 'ReadableByteStreamController', ReadableByteStreamControllerImpl); + const controller = ReadableByteStreamController.new(globalThis); let startAlgorithm = () => undefined; let pullAlgorithm = () => promiseResolvedWith(undefined); diff --git a/reference-implementation/lib/abstract-ops/transform-streams.js b/reference-implementation/lib/abstract-ops/transform-streams.js index a8a6f8c19..9691cd742 100644 --- a/reference-implementation/lib/abstract-ops/transform-streams.js +++ b/reference-implementation/lib/abstract-ops/transform-streams.js @@ -2,15 +2,15 @@ const assert = require('assert'); const verbose = require('debug')('streams:transform-stream:verbose'); -const { webidlNew, promiseInvoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, - transformPromiseWith } = require('../helpers/webidl.js'); +const { promiseInvoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, transformPromiseWith } = + require('../helpers/webidl.js'); const { CreateReadableStream, ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue, ReadableStreamDefaultControllerError, ReadableStreamDefaultControllerHasBackpressure, ReadableStreamDefaultControllerCanCloseOrEnqueue } = require('./readable-streams.js'); const { CreateWritableStream, WritableStreamDefaultControllerErrorIfNeeded } = require('./writable-streams.js'); -const TransformStreamImpl = require('../TransformStream-impl.js'); -const TransformStreamDefaultControllerImpl = require('../TransformStreamDefaultController-impl.js'); +const TransformStream = require('../generated/TransformStream.js'); +const TransformStreamDefaultController = require('../../generated/TransformStreamDefaultController.js'); Object.assign(exports, { InitializeTransformStream, @@ -103,7 +103,7 @@ function TransformStreamSetBackpressure(stream, backpressure) { // Default controllers function SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm) { - assert(stream instanceof TransformStreamImpl.implementation); + assert(TransformStream.isImpl(stream)); assert(stream._transformStreamController === undefined); controller._controlledTransformStream = stream; @@ -116,7 +116,7 @@ function SetUpTransformStreamDefaultController(stream, controller, transformAlgo function SetUpTransformStreamDefaultControllerFromTransformer(stream, transformer, transformerDict) { assert(transformer !== undefined); - const controller = webidlNew(globalThis, 'TransformStreamDefaultController', TransformStreamDefaultControllerImpl); + const controller = TransformStreamDefaultController.new(globalThis); let transformAlgorithm = chunk => { try { diff --git a/reference-implementation/lib/abstract-ops/writable-streams.js b/reference-implementation/lib/abstract-ops/writable-streams.js index 4a5825145..607abe9f9 100644 --- a/reference-implementation/lib/abstract-ops/writable-streams.js +++ b/reference-implementation/lib/abstract-ops/writable-streams.js @@ -2,15 +2,15 @@ const assert = require('assert'); const verbose = require('debug')('streams:writable-stream:verbose'); -const { webidlNew, promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, - rejectPromise, uponPromise, setPromiseIsHandledToTrue, stateIsPending } = require('../helpers/webidl.js'); +const { promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, + uponPromise, setPromiseIsHandledToTrue, stateIsPending } = require('../helpers/webidl.js'); const { IsNonNegativeNumber } = require('./miscellaneous.js'); const { DequeueValue, EnqueueValueWithSize, PeekQueueValue, ResetQueue } = require('./queue-with-sizes.js'); const { AbortSteps, ErrorSteps } = require('./internal-methods.js'); -const WritableStreamImpl = require('../WritableStream-impl.js'); -const WritableStreamDefaultControllerImpl = require('../WritableStreamDefaultController-impl.js'); -const WritableStreamDefaultWriterImpl = require('../WritableStreamDefaultWriter-impl.js'); +const WritableStream = require('../../generated/WritableStream.js'); +const WritableStreamDefaultController = require('../../generated/WritableStreamDefaultController.js'); +const WritableStreamDefaultWriter = require('../../generated/WritableStreamDefaultWriter.js'); Object.assign(exports, { AcquireWritableStreamDefaultWriter, @@ -36,7 +36,7 @@ Object.assign(exports, { // Working with writable streams function AcquireWritableStreamDefaultWriter(stream) { - const writer = webidlNew(globalThis, 'WritableStreamDefaultWriter', WritableStreamDefaultWriterImpl); + const writer = WritableStreamDefaultWriter.new(globalThis); SetUpWritableStreamDefaultWriter(writer, stream); return writer; } @@ -45,10 +45,10 @@ function CreateWritableStream(startAlgorithm, writeAlgorithm, closeAlgorithm, ab sizeAlgorithm = () => 1) { assert(IsNonNegativeNumber(highWaterMark) === true); - const stream = webidlNew(globalThis, 'WritableStream', WritableStreamImpl); + const stream = WritableStream.new(globalThis); InitializeWritableStream(stream); - const controller = webidlNew(globalThis, 'WritableStreamDefaultController', WritableStreamDefaultControllerImpl); + const controller = WritableStreamDefaultController.new(globalThis); SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm); @@ -92,7 +92,7 @@ function InitializeWritableStream(stream) { } function IsWritableStreamLocked(stream) { - assert(stream instanceof WritableStreamImpl.implementation); + assert(WritableStream.isImpl(stream)); if (stream._writer === undefined) { return false; @@ -524,7 +524,7 @@ function WritableStreamDefaultWriterWrite(writer, chunk) { function SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm) { - assert(stream instanceof WritableStreamImpl.implementation); + assert(WritableStream.isImpl(stream)); assert(stream._writableStreamController === undefined); controller._controlledWritableStream = stream; @@ -568,7 +568,7 @@ function SetUpWritableStreamDefaultControllerFromUnderlyingSink(stream, underlyi highWaterMark, sizeAlgorithm) { assert(underlyingSink !== undefined); - const controller = webidlNew(globalThis, 'WritableStreamDefaultController', WritableStreamDefaultControllerImpl); + const controller = WritableStreamDefaultController.new(globalThis); let startAlgorithm = () => undefined; let writeAlgorithm = () => promiseResolvedWith(undefined); diff --git a/reference-implementation/lib/helpers/webidl.js b/reference-implementation/lib/helpers/webidl.js index 72a5d0858..92c23d6b0 100644 --- a/reference-implementation/lib/helpers/webidl.js +++ b/reference-implementation/lib/helpers/webidl.js @@ -2,30 +2,8 @@ const utils = require('../../generated/utils.js'); const { rethrowAssertionErrorRejection } = require('./miscellaneous.js'); -// https://heycam.github.io/webidl/#new -// https://github.com/jsdom/webidl2js/issues/193 -exports.webidlNew = (globalObject, typeName, implModule) => { - if (globalObject[utils.ctorRegistrySymbol] === undefined) { - throw new Error('Internal error: invalid global object'); - } - - const ctor = globalObject[utils.ctorRegistrySymbol][typeName]; - if (ctor === undefined) { - throw new Error(`Internal error: constructor ${typeName} is not installed on the passed global object`); - } - - const obj = Object.create(ctor.prototype); - Object.defineProperty(obj, utils.implSymbol, { - value: Object.create(implModule.implementation.prototype), - configurable: true - }); - obj[utils.implSymbol][utils.wrapperSymbol] = obj; - return obj[utils.implSymbol]; -}; - const originalPromise = Promise; const originalPromiseThen = Promise.prototype.then; -const originalPromiseResolve = Promise.resolve; const originalPromiseReject = Promise.reject; const promiseSideTable = new WeakMap(); diff --git a/reference-implementation/package.json b/reference-implementation/package.json index b259192a2..89b94de50 100644 --- a/reference-implementation/package.json +++ b/reference-implementation/package.json @@ -18,7 +18,7 @@ "minimatch": "^3.0.4", "nyc": "^15.0.1", "opener": "^1.5.1", - "webidl2js": "^15.2.0", + "webidl2js": "^15.3.0", "wpt-runner": "^3.0.1" }, "nyc": { From 6cf53035c61b6dd6c4e41bb2189ae6ab5991ebe2 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 22 Apr 2020 13:07:43 -0400 Subject: [PATCH 15/30] Make view nullable, not undefined-able --- index.bs | 8 ++++---- .../lib/ReadableStreamBYOBRequest.webidl | 2 +- .../lib/abstract-ops/readable-streams.js | 2 +- .../lib/abstract-ops/transform-streams.js | 2 +- reference-implementation/web-platform-tests | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/index.bs b/index.bs index bdb0be911..473317483 100644 --- a/index.bs +++ b/index.bs @@ -1601,7 +1601,7 @@ The Web IDL definition for the {{ReadableStreamBYOBRequest}} class is given as f <xmp class="idl"> [Exposed=(Window,Worker,Worklet)] interface ReadableStreamBYOBRequest { - readonly attribute ArrayBufferView view; + readonly attribute ArrayBufferView? view; void respond([EnforceRange] unsigned long long bytesWritten); void respondWithNewView(ArrayBufferView view); @@ -1625,7 +1625,7 @@ following table: <tr> <td>\[[view]] <td class="non-normative">A [=typed array=] representing the destination region to which the - controller can write generated data + controller can write generated data, or null after the BYOB request has been invalidated. </table> <h4 id="rs-byob-request-prototype">Methods and properties</h4> @@ -1633,7 +1633,7 @@ following table: <dl class="domintro non-normative"> <dt><code><var ignore>view</var> = <var ignore>byobRequest</var>.{{ReadableStreamBYOBRequest/view}}</code> <dd> - <p>Returns the view for writing in to. + <p>Returns the view for writing in to, or null if the BYOB request has already been responded to. <dt><code><var ignore>byobRequest</var>.{{ReadableStreamBYOBRequest/respond()|respond}}(<var ignore>bytesWritten</var>)</code> <dd> @@ -2798,7 +2798,7 @@ The following abstract operations support the implementation of the 1. If |controller|.\[[byobRequest]] is null, return. 1. Set |controller|.\[[byobRequest]].\[[controller]] to undefined. - 1. Set |controller|.\[[byobRequest]].\[[view]] to undefined. + 1. Set |controller|.\[[byobRequest]].\[[view]] to null. 1. Set |controller|.\[[byobRequest]] to null. </div> diff --git a/reference-implementation/lib/ReadableStreamBYOBRequest.webidl b/reference-implementation/lib/ReadableStreamBYOBRequest.webidl index e7950727d..0f77734e9 100644 --- a/reference-implementation/lib/ReadableStreamBYOBRequest.webidl +++ b/reference-implementation/lib/ReadableStreamBYOBRequest.webidl @@ -1,6 +1,6 @@ [Exposed=(Window,Worker,Worklet)] interface ReadableStreamBYOBRequest { - readonly attribute ArrayBufferView view; + readonly attribute ArrayBufferView? view; void respond([EnforceRange] unsigned long long bytesWritten); void respondWithNewView(ArrayBufferView view); diff --git a/reference-implementation/lib/abstract-ops/readable-streams.js b/reference-implementation/lib/abstract-ops/readable-streams.js index 8a39c65ef..b4e9e6d4a 100644 --- a/reference-implementation/lib/abstract-ops/readable-streams.js +++ b/reference-implementation/lib/abstract-ops/readable-streams.js @@ -1133,7 +1133,7 @@ function ReadableByteStreamControllerInvalidateBYOBRequest(controller) { } controller._byobRequest._controller = undefined; - controller._byobRequest._view = undefined; + controller._byobRequest._view = null; controller._byobRequest = null; } diff --git a/reference-implementation/lib/abstract-ops/transform-streams.js b/reference-implementation/lib/abstract-ops/transform-streams.js index 9691cd742..84809127a 100644 --- a/reference-implementation/lib/abstract-ops/transform-streams.js +++ b/reference-implementation/lib/abstract-ops/transform-streams.js @@ -9,7 +9,7 @@ const { CreateReadableStream, ReadableStreamDefaultControllerClose, ReadableStre ReadableStreamDefaultControllerCanCloseOrEnqueue } = require('./readable-streams.js'); const { CreateWritableStream, WritableStreamDefaultControllerErrorIfNeeded } = require('./writable-streams.js'); -const TransformStream = require('../generated/TransformStream.js'); +const TransformStream = require('../../generated/TransformStream.js'); const TransformStreamDefaultController = require('../../generated/TransformStreamDefaultController.js'); Object.assign(exports, { diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index ef7f4c143..0c47257d2 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit ef7f4c1434d69075af81af855ca60f3bf4f4132d +Subproject commit 0c47257d2218b71451b0fff50444835d9bdb396f From b7ab6bedc62b83a6597d78739bcb37a44a92e93b Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 22 Apr 2020 13:47:39 -0400 Subject: [PATCH 16/30] Add async iterableness to spec (Not yet to reference implementation) --- index.bs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/index.bs b/index.bs index 473317483..48487c205 100644 --- a/index.bs +++ b/index.bs @@ -75,7 +75,7 @@ data, embodied in [=readable streams=], [=writable streams=], and [=transform st These APIs have been designed to efficiently map to low-level I/O primitives, including specializations for byte streams where appropriate. They allow easy composition of multiple streams -into [=pipe chains=], or can be used directly via [=readers=] and [=writers=]. Finally, they are +into [=pipe chains=], or can be used directly via [=/readers=] and [=writers=]. Finally, they are designed to automatically provide [=backpressure=] and queuing. This standard provides the base stream primitives which other parts of the web platform can use to @@ -382,7 +382,7 @@ values. <div class="example" id="example-manual-read"> Although readable streams will usually be used by piping them to a writable stream, you can also - read them directly by acquiring a [=reader=] and using its <code>read()</code> method to get + read them directly by acquiring a [=/reader=] and using its <code>read()</code> method to get successive chunks. For example, this code logs the next [=chunk=] in the stream, if available: <xmp highlight="js"> @@ -463,7 +463,7 @@ interface ReadableStream { Promise<void> pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); sequence<ReadableStream> tee(); - // TODO: async iterator + async iterable<any>(optional ReadableStreamIteratorOptions options = {}); }; typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader; @@ -474,6 +474,10 @@ dictionary ReadableStreamGetReaderOptions { ReadableStreamReaderMode mode; }; +dictionary ReadableStreamIteratorOptions { + boolean preventCancel = false; +}; + dictionary ReadableWritablePair { required ReadableStream readable; required WritableStream writable; @@ -892,7 +896,70 @@ option. If {{UnderlyingSource/type}} is set to undefined (including via omission </div> </div> -<!-- TODO async iterator stuff --> +<h4 id="rs-asynciterator" oldids="rs-asynciterator-prototype, +default-reader-asynciterator-prototype-internal-slots">Asynchronous iteration</h4> + +<dl class="domintro"> + <dt><code>for await (const <var ignore>chunk</var> of <var ignore>stream</var>) { ... }</code> + <dt><code>for await (const <var ignore>chunk</var> of <var ignore>stream</var>.values([{ {{ReadableStreamIteratorOptions/preventCancel}} }])) { ... }</code> + <dd> + <p>Asynchronously iterates over the [=chunks=] in the stream's internal queue. + + <p>Asynchronously iterating over the stream will [=locked to a reader|lock=] it, preventing any + other consumer from acquiring a reader. The lock will be released if the async iterator's + `return()` method is called. + + <p>By default, calling the async iterator's `return()` method will also [=cancel a readable + stream|cancel=] the stream. To prevent this, set {{ReadableStreamIteratorOptions/preventCancel}} + to true. + </dd> +</dl> + +<div algorithm="ReadableStream asynchronous iterator initialization steps" id="rs-get-iterator"> + The [=asynchronous iterator initialization steps=] for a {{ReadableStream}}, given |stream|, + |iterator|, and |args|, are: + + 1. Let |reader| be ? [$AcquireReadableStreamDefaultReader$](|stream|). + 1. Set |iterator|'s <dfn for="ReadableStream async iterator">reader</dfn> to |reader|. + 1. Let |preventCancel| be |args|[0]["{{ReadableStreamIteratorOptions/preventCancel}}"]. + 1. Set |iterator|'s <dfn for="ReadableStream async iterator">prevent cancel</dfn> to + |preventCancel|. +</div> + +<div algorithm="ReadableStream get the next iteration result" id="rs-asynciterator-prototype-next"> + The [=get the next iteration result=] steps for a {{ReadableStream}}, given <var + ignore>stream</var> and |iterator|, are: + + 1. Let |reader| be |iterator|'s [=ReadableStream async iterator/reader=]. + 1. If |reader|.\[[ownerReadableStream]] is undefined, return [=a promise rejected with=] a + {{TypeError}}. + 1. Return the result of [=reacting=] to ! [$ReadableStreamDefaultReaderRead$](|reader|) with the + following fulfillment steps given the argument |result|: + 1. Assert: [$Type$](|result|) is Object. + 1. Let |done| be ! [$Get$](|result|, "`done`"). + 1. Assert: [$Type$](|done|) is Boolean. + 1. If |done| is true, perform [$ReadableStreamReaderGenericRelease$](|reader|). + 1. Let |value| be ! [$Get$](|result|, "`value`"). + 1. Return |value|. +</div> + +<div algorithm="ReadableStream asynchronous iterator return" id="rs-asynciterator-prototype-return"> + The [=asynchronous iterator return=] steps for a {{ReadableStream}}, given <var + ignore>stream</var>, |iterator|, and |arg|, are: + + 1. Let |reader| be |iterator|'s [=ReadableStream async iterator/reader=]. + 1. If |reader|.\[[ownerReadableStream]] is undefined, return [=a promise rejected with=] a + {{TypeError}}. + 1. If |reader|.\[[readRequests]] is not [=list/is empty|empty=], return [=a promise rejected with=] + a {{TypeError}}. + 1. Let |preventCancel| be |iterator|'s [=ReadableStream async iterator/prevent cancel=]. + 1. If |preventCancel| is false: + 1. Let |result| be ! [$ReadableStreamReaderGenericCancel$](|reader|, |arg|). + 1. Perform ! [$ReadableStreamReaderGenericRelease$](|reader|). + 1. Return |result|. + 1. Perform ! [$ReadableStreamReaderGenericRelease$](|reader|). + 1. Return [=a promise resolved with=] undefined. +</div> <h3 id="default-reader-class">The {{ReadableStreamDefaultReader}} class</h3> From 4ff1dc11139bbae7b105268f4f561142cc36f33b Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 22 Apr 2020 13:53:46 -0400 Subject: [PATCH 17/30] Fix example indentation --- index.bs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 48487c205..bb336d9ad 100644 --- a/index.bs +++ b/index.bs @@ -5928,8 +5928,8 @@ function makeReadableByteFileStream(filename) { type: "bytes", async start() { - fileHandle = await fs.open(filename, "r"); - }, + fileHandle = await fs.open(filename, "r"); + }, pull(controller) { // Even when the consumer is using the default reader, the auto-allocation From 28ff9ed5f09a0895966ef19b9fb0b0b8549855eb Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 22 Apr 2020 14:33:35 -0400 Subject: [PATCH 18/30] Tweak domintro --- index.bs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.bs b/index.bs index bb336d9ad..c1fc0ab9c 100644 --- a/index.bs +++ b/index.bs @@ -901,17 +901,17 @@ default-reader-asynciterator-prototype-internal-slots">Asynchronous iteration</h <dl class="domintro"> <dt><code>for await (const <var ignore>chunk</var> of <var ignore>stream</var>) { ... }</code> - <dt><code>for await (const <var ignore>chunk</var> of <var ignore>stream</var>.values([{ {{ReadableStreamIteratorOptions/preventCancel}} }])) { ... }</code> + <dt><code>for await (const <var ignore>chunk</var> of <var ignore>stream</var>.values({ {{ReadableStreamIteratorOptions/preventCancel}}: true })) { ... }</code> <dd> <p>Asynchronously iterates over the [=chunks=] in the stream's internal queue. <p>Asynchronously iterating over the stream will [=locked to a reader|lock=] it, preventing any other consumer from acquiring a reader. The lock will be released if the async iterator's - `return()` method is called. + `return()` method is called, e.g. by `break`ing out of the loop. <p>By default, calling the async iterator's `return()` method will also [=cancel a readable - stream|cancel=] the stream. To prevent this, set {{ReadableStreamIteratorOptions/preventCancel}} - to true. + stream|cancel=] the stream. To prevent this, use the stream's `values()`method, passing true for + the {{ReadableStreamIteratorOptions/preventCancel}} option. </dd> </dl> From 1b7085bc41d4717413c26f3fe3778c9f47effd8d Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 22 Apr 2020 18:55:25 -0400 Subject: [PATCH 19/30] Add back missing space Co-Authored-By: Adam Rice <ricea@chromium.org> --- index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.bs b/index.bs index c1fc0ab9c..89456a7c4 100644 --- a/index.bs +++ b/index.bs @@ -910,7 +910,7 @@ default-reader-asynciterator-prototype-internal-slots">Asynchronous iteration</h `return()` method is called, e.g. by `break`ing out of the loop. <p>By default, calling the async iterator's `return()` method will also [=cancel a readable - stream|cancel=] the stream. To prevent this, use the stream's `values()`method, passing true for + stream|cancel=] the stream. To prevent this, use the stream's `values()` method, passing true for the {{ReadableStreamIteratorOptions/preventCancel}} option. </dd> </dl> From 4748ef03aff4116910c6153d56201f1374d79b13 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 29 Apr 2020 14:27:28 -0400 Subject: [PATCH 20/30] Upgrade webidl2js Fixes a good number of test failures, but still waiting on callback function support --- reference-implementation/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference-implementation/package.json b/reference-implementation/package.json index 89b94de50..05a640c54 100644 --- a/reference-implementation/package.json +++ b/reference-implementation/package.json @@ -18,7 +18,7 @@ "minimatch": "^3.0.4", "nyc": "^15.0.1", "opener": "^1.5.1", - "webidl2js": "^15.3.0", + "webidl2js": "^16.0.0", "wpt-runner": "^3.0.1" }, "nyc": { From 82fac0e107439b0469b77183f26e0377039f4a01 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 29 Apr 2020 14:36:05 -0400 Subject: [PATCH 21/30] Change domintro format for getReader() too --- index.bs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/index.bs b/index.bs index 89456a7c4..8921eca67 100644 --- a/index.bs +++ b/index.bs @@ -664,28 +664,27 @@ option. If {{UnderlyingSource/type}} is set to undefined (including via omission {{TypeError}} (without attempting to cancel the stream) if the stream is currently [=locked to a reader|locked=]. - <dt><code><var ignore>reader</var> = <var ignore>stream</var>.{{ReadableStream/getReader(options)|getReader}}([{ {{ReadableStreamGetReaderOptions/mode}}: "{{ReadableStreamReaderMode/byob}}" }])</code> + <dt><code><var ignore>reader</var> = <var ignore>stream</var>.{{ReadableStream/getReader(options)|getReader}}()</code> <dd> - <p>Creates a reader of the type specified by the {{ReadableStreamGetReaderOptions/mode}} option - and [=locked to a reader|locks=] the stream to the new reader. While the stream is locked, no - other reader can be acquired until this one is [=release a read lock|released=]. + <p>Creates a {{ReadableStreamDefaultReader}} and [=locked to a reader|locks=] the stream to the + new reader. While the stream is locked, no other reader can be acquired until this one is + [=release a read lock|released=]. <p>This functionality is especially useful for creating abstractions that desire the ability to consume a stream in its entirety. By getting a reader for the stream, you can ensure nobody else can interleave reads with yours or cancel the stream, which would interfere with your abstraction. - <p>When {{ReadableStreamGetReaderOptions/mode}} is undefined or not provided, the method creates - a [=default reader=] (an instance of {{ReadableStreamDefaultReader}}). The reader provides the - ability to directly read individual [=chunks=] from the stream via the reader's - {{ReadableStreamDefaultReader/read()}} method. - - <p>When {{ReadableStreamGetReaderOptions/mode}} is "{{ReadableStreamReaderMode/byob}}", the - method creates a [=BYOB reader=] (an instance of {{ReadableStreamBYOBReader}}). This feature only - works on [=readable byte streams=], i.e. streams which were constructed specifically with the - ability to handle "bring your own buffer" reading. The reader provides the ability to directly - read individual [=chunks=] from the stream via the reader's {{ReadableStreamBYOBReader/read()}} - method, into developer-supplied buffers, allowing more precise control over allocation. + <dt><code><var ignore>reader</var> = <var ignore>stream</var>.{{ReadableStream/getReader(options)|getReader}}({ {{ReadableStreamGetReaderOptions/mode}}: "{{ReadableStreamReaderMode/byob}}" })</code> + <dd> + <p>Creates a {{ReadableStreamBYOBReader}} and [=locked to a reader|locks=] the stream to the new + reader. + + <p>This call behaves the same way as the no-argument variant, except that it only works on + [=readable byte streams=], i.e. streams which were constructed specifically with the ability to + handle "bring your own buffer" reading. The returned [=BYOB reader=] provides the ability to + directly read individual [=chunks=] from the stream via its {{ReadableStreamBYOBReader/read()}} + method, into developer-supplied buffers, allowing more precise control over allocation. <dt><code><var ignore>readable</var> = <var ignore>stream</var>.{{ReadableStream/pipeThrough(transform, options)|pipeThrough}}({ {{ReadableWritablePair/writable}}, {{ReadableWritablePair/readable}} }[, { {{StreamPipeOptions/preventClose}}, {{StreamPipeOptions/preventCancel}}, {{StreamPipeOptions/preventCancel}}, {{StreamPipeOptions/signal}} }])</code></dt> <dd> From 9216e46ac2046eef0d39eee34a0f5baab32e8438 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Wed, 29 Apr 2020 15:18:26 -0400 Subject: [PATCH 22/30] More WPT updates --- reference-implementation/web-platform-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index 0c47257d2..4913e2edf 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit 0c47257d2218b71451b0fff50444835d9bdb396f +Subproject commit 4913e2edf9eafd58ca250101632747dbaa9dcb50 From f7e37042587067dfa85cacd6903356aa187ef3e1 Mon Sep 17 00:00:00 2001 From: Domenic Denicola <d@domenic.me> Date: Tue, 5 May 2020 14:06:35 -0400 Subject: [PATCH 23/30] Remove highWaterMark setter --- index.bs | 34 +++++-------------- .../lib/ByteLengthQueuingStrategy.webidl | 2 +- .../lib/CountQueuingStrategy.webidl | 2 +- reference-implementation/web-platform-tests | 2 +- 4 files changed, 12 insertions(+), 28 deletions(-) diff --git a/index.bs b/index.bs index 8921eca67..518804079 100644 --- a/index.bs +++ b/index.bs @@ -5266,6 +5266,11 @@ callback QueuingStrategySize = unrestricted double (optional any chunk);
+
highWaterMark
+
+

A non-negative number indicating the [=high water mark=] of the stream using this queuing + strategy. +

size(chunk) (non-byte streams only)

A function that computes and returns the finite non-negative size of the given [=chunk=] @@ -5284,11 +5289,6 @@ callback QueuingStrategySize = unrestricted double (optional any chunk);

For [=readable byte streams=], this function is not used, as chunks are always measured in bytes. - -

highWaterMark
-
-

A non-negative number indicating the [=high water mark=] of the stream using this queuing - strategy.

Any object with these properties can be used when a queuing strategy object is expected. However, @@ -5349,7 +5349,7 @@ The Web IDL definition for the {{ByteLengthQueuingStrategy}} class is given as f interface ByteLengthQueuingStrategy { constructor(QueuingStrategyInit init); - attribute unrestricted double highWaterMark; + readonly attribute unrestricted double highWaterMark; readonly attribute Function size; }; @@ -5391,12 +5391,8 @@ value given in the constructor. corresponding stream constructor to throw.
highWaterMark = strategy.{{ByteLengthQueuingStrategy/highWaterMark}} -
strategy.{{ByteLengthQueuingStrategy/highWaterMark}} = highWaterMark
-

Returns the [=high water mark=] provided to the constructor, or sets it to a new value. - -

Note that high water marks are only read from [=queuing strategies=] upon stream construction, - i.e. setting this property will not change the behavior of any already-constructed streams. +

Returns the [=high water mark=] provided to the constructor.

strategy.{{ByteLengthQueuingStrategy/size}}(chunk)
@@ -5416,10 +5412,6 @@ value given in the constructor. attribute's getter steps are: 1. Return [=this=].\[[highWaterMark]]. - - Its setter steps are: - - 1. Set [=this=].\[[highWaterMark]] to [=the given value=].
@@ -5471,7 +5463,7 @@ The Web IDL definition for the {{CountQueuingStrategy}} class is given as follow interface CountQueuingStrategy { constructor(QueuingStrategyInit init); - attribute unrestricted double highWaterMark; + readonly attribute unrestricted double highWaterMark; readonly attribute Function size; }; @@ -5513,12 +5505,8 @@ value given in the constructor. corresponding stream constructor to throw.
highWaterMark = strategy.{{CountQueuingStrategy/highWaterMark}} -
strategy.{{CountQueuingStrategy/highWaterMark}} = highWaterMark
-

Returns the [=high water mark=] provided to the constructor, or sets it to a new value. - -

Note that high water marks are only read from [=queuing strategies=] upon stream construction, - i.e. setting this property will not change the behavior of any already-constructed streams. +

Returns the [=high water mark=] provided to the constructor.

strategy.{{CountQueuingStrategy/size}}(chunk)
@@ -5538,10 +5526,6 @@ value given in the constructor. attribute's getter steps are: 1. Return [=this=].\[[highWaterMark]]. - - Its setter steps are: - - 1. Set [=this=].\[[highWaterMark]] to [=the given value=].
diff --git a/reference-implementation/lib/ByteLengthQueuingStrategy.webidl b/reference-implementation/lib/ByteLengthQueuingStrategy.webidl index e712dd3a9..48b90bd09 100644 --- a/reference-implementation/lib/ByteLengthQueuingStrategy.webidl +++ b/reference-implementation/lib/ByteLengthQueuingStrategy.webidl @@ -2,6 +2,6 @@ interface ByteLengthQueuingStrategy { constructor(QueuingStrategyInit init); - attribute unrestricted double highWaterMark; + readonly attribute unrestricted double highWaterMark; readonly attribute Function size; }; diff --git a/reference-implementation/lib/CountQueuingStrategy.webidl b/reference-implementation/lib/CountQueuingStrategy.webidl index a5ba8ec5a..13bb1fd07 100644 --- a/reference-implementation/lib/CountQueuingStrategy.webidl +++ b/reference-implementation/lib/CountQueuingStrategy.webidl @@ -2,6 +2,6 @@ interface CountQueuingStrategy { constructor(QueuingStrategyInit init); - attribute unrestricted double highWaterMark; + readonly attribute unrestricted double highWaterMark; readonly attribute Function size; }; diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index 4913e2edf..81329f7c5 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit 4913e2edf9eafd58ca250101632747dbaa9dcb50 +Subproject commit 81329f7c58c4f8d1f89ff3ce49bdc403f415a456 From 0116889e96ca42a053de11ff8c31523643a35c30 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Tue, 5 May 2020 15:05:13 -0400 Subject: [PATCH 24/30] Use webidl2js v16.1.0 It supports callback functions --- .../lib/TransformStream-impl.js | 4 ++-- .../lib/abstract-ops/queuing-strategy.js | 11 +++++------ .../lib/abstract-ops/readable-streams.js | 18 +++++++++--------- .../lib/abstract-ops/transform-streams.js | 6 +++--- .../lib/abstract-ops/writable-streams.js | 12 ++++++------ .../lib/helpers/webidl.js | 19 ------------------- reference-implementation/package.json | 2 +- .../run-web-platform-tests.js | 13 +++++++++++-- reference-implementation/web-platform-tests | 2 +- 9 files changed, 38 insertions(+), 49 deletions(-) diff --git a/reference-implementation/lib/TransformStream-impl.js b/reference-implementation/lib/TransformStream-impl.js index 2e29fdfe0..75c3a37d3 100644 --- a/reference-implementation/lib/TransformStream-impl.js +++ b/reference-implementation/lib/TransformStream-impl.js @@ -1,6 +1,6 @@ 'use strict'; -const { newPromise, resolvePromise, invoke } = require('./helpers/webidl.js'); +const { newPromise, resolvePromise } = require('./helpers/webidl.js'); const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/queuing-strategy.js'); const aos = require('./abstract-ops/transform-streams.js'); @@ -32,7 +32,7 @@ exports.implementation = class TransformStreamImpl { aos.SetUpTransformStreamDefaultControllerFromTransformer(this, transformer, transformerDict); if ('start' in transformerDict) { - resolvePromise(startPromise, invoke(transformerDict.start, [this._transformStreamController], transformer)); + resolvePromise(startPromise, transformerDict.start.call(transformer, this._transformStreamController)); } else { resolvePromise(startPromise, undefined); } diff --git a/reference-implementation/lib/abstract-ops/queuing-strategy.js b/reference-implementation/lib/abstract-ops/queuing-strategy.js index b7d4af6be..7fdc0a8fa 100644 --- a/reference-implementation/lib/abstract-ops/queuing-strategy.js +++ b/reference-implementation/lib/abstract-ops/queuing-strategy.js @@ -1,5 +1,4 @@ 'use strict'; -const { invoke } = require('../helpers/webidl.js'); exports.ExtractHighWaterMark = (strategy, defaultHWM) => { if (!('highWaterMark' in strategy)) { @@ -15,12 +14,12 @@ exports.ExtractHighWaterMark = (strategy, defaultHWM) => { }; exports.ExtractSizeAlgorithm = strategy => { - if (!('size' in strategy)) { + const { size } = strategy; + + if (!size) { return () => 1; } - return chunk => { - // TODO: manual number conversion won't be necessary when https://github.com/jsdom/webidl2js/pull/123 lands. - return Number(invoke(strategy.size, [chunk])); - }; + // This is silly, but more obviously matches the spec (which distinguishes between algorithms and JS functions). + return chunk => size(chunk); }; diff --git a/reference-implementation/lib/abstract-ops/readable-streams.js b/reference-implementation/lib/abstract-ops/readable-streams.js index b4e9e6d4a..9959abe97 100644 --- a/reference-implementation/lib/abstract-ops/readable-streams.js +++ b/reference-implementation/lib/abstract-ops/readable-streams.js @@ -1,9 +1,9 @@ 'use strict'; const assert = require('assert'); -const { promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, - uponPromise, setPromiseIsHandledToTrue, waitForAllPromise, transformPromiseWith, uponFulfillment, - uponRejection } = require('../helpers/webidl.js'); +const { promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, uponPromise, + setPromiseIsHandledToTrue, waitForAllPromise, transformPromiseWith, uponFulfillment, uponRejection } = + require('../helpers/webidl.js'); const { typeIsObject } = require('../helpers/miscellaneous.js'); const { CopyDataBlockBytes, CreateArrayFromList, TransferArrayBuffer } = require('./ecmascript.js'); const { IsNonNegativeNumber } = require('./miscellaneous.js'); @@ -879,13 +879,13 @@ function SetUpReadableStreamDefaultControllerFromUnderlyingSource( let cancelAlgorithm = () => promiseResolvedWith(undefined); if ('start' in underlyingSourceDict) { - startAlgorithm = () => invoke(underlyingSourceDict.start, [controller], underlyingSource); + startAlgorithm = () => underlyingSourceDict.start.call(underlyingSource, controller); } if ('pull' in underlyingSourceDict) { - pullAlgorithm = () => promiseInvoke(underlyingSourceDict.pull, [controller], underlyingSource); + pullAlgorithm = () => underlyingSourceDict.pull.call(underlyingSource, controller); } if ('cancel' in underlyingSourceDict) { - cancelAlgorithm = reason => promiseInvoke(underlyingSourceDict.cancel, [reason], underlyingSource); + cancelAlgorithm = reason => underlyingSourceDict.cancel.call(underlyingSource, reason); } SetUpReadableStreamDefaultController( @@ -1402,13 +1402,13 @@ function SetUpReadableByteStreamControllerFromUnderlyingSource( let cancelAlgorithm = () => promiseResolvedWith(undefined); if ('start' in underlyingSourceDict) { - startAlgorithm = () => invoke(underlyingSourceDict.start, [controller], underlyingSource); + startAlgorithm = () => underlyingSourceDict.start.call(underlyingSource, controller); } if ('pull' in underlyingSourceDict) { - pullAlgorithm = () => promiseInvoke(underlyingSourceDict.pull, [controller], underlyingSource); + pullAlgorithm = () => underlyingSourceDict.pull.call(underlyingSource, controller); } if ('cancel' in underlyingSourceDict) { - cancelAlgorithm = reason => promiseInvoke(underlyingSourceDict.cancel, [reason], underlyingSource); + cancelAlgorithm = reason => underlyingSourceDict.cancel.call(underlyingSource, reason); } const autoAllocateChunkSize = underlyingSourceDict.autoAllocateChunkSize; diff --git a/reference-implementation/lib/abstract-ops/transform-streams.js b/reference-implementation/lib/abstract-ops/transform-streams.js index 84809127a..2affc0cb1 100644 --- a/reference-implementation/lib/abstract-ops/transform-streams.js +++ b/reference-implementation/lib/abstract-ops/transform-streams.js @@ -2,7 +2,7 @@ const assert = require('assert'); const verbose = require('debug')('streams:transform-stream:verbose'); -const { promiseInvoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, transformPromiseWith } = +const { promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, transformPromiseWith } = require('../helpers/webidl.js'); const { CreateReadableStream, ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue, ReadableStreamDefaultControllerError, ReadableStreamDefaultControllerHasBackpressure, @@ -130,10 +130,10 @@ function SetUpTransformStreamDefaultControllerFromTransformer(stream, transforme let flushAlgorithm = () => promiseResolvedWith(undefined); if ('transform' in transformerDict) { - transformAlgorithm = chunk => promiseInvoke(transformerDict.transform, [chunk, controller], transformer); + transformAlgorithm = chunk => transformerDict.transform.call(transformer, chunk, controller); } if ('flush' in transformerDict) { - flushAlgorithm = () => promiseInvoke(transformerDict.flush, [controller], transformer); + flushAlgorithm = () => transformerDict.flush.call(transformer, controller); } SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm); diff --git a/reference-implementation/lib/abstract-ops/writable-streams.js b/reference-implementation/lib/abstract-ops/writable-streams.js index 607abe9f9..e77cb36d8 100644 --- a/reference-implementation/lib/abstract-ops/writable-streams.js +++ b/reference-implementation/lib/abstract-ops/writable-streams.js @@ -2,8 +2,8 @@ const assert = require('assert'); const verbose = require('debug')('streams:writable-stream:verbose'); -const { promiseInvoke, invoke, promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, - uponPromise, setPromiseIsHandledToTrue, stateIsPending } = require('../helpers/webidl.js'); +const { promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, uponPromise, + setPromiseIsHandledToTrue, stateIsPending } = require('../helpers/webidl.js'); const { IsNonNegativeNumber } = require('./miscellaneous.js'); const { DequeueValue, EnqueueValueWithSize, PeekQueueValue, ResetQueue } = require('./queue-with-sizes.js'); const { AbortSteps, ErrorSteps } = require('./internal-methods.js'); @@ -576,16 +576,16 @@ function SetUpWritableStreamDefaultControllerFromUnderlyingSink(stream, underlyi let abortAlgorithm = () => promiseResolvedWith(undefined); if ('start' in underlyingSinkDict) { - startAlgorithm = () => invoke(underlyingSinkDict.start, [controller], underlyingSink); + startAlgorithm = () => underlyingSinkDict.start.call(underlyingSink, controller); } if ('write' in underlyingSinkDict) { - writeAlgorithm = chunk => promiseInvoke(underlyingSinkDict.write, [chunk, controller], underlyingSink); + writeAlgorithm = chunk => underlyingSinkDict.write.call(underlyingSink, chunk, controller); } if ('close' in underlyingSinkDict) { - closeAlgorithm = () => promiseInvoke(underlyingSinkDict.close, [], underlyingSink); + closeAlgorithm = () => underlyingSinkDict.close.call(underlyingSink); } if ('abort' in underlyingSinkDict) { - abortAlgorithm = reason => promiseInvoke(underlyingSinkDict.abort, [reason], underlyingSink); + abortAlgorithm = reason => underlyingSinkDict.abort.call(underlyingSink, reason); } SetUpWritableStreamDefaultController( diff --git a/reference-implementation/lib/helpers/webidl.js b/reference-implementation/lib/helpers/webidl.js index 92c23d6b0..1e376e840 100644 --- a/reference-implementation/lib/helpers/webidl.js +++ b/reference-implementation/lib/helpers/webidl.js @@ -1,5 +1,4 @@ 'use strict'; -const utils = require('../../generated/utils.js'); const { rethrowAssertionErrorRejection } = require('./miscellaneous.js'); const originalPromise = Promise; @@ -8,24 +7,6 @@ const originalPromiseReject = Promise.reject; const promiseSideTable = new WeakMap(); -// A specialization of https://heycam.github.io/webidl/#invoke-a-callback-function -// Can be replaced when https://github.com/jsdom/webidl2js/pull/123 lands. -exports.promiseInvoke = (func, args, thisArg) => { - args = args.map(utils.tryWrapperForImpl); - try { - return Promise.resolve(Reflect.apply(func, thisArg, args)); - } catch (e) { - return Promise.reject(e); - } -}; - -// A specialization of https://heycam.github.io/webidl/#invoke-a-callback-function -// Can be replaced when https://github.com/jsdom/webidl2js/pull/123 lands. -exports.invoke = (func, args, thisArg) => { - args = args.map(utils.tryWrapperForImpl); - return Reflect.apply(func, thisArg, args); -}; - // https://heycam.github.io/webidl/#a-new-promise function newPromise() { // The stateIsPending tracking only works if we never resolve the promises with other promises. diff --git a/reference-implementation/package.json b/reference-implementation/package.json index 05a640c54..036a0a693 100644 --- a/reference-implementation/package.json +++ b/reference-implementation/package.json @@ -18,7 +18,7 @@ "minimatch": "^3.0.4", "nyc": "^15.0.1", "opener": "^1.5.1", - "webidl2js": "^16.0.0", + "webidl2js": "^16.1.0", "wpt-runner": "^3.0.1" }, "nyc": { diff --git a/reference-implementation/run-web-platform-tests.js b/reference-implementation/run-web-platform-tests.js index d85e49b28..c8520118e 100644 --- a/reference-implementation/run-web-platform-tests.js +++ b/reference-implementation/run-web-platform-tests.js @@ -41,8 +41,17 @@ async function main() { window.eval(bundledJS); }, filter(testPath) { - return !workerTestPattern.test(testPath) && // ignore the worker versions - filterGlobs.some(glob => minimatch(testPath, glob)); + // Ignore the worker versions + if (workerTestPattern.test(testPath)) { + return false; + } + + // Ignore for now: wpt-runner doesn't handle the cross-folder dependencies well. + if (testPath === 'idlharness.any.html') { + return false; + } + + return filterGlobs.some(glob => minimatch(testPath, glob)); } }); diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index 81329f7c5..2c5ebb98a 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit 81329f7c58c4f8d1f89ff3ce49bdc403f415a456 +Subproject commit 2c5ebb98a77d0535574f24f4de966bfee1f02df4 From 1b34185d48e2d1b922fdfbc1e249105ba82faf1d Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Mon, 18 May 2020 18:28:23 -0400 Subject: [PATCH 25/30] Use webidl2js branch which supports async iterators --- index.bs | 5 +- .../lib/ReadableStream-impl.js | 56 ++++++++++++++++++- .../lib/ReadableStream.webidl | 6 +- reference-implementation/package.json | 2 +- reference-implementation/web-platform-tests | 2 +- 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/index.bs b/index.bs index 518804079..543faca32 100644 --- a/index.bs +++ b/index.bs @@ -937,7 +937,7 @@ default-reader-asynciterator-prototype-internal-slots">Asynchronous iteration @@ -951,8 +951,7 @@ default-reader-asynciterator-prototype-internal-slots">Asynchronous iteration { + assert(typeIsObject(result)); + + const { done } = result; + assert(typeof done === 'boolean'); + + if (done === true) { + aos.ReadableStreamReaderGenericRelease(reader); + } + + const { value } = result; + return value; + }); + } + + [idlUtils.asyncIteratorReturn](iterator, arg) { + const reader = iterator._reader; + if (reader._ownerReadableStream === undefined) { + return promiseRejectedWith( + new TypeError('Cannot cancel the async iterator once the reader has been released') + ); + } + + if (reader._readRequests.length > 0) { + return promiseRejectedWith( + new TypeError('Cannot cancel the async iterator with read requests ongoing') + ); + } + + if (iterator._preventCancel === false) { + const result = aos.ReadableStreamReaderGenericCancel(reader, arg); + aos.ReadableStreamReaderGenericRelease(reader); + return result; + } + + aos.ReadableStreamReaderGenericRelease(reader); + return promiseResolvedWith(undefined); + } }; // See pipeTo()/pipeThrough() for why this is needed. diff --git a/reference-implementation/lib/ReadableStream.webidl b/reference-implementation/lib/ReadableStream.webidl index 67ce71741..54d070f82 100644 --- a/reference-implementation/lib/ReadableStream.webidl +++ b/reference-implementation/lib/ReadableStream.webidl @@ -10,7 +10,7 @@ interface ReadableStream { Promise pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); sequence tee(); - // TODO: async iterator + [WebIDL2JSHasReturnSteps] async iterable(optional ReadableStreamIteratorOptions options = {}); }; typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader; @@ -21,6 +21,10 @@ dictionary ReadableStreamGetReaderOptions { ReadableStreamReaderMode mode; }; +dictionary ReadableStreamIteratorOptions { + boolean preventCancel = false; +}; + dictionary ReadableWritablePair { required ReadableStream readable; required WritableStream writable; diff --git a/reference-implementation/package.json b/reference-implementation/package.json index 036a0a693..edda6a444 100644 --- a/reference-implementation/package.json +++ b/reference-implementation/package.json @@ -18,7 +18,7 @@ "minimatch": "^3.0.4", "nyc": "^15.0.1", "opener": "^1.5.1", - "webidl2js": "^16.1.0", + "webidl2js": "jsdom/webidl2js#async-iterable", "wpt-runner": "^3.0.1" }, "nyc": { diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index 2c5ebb98a..2362df670 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit 2c5ebb98a77d0535574f24f4de966bfee1f02df4 +Subproject commit 2362df670690d3ab63a36545b3bd22a80d967447 From 3a0dc281006486fe143bf61d9983e62cc364b92a Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Mon, 18 May 2020 19:03:28 -0400 Subject: [PATCH 26/30] Tweak conventions section --- index.bs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 543faca32..5d03a2386 100644 --- a/index.bs +++ b/index.bs @@ -331,13 +331,16 @@ writers to be acquired. This is done via the This specification depends on the Infra Standard. [[!INFRA]] This specification uses the [=abstract operation=] concept from [[!ECMASCRIPT]] for its internal -algorithms. This includes treating their return values as [=completion records=], the use of ! and -? prefixes for unwrapping those completion records. +algorithms. This includes treating their return values as [=completion records=], and the use of ! +and ? prefixes for unwrapping those completion records. This specification also uses the [=internal slot=] and [=record=] concepts and notation from ECMAScript. (Although, the internal slots are on Web IDL [=platform objects=] instead of ECMAScript objects.) +

The reasons for the usage of these foreign ECMAScript conventions are largely +historical. We urge you to avoid following our example when writing your own web specifications. + Finally, as in [[!ECMASCRIPT]], all numbers are represented as double-precision floating point values, and all arithmetic operations performed on them must be done in the standard way for such values. From d80964ed8ecac8001e957d95afb02fd47e133ce6 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Tue, 19 May 2020 12:21:33 -0400 Subject: [PATCH 27/30] More conventions section tweaks --- index.bs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/index.bs b/index.bs index 5d03a2386..9ccbc9994 100644 --- a/index.bs +++ b/index.bs @@ -37,6 +37,7 @@ urlPrefix: https://tc39.es/ecma262/; spec: ECMASCRIPT text: the current Realm; url: #current-realm text: the typed array constructors table; url: #table-49 text: typed array; url: #sec-typedarray-objects + text: Number type; url: #sec-ecmascript-language-types-number-type type: abstract-op text: CloneArrayBuffer; url: #sec-clonearraybuffer text: CopyDataBlockBytes; url: #sec-copydatablockbytes @@ -330,20 +331,22 @@ writers to be acquired. This is done via the This specification depends on the Infra Standard. [[!INFRA]] -This specification uses the [=abstract operation=] concept from [[!ECMASCRIPT]] for its internal -algorithms. This includes treating their return values as [=completion records=], and the use of ! -and ? prefixes for unwrapping those completion records. +This specification uses the [=abstract operation=] concept from the JavaScript specification for its +internal algorithms. This includes treating their return values as [=completion records=], and the +use of ! and ? prefixes for unwrapping those completion records. [[!ECMASCRIPT]] -This specification also uses the [=internal slot=] and [=record=] concepts and notation from -ECMAScript. (Although, the internal slots are on Web IDL [=platform objects=] instead of ECMAScript -objects.) +This specification also uses the [=internal slot=] and [=record=] concepts and notation from the +JavaScript specification. (Although, the internal slots are on Web IDL [=platform objects=] instead +of on JavaScript objects.) -

The reasons for the usage of these foreign ECMAScript conventions are largely -historical. We urge you to avoid following our example when writing your own web specifications. +

The reasons for the usage of these foreign JavaScript specification conventions are +largely historical. We urge you to avoid following our example when writing your own web +specifications. -Finally, as in [[!ECMASCRIPT]], all numbers are represented as double-precision floating point -values, and all arithmetic operations performed on them must be done in the standard way for such -values. +In this specification, all numbers are represented as double-precision 64-bit IEEE 754 floating +point values (like the JavaScript [=Number type=] or Web IDL {{unrestricted double}} type), and all +arithmetic operations performed on them must be done in the standard way for such values. This is +particularly important for the data structure described in [[#queue-with-sizes]]. [[!IEEE-754]]

Readable streams

From eadc7551badf63f33586eb25e226c9695af1ce96 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Tue, 26 May 2020 11:31:33 -0400 Subject: [PATCH 28/30] Use new Web IDL "end of iteration" for async iterator --- index.bs | 4 +++- reference-implementation/lib/ReadableStream-impl.js | 1 + reference-implementation/web-platform-tests | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/index.bs b/index.bs index 9ccbc9994..2eaf2ecf1 100644 --- a/index.bs +++ b/index.bs @@ -943,7 +943,9 @@ default-reader-asynciterator-prototype-internal-slots">Asynchronous iteration diff --git a/reference-implementation/lib/ReadableStream-impl.js b/reference-implementation/lib/ReadableStream-impl.js index 50e194af2..42dd60b91 100644 --- a/reference-implementation/lib/ReadableStream-impl.js +++ b/reference-implementation/lib/ReadableStream-impl.js @@ -134,6 +134,7 @@ exports.implementation = class ReadableStreamImpl { if (done === true) { aos.ReadableStreamReaderGenericRelease(reader); + return idlUtils.asyncIteratorEOI; } const { value } = result; diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index 2362df670..178dfe0cb 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit 2362df670690d3ab63a36545b3bd22a80d967447 +Subproject commit 178dfe0cb3c90ea4833b4ea8a50b6a0604629aba From bb3766f93c8bc2d8c030bf7b8de0268c6ad91e70 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Fri, 5 Jun 2020 16:56:33 -0400 Subject: [PATCH 29/30] More async iterator updates --- index.bs | 11 +++-- .../lib/ReadableStream-impl.js | 41 ++++++++++--------- reference-implementation/web-platform-tests | 2 +- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/index.bs b/index.bs index 2eaf2ecf1..c0ec6af8a 100644 --- a/index.bs +++ b/index.bs @@ -948,6 +948,10 @@ default-reader-asynciterator-prototype-internal-slots">Asynchronous iteration
@@ -955,10 +959,9 @@ default-reader-asynciterator-prototype-internal-slots">Asynchronous iterationstream, |iterator|, and |arg|, are: 1. Let |reader| be |iterator|'s [=ReadableStream async iterator/reader=]. - 1. If |reader|.\[[ownerReadableStream]] is undefined, return [=a promise rejected with=] a - {{TypeError}}. - 1. If |reader|.\[[readRequests]] is not [=list/is empty|empty=], return [=a promise rejected with=] - a {{TypeError}}. + 1. If |reader|.\[[ownerReadableStream]] is undefined, return [=a promise resolved with=] undefined. + 1. Assert: |reader|.\[[readRequests]] is [=list/is empty|empty=], as the async iterator machinery + guarantees that any previous calls to `next()` have settled before this is called. 1. If |iterator|'s [=ReadableStream async iterator/prevent cancel=] is false: 1. Let |result| be ! [$ReadableStreamReaderGenericCancel$](|reader|, |arg|). 1. Perform ! [$ReadableStreamReaderGenericRelease$](|reader|). diff --git a/reference-implementation/lib/ReadableStream-impl.js b/reference-implementation/lib/ReadableStream-impl.js index 42dd60b91..11d6481c0 100644 --- a/reference-implementation/lib/ReadableStream-impl.js +++ b/reference-implementation/lib/ReadableStream-impl.js @@ -126,35 +126,36 @@ exports.implementation = class ReadableStreamImpl { ); } - return transformPromiseWith(aos.ReadableStreamDefaultReaderRead(reader), result => { - assert(typeIsObject(result)); - - const { done } = result; - assert(typeof done === 'boolean'); - - if (done === true) { + return transformPromiseWith( + aos.ReadableStreamDefaultReaderRead(reader), + result => { + assert(typeIsObject(result)); + + const { done } = result; + assert(typeof done === 'boolean'); + + if (done === true) { + aos.ReadableStreamReaderGenericRelease(reader); + return idlUtils.asyncIteratorEOI; + } + + const { value } = result; + return value; + }, + reason => { aos.ReadableStreamReaderGenericRelease(reader); - return idlUtils.asyncIteratorEOI; + throw reason; } - - const { value } = result; - return value; - }); + ); } [idlUtils.asyncIteratorReturn](iterator, arg) { const reader = iterator._reader; if (reader._ownerReadableStream === undefined) { - return promiseRejectedWith( - new TypeError('Cannot cancel the async iterator once the reader has been released') - ); + return promiseResolvedWith(undefined); } - if (reader._readRequests.length > 0) { - return promiseRejectedWith( - new TypeError('Cannot cancel the async iterator with read requests ongoing') - ); - } + assert(reader._readRequests.length === 0); if (iterator._preventCancel === false) { const result = aos.ReadableStreamReaderGenericCancel(reader, arg); diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index 178dfe0cb..ec5a2e314 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit 178dfe0cb3c90ea4833b4ea8a50b6a0604629aba +Subproject commit ec5a2e31409c0f3cba15a9196cc1ddbd104530c7 From 1da8031d34d8fd20f109303b68b2e2e18d7d81f9 Mon Sep 17 00:00:00 2001 From: Domenic Denicola Date: Thu, 11 Jun 2020 11:32:31 -0400 Subject: [PATCH 30/30] Sync webidl2js and WPT! --- reference-implementation/package.json | 2 +- reference-implementation/web-platform-tests | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reference-implementation/package.json b/reference-implementation/package.json index edda6a444..563971428 100644 --- a/reference-implementation/package.json +++ b/reference-implementation/package.json @@ -18,7 +18,7 @@ "minimatch": "^3.0.4", "nyc": "^15.0.1", "opener": "^1.5.1", - "webidl2js": "jsdom/webidl2js#async-iterable", + "webidl2js": "^16.2.0", "wpt-runner": "^3.0.1" }, "nyc": { diff --git a/reference-implementation/web-platform-tests b/reference-implementation/web-platform-tests index ec5a2e314..887350c2f 160000 --- a/reference-implementation/web-platform-tests +++ b/reference-implementation/web-platform-tests @@ -1 +1 @@ -Subproject commit ec5a2e31409c0f3cba15a9196cc1ddbd104530c7 +Subproject commit 887350c2f46def5b01c4dd1f8d2eee35dfb9c5bb