├── .editorconfig
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── 0-new-issue.yml
│ ├── 1-new-feature.yml
│ └── config.yml
└── workflows
│ ├── build.yml
│ └── test.yml
├── .gitignore
├── .gitmodules
├── .pr-preview.json
├── CONTRIBUTING.md
├── FAQ.md
├── LICENSE
├── Makefile
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── Requirements.md
├── byte-streams-explainer.md
├── demos
├── append-child.html
├── append-child.mp4
├── data
│ ├── commits.include
│ └── commits.json
├── index.html
├── resources
│ ├── commits.css
│ ├── common.css
│ ├── highlight-default.css
│ ├── highlight.pack.js
│ ├── jank-meter.css
│ ├── web-animations.min.js
│ └── web-animations.min.js.map
├── streaming-element-backpressure.html
├── streaming-element-backpressure.mp4
├── streaming-element.html
├── streaming-element.mp4
├── tags
│ ├── streaming-element-backpressure.js
│ ├── streaming-element.js
│ └── view-source.js
└── transforms
│ ├── parse-json.js
│ ├── split-stream.js
│ ├── text-encode-transform.js
│ └── transform-stream-polyfill.js
├── index.bs
├── readable-stream-async-iteration-explainer.md
├── reference-implementation
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── COPYING.txt
├── LICENSE.md
├── README.md
├── compile-idl.js
├── lib
│ ├── ByteLengthQueuingStrategy-impl.js
│ ├── ByteLengthQueuingStrategy.webidl
│ ├── CountQueuingStrategy-impl.js
│ ├── CountQueuingStrategy.webidl
│ ├── QueuingStrategy.webidl
│ ├── QueuingStrategyInit.webidl
│ ├── ReadableByteStreamController-impl.js
│ ├── ReadableByteStreamController.webidl
│ ├── ReadableStream-impl.js
│ ├── ReadableStream.webidl
│ ├── ReadableStreamBYOBReader-impl.js
│ ├── ReadableStreamBYOBReader.webidl
│ ├── ReadableStreamBYOBRequest-impl.js
│ ├── ReadableStreamBYOBRequest.webidl
│ ├── ReadableStreamDefaultController-impl.js
│ ├── ReadableStreamDefaultController.webidl
│ ├── ReadableStreamDefaultReader-impl.js
│ ├── ReadableStreamDefaultReader.webidl
│ ├── ReadableStreamGenericReader-impl.js
│ ├── ReadableStreamGenericReader.webidl
│ ├── ReadableStreamReadResult.webidl
│ ├── TransformStream-impl.js
│ ├── TransformStream.webidl
│ ├── TransformStreamDefaultController-impl.js
│ ├── TransformStreamDefaultController.webidl
│ ├── Transformer.webidl
│ ├── UnderlyingSink.webidl
│ ├── UnderlyingSource.webidl
│ ├── WritableStream-impl.js
│ ├── WritableStream.webidl
│ ├── WritableStreamDefaultController-impl.js
│ ├── WritableStreamDefaultController.webidl
│ ├── WritableStreamDefaultWriter-impl.js
│ ├── WritableStreamDefaultWriter.webidl
│ ├── abstract-ops
│ │ ├── ecmascript.js
│ │ ├── internal-methods.js
│ │ ├── miscellaneous.js
│ │ ├── queue-with-sizes.js
│ │ ├── queuing-strategy.js
│ │ ├── readable-streams.js
│ │ ├── transform-streams.js
│ │ └── writable-streams.js
│ ├── helpers
│ │ ├── miscellaneous.js
│ │ └── webidl.js
│ └── index.js
├── package.json
└── run-web-platform-tests.js
├── review-drafts
├── 2018-08.bs
├── 2019-02.bs
├── 2019-08.bs
├── 2020-02.bs
├── 2020-08.bs
├── 2021-02.bs
├── 2021-08.bs
├── 2022-02.bs
├── 2022-08.bs
├── 2023-02.bs
├── 2023-08.bs
├── 2024-02.bs
├── 2024-08.bs
└── 2025-02.bs
├── streams-for-raw-video-explainer.md
├── transferable-streams-explainer.md
└── writable-stream-abort-signal-explainer.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | charset = utf-8
7 | indent_size = 2
8 | indent_style = space
9 | trim_trailing_whitespace = true
10 | max_line_length = 100
11 |
12 | [Makefile]
13 | indent_style = tab
14 |
15 | [*.bs]
16 | indent_size = 1
17 |
18 | [*.py]
19 | indent_size = 4
20 |
21 | [*.js]
22 | max_line_length = 120
23 |
24 | [.gitmodules]
25 | indent_style = tab
26 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.bs diff=html linguist-language=HTML
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/0-new-issue.yml:
--------------------------------------------------------------------------------
1 | name: New issue
2 | description: File a new issue against the Streams Standard.
3 | body:
4 | - type: markdown
5 | attributes:
6 | value: |
7 | Before filling out this form, please familiarize yourself with the [Code of Conduct](https://whatwg.org/code-of-conduct). You might also find the [FAQ](https://whatwg.org/faq) and [Working Mode](https://whatwg.org/working-mode) useful.
8 |
9 | If at any point you have questions, please reach out to us on [Chat](https://whatwg.org/chat).
10 | - type: textarea
11 | attributes:
12 | label: "What is the issue with the Streams Standard?"
13 | validations:
14 | required: true
15 | - type: markdown
16 | attributes:
17 | value: "Thank you for taking the time to improve the Streams Standard!"
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1-new-feature.yml:
--------------------------------------------------------------------------------
1 | name: New feature
2 | description: Request a new feature in the Streams Standard.
3 | labels: ["addition/proposal", "needs implementer interest"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Before filling out this form, please familiarize yourself with the [Code of Conduct](https://whatwg.org/code-of-conduct), [FAQ](https://whatwg.org/faq), and [Working Mode](https://whatwg.org/working-mode). They help with setting expectations and making sure you know what is required. The FAQ ["How should I go about proposing new features to WHATWG standards?"](https://whatwg.org/faq#adding-new-features) is especially relevant.
9 |
10 | If at any point you have questions, please reach out to us on [Chat](https://whatwg.org/chat).
11 | - type: textarea
12 | attributes:
13 | label: "What problem are you trying to solve?"
14 | validations:
15 | required: true
16 | - type: textarea
17 | attributes:
18 | label: "What solutions exist today?"
19 | - type: textarea
20 | attributes:
21 | label: "How would you solve it?"
22 | - type: textarea
23 | attributes:
24 | label: "Anything else?"
25 | - type: markdown
26 | attributes:
27 | value: "Thank you for taking the time to improve the Streams Standard!"
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Chat
4 | url: https://whatwg.org/chat
5 | about: Please do reach out with questions and feedback!
6 | - name: Stack Overflow
7 | url: https://stackoverflow.com/
8 | about: If you're having trouble building a web page, this is not the right repository. Consider asking your question on Stack Overflow instead.
9 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches:
9 | - main
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build:
14 | name: Build
15 | runs-on: ubuntu-22.04
16 | steps:
17 | - uses: actions/checkout@v3
18 | with:
19 | fetch-depth: 2
20 | - uses: actions/setup-python@v4
21 | with:
22 | python-version: "3.11"
23 | - run: pip install bikeshed && bikeshed update
24 | # Note: `make deploy` will do a deploy dry run on PRs.
25 | - run: make deploy
26 | env:
27 | SERVER: ${{ secrets.MARQUEE_SERVER }}
28 | SERVER_PUBLIC_KEY: ${{ secrets.MARQUEE_PUBLIC_KEY }}
29 | SERVER_DEPLOY_KEY: ${{ secrets.MARQUEE_DEPLOY_KEY }}
30 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | pull_request:
4 | branches:
5 | - main
6 | push:
7 | branches:
8 | - main
9 | jobs:
10 | test:
11 | name: Test
12 | runs-on: ubuntu-latest
13 | defaults:
14 | run:
15 | working-directory: reference-implementation
16 | steps:
17 | - uses: actions/checkout@v4
18 | with:
19 | submodules: true
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: 22
23 | - run: npm install
24 | - run: npm test
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /streams.spec.whatwg.org/
2 | /deploy.sh
3 | /index.html
4 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "reference-implementation/web-platform-tests"]
2 | path = reference-implementation/web-platform-tests
3 | url = https://github.com/web-platform-tests/wpt.git
4 |
--------------------------------------------------------------------------------
/.pr-preview.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_file": "index.bs",
3 | "type": "bikeshed",
4 | "params": {
5 | "force": 1,
6 | "md-status": "LS-PR",
7 | "md-Text-Macro": "PR-NUMBER {{ pull_request.number }}"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Streams Standard contributor guidelines
2 |
3 | These are the guidelines for contributing to the Streams Standard. First see the [WHATWG contributor guidelines](https://github.com/whatwg/meta/blob/main/CONTRIBUTING.md).
4 |
5 | We label [good first issues](https://github.com/whatwg/streams/labels/good%20first%20issue) that you could help us fix, to get a taste for how to submit pull requests, how the build process works, and so on.
6 |
7 | ## Spec editorial conventions
8 |
9 | In general, follow existing practice.
10 |
11 | Wrap lines to 100 columns.
12 |
13 | Use single-space indents.
14 |
15 | Do not introduce explicit `
` tags. (Bikeshed does this automatically.)
16 |
17 | Mark up definitions [appropriately](https://speced.github.io/bikeshed/#definitions). This is mostly applicable when defining new classes or methods; follow the existing examples in the spec.
18 |
19 | Use cross-reference [autolinking](https://speced.github.io/bikeshed/#autolinking) liberally. This generally amounts to writing references to "definitions" as `[=term=]`, and writing references to classes or methods as `{{ClassName}}` or `{{ClassName/methodName()}}`.
20 |
21 | When writing examples or notes, JavaScript variables and values are enclosed in `` tags, not in `` tags.
22 |
23 | Use abstract operations to factor out shared logic used by multiple public APIs, or by multiple other abstract operations.
24 |
25 | ## Reference implementation style
26 |
27 | Wrap lines to 120 columns.
28 |
29 | Use two-space indents.
30 |
31 | Alphabetize imports.
32 |
33 | Use single quotes.
34 |
35 | Pass ESLint.
36 |
--------------------------------------------------------------------------------
/FAQ.md:
--------------------------------------------------------------------------------
1 | # Streams API FAQ
2 |
3 | This is a work in progress, documenting some design decisions that were made and that were non-obvious enough that we feel the need to explain them. We'll probably be adding to it as we go. If there's something you feel belongs here, please file an issue or pull request and we'll add it!
4 |
5 | ## Are readable streams appropriate for my use case?
6 |
7 | Readable streams fit best in situations where:
8 |
9 | - You are trying to represent some underlying I/O source of data, especially one for which backpressure is meaningful.
10 | - Consumers care about the logical concatenation of all chunks of the stream, such that every value produced is important.
11 | - Your usual use case is a single consumer, with allowances for multi-consumer via teeing.
12 | - Errors are either final and non-recoverable (socket prematurely closed), or just a specialized type of data (404).
13 |
14 | Notably, readable streams are _not_ a good fit for:
15 |
16 | - Continuous "snapshots" of some varying quantity, where consumers tune in and out as appropriate.
17 | - Broadcasts to a large number of independent consumers.
18 |
19 | In these types of scenarios, the buffering and backpressure built in to readable streams is unnecessary and problematic, and the reader design becomes awkward to work with.
20 |
21 | ## Are writable streams appropriate for my use case?
22 |
23 | Writable streams shine in situations where:
24 |
25 | - You are trying to represent some underlying I/O sink for data, especially one which cannot accept that data instantly.
26 | - Your sink must receive the sequence of chunks written in the order provided.
27 | - Your sink has a way of acknowledging successful writes, or at least receipt of the data.
28 | - Your sink requires a "close" signal to release resources or similar.
29 |
30 | ## Are streams only for byte data?
31 |
32 | No! Although streams are designed around I/O use cases, the APIs in the Streams Standard accommodate other types of chunks flowing through the streams just as well. Although usually the original source of or ultimate sink for I/O deals in bytes, there are many steps in between where streaming APIs are important, and they are meant to play well there too.
33 |
34 | For example, your program could easily have readable stream of JavaScript objects representing users. This stream was probably originally derived from a readable stream of bytes coming from a HTTP request; maybe there was even an intermediate stream of decoded text. But the same APIs work for all of these streams: it is only the chunk type that differs.
35 |
36 | A key part of allowing this flexibility in the API is the concept of a _queuing strategy_. A stream's queuing strategy is responsible for knowing what type of chunks it can process, and crucially, for measuring their size. This is what allows backpressure to work, without us coding knowledge of every possible chunk type into the Streams Standard.
37 |
38 | ## What's the deal with byte streams then?
39 |
40 | That is: given that the stream APIs work great for any type of data, why are there special APIs, like BYOB readers, for dealing specifically with byte streams?
41 |
42 | The answer is that there are certain operations, mostly around memory management, that only make sense when dealing directly with bytes. So the reader returned by `getReader({ mode: "byob" })`, or the controller interface provided when you give the constructor the `type: "bytes"` option, provide _extra_ APIs on top of the normal ones, which can be used by consumers and producers that are specifically interested in working at that level. But generally, your code can ignore the difference—a readable byte stream will act exactly like a normal readable stream, if you simply use `.getReader()` with no `mode` option.
43 |
44 | ## How do readable streams relate to [observables](https://github.com/zenparsing/es-observable) or EventTarget?
45 |
46 | Readable streams and observables/EventTarget are not terribly related. Observables/EventTarget are specifically designed for multi-consumer scenarios, and have no support for backpressure. They are generally a bad fit for anything related to I/O, whereas streams excel there.
47 |
48 | Observables and readable streams both share the semantic of "zero or more chunks, followed by either an error or done signal". But beyond that, they are not very comparable.
49 |
50 | ## How do readable streams relate to [async iterables](https://github.com/zenparsing/async-iteration/)?
51 |
52 | Readable streams are conceptually a special case of async iterables, with a focus on I/O. The best analogy is something like "readable streams is to async iterable as array is to sync iterable." That is, just like arrays are a specialized type of iterable optimized for memory locality and random access, readable streams are a specialized type of async iterable optimized for things like off-main-thread data transfer and precise backpressure signaling.
53 |
54 | As the async iterable proposal progresses, we anticipate making readable streams into async iterables proper, so that you could use the proposed `for async (const chunk of rs)` syntax.
55 |
56 | ## What are the main differences between these streams and Node.js streams?
57 |
58 | The Streams Standard specifically started with the Node.js streams model as the basis, vowing to learn the lessons from Node's iterative streams1/2/3 process and incorporate them. Over time it's diverged in a few more ways, often via urging from the Node.js team. At this point, the main differences are:
59 |
60 | - Readable streams:
61 | - An asynchronous `.read()` method, instead of async `.on("readable", ...)` plus synchronous `.read()`
62 | - Addition of the exclusive reader and locking concepts, to better support off-main-thread piping
63 | - Addition of cancelation semantics
64 | - Addition of more precise flow control via the `desiredSize` signal
65 | - Built-in teeing support
66 | - Removal of the `"data"` event, which competes conceptually with other ways of reading
67 | - Removal of pause/resume for managing backpressure
68 | - Removal of the `unshift` method for putting chunks back into the queue after reading them
69 | - No "binary/string mode" vs. "object mode" switch; instead, queueing strategies allow custom chunk types
70 | - No optional and only sometimes-working size parameter while reading; instead use BYOB readers
71 | - Writable streams:
72 | - No cork/uncork support, yet (this is [#27](https://github.com/whatwg/streams/issues/27))
73 | - Transform streams:
74 | - No longer a mash of the readable and writable stream APIs, but instead simple `{ readable, writable }` objects where the component streams are connected behind the scenes
75 | - General:
76 | - Modernized API to use promises instead of callbacks and one-time events
77 | - No built-in text encoding/decoding
78 | - Split the `pipe` method into `pipeTo(writable)` and `pipeThrough(transform)`
79 |
80 | ## Why don't errors that occur while cancelling put the readable stream in an error state?
81 |
82 | The idea of cancelling a readable stream is that it should behave as a "loss of interest": the consumer cancelling the stream expects nobody will read from it further, and that the stream should be treated in the same way as if it had closed naturally. Thus, cancellation _immediately_ moves the stream into a `"closed"` state, which has the same properties as if the stream had closed itself. This gives the most consistent view of the stream to the outside world.
83 |
84 | On the other hand, it may be important for the consumer _performing_ the cancellation to be notified whether the cancellation succeeds or fails. To handle this, you can simply use the promise returned from `.cancel()`:
85 |
86 | ```js
87 | readableStream.cancel().then(
88 | () => console.log("Cancellation successful!"),
89 | err => console.error("Cancellation failed!", err)
90 | );
91 | ```
92 |
93 | ## What's with `pipeTo` vs `pipeThrough`?
94 |
95 | There are only two types of streams: readable and writable streams. `pipeTo` is for piping between them.
96 |
97 | For the concept of something with a writable end and a readable end, we have "duplex streams." Duplex streams are really just containers for a pair of streams, one writable and one readable, stored in the properties `writable` and `readable` respectively.
98 |
99 | Some duplex streams will be transform streams, wherein the two streams are entangled, so that writing to the writable side affects what can be read from the readable side. This could be a very direct entanglement, of the sort produced by the `TransformStream` class, or something more indirect, such as the relationship between `stdin` and `stdout`.
100 |
101 | `pipeThrough` is for piping into the writable half of the entangled streams and out the readable side. That is,
102 |
103 | ```js
104 | src.pipeThrough(through).pipeTo(dest);
105 | ```
106 |
107 | is really just sugar for:
108 |
109 | ```js
110 | src.pipeTo(through.writable);
111 | through.readable.pipeTo(dest);
112 | ```
113 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL=/bin/bash -o pipefail
2 | .PHONY: local remote deploy
3 |
4 | remote: index.bs
5 | @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \
6 | --output index.html \
7 | --write-out "%{http_code}" \
8 | --header "Accept: text/plain, text/html" \
9 | -F die-on=warning \
10 | -F md-Text-Macro="COMMIT-SHA LOCAL COPY" \
11 | -F file=@index.bs) && \
12 | [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \
13 | echo ""; cat index.html; echo ""; \
14 | rm -f index.html; \
15 | exit 22 \
16 | );
17 |
18 | local: index.bs
19 | bikeshed spec index.bs index.html --md-Text-Macro="COMMIT-SHA LOCAL-COPY"
20 |
21 | deploy: index.bs
22 | curl --remote-name --fail https://resources.whatwg.org/build/deploy.sh
23 | EXTRA_FILES="demos/* demos/**/*" \
24 | bash ./deploy.sh
25 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | - [ ] At least two implementers are interested (and none opposed):
8 | * …
9 | * …
10 | - [ ] [Tests](https://github.com/web-platform-tests/wpt) are written and can be reviewed and commented upon at:
11 | * …
12 | - [ ] [Implementation bugs](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md#handling-pull-requests) are filed:
13 | * Chromium: …
14 | * Gecko: …
15 | * WebKit: …
16 | * Deno: …
17 | * Node.js: …
18 | - [ ] [MDN issue](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md#handling-pull-requests) is filed: …
19 | - [ ] The top of this comment includes a [clear commit message](https://github.com/whatwg/meta/blob/main/COMMITTING.md) to use.
20 |
21 | (See [WHATWG Working Mode: Changes](https://whatwg.org/working-mode#changes) for more details.)
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Streams Standard
2 |
3 | This repository hosts the [Streams Standard](https://streams.spec.whatwg.org/).
4 |
5 | ## Contribution opportunities
6 |
7 | Folks notice minor and larger issues with the Streams Standard all the time and we'd love your
8 | help fixing those. Pull requests for typographical and grammar errors are also most welcome.
9 |
10 | We'd be happy to mentor you through this process. If you're interested and need help getting
11 | started, leave a comment on the issue or ask around
12 | [in the Matrix chat room](https://whatwg.org/chat).
13 |
14 | ## Pull requests
15 |
16 | In short, change `index.bs` and submit your patch, with a
17 | [good commit message](https://github.com/whatwg/meta/blob/main/COMMITTING.md). Consider
18 | reading through the [WHATWG FAQ](https://whatwg.org/faq) if you are new here.
19 |
20 | Please add your name to the Acknowledgments section in your first pull request, even for trivial
21 | fixes. The names are sorted lexicographically.
22 |
23 | For formatting specific to this standard, see [CONTRIBUTING.md](CONTRIBUTING.md). Additionally, for
24 | any normative changes, we'll also want to change the tests and reference implementation:
25 |
26 | ## Tests
27 |
28 | Tests can be found in the `streams/` directory of
29 | [`web-platform-tests/wpt`](https://github.com/web-platform-tests/wpt).
30 |
31 | ## Reference implementation
32 |
33 | This repository also includes a reference implementation, written in JavaScript, under
34 | `reference-implementation/`. See the README under that directory for more details.
35 |
36 | We strive for every commit that changes the spec to also add tests, and to change the reference
37 | implementation in order to pass those tests.
38 |
39 | ## Building "locally"
40 |
41 | For quick local iteration, run `make`. To verify your changes locally, run `make deploy`. See more
42 | in the
43 | [WHATWG Contributor Guidelines](https://github.com/whatwg/meta/blob/main/CONTRIBUTING.md#building).
44 |
45 | ## Merge policy
46 |
47 | If you can commit to this repository, see the
48 | [WHATWG Maintainer Guidelines](https://github.com/whatwg/meta/blob/main/MAINTAINERS.md).
49 |
50 | ## Code of conduct
51 |
52 | We are committed to providing a friendly, safe, and welcoming environment for all. Please read and
53 | respect the [WHATWG Code of Conduct](https://whatwg.org/code-of-conduct).
54 |
--------------------------------------------------------------------------------
/byte-streams-explainer.md:
--------------------------------------------------------------------------------
1 | # Byte Streams Explainer
2 |
3 |
4 | ## Introduction
5 |
6 | The streams APIs provide ubiquitous, interoperable primitives for creating, composing, and consuming streams of data.
7 | For streams representing bytes, readable byte streams are an extended version of readable streams which are provided to
8 | handle bytes efficiently.
9 |
10 | Byte streams allow for BYOB (bring-your-own-buffer) readers to be acquired. The default stream type can give a range of
11 | different outputs, for example strings or array buffers in the case of WebSockets, whereas byte streams guarantee byte
12 | output. Furthermore, being able to have BYOB readers has benefits in terms of stability. This is because if a buffer
13 | detaches, it can guarantee that one does not write into the same buffer twice, hence avoiding race conditions. BYOB
14 | readers can reduce the number of times we run garbage collection, because we can reuse buffers.
15 |
16 | ## API Proposed
17 |
18 | * Constructing a Readable Byte Stream
19 | * new [ReadableStream](https://streams.spec.whatwg.org/#rs-constructor)({ type: "bytes" })
20 | * Getting a BYOB reader
21 | * [getReader](https://streams.spec.whatwg.org/#rs-get-reader)({ mode: "byob" })
22 | * As part of the implementation, there are 3 main classes that will be added to the Streams API:
23 | * The [ReadableStreamBYOBReader](https://streams.spec.whatwg.org/#byob-reader-class) class
24 | * This class represents a BYOB reader designed to be vended by a `ReadableStream` instance.
25 | * The [ReadableByteStreamController](https://streams.spec.whatwg.org/#rbs-controller-class) class
26 | * This class has methods that allow control of a `ReadableStream`’s state and internal queue. When
27 | constructing a `ReadableStream` that is a readable byte stream, the underlying source is given a corresponding
28 | `ReadableByteStreamController` instance to manipulate.
29 | * The [ReadableStreamBYOBRequest](https://streams.spec.whatwg.org/#rs-byob-request-class) class
30 | * This class represents a pull-into request in a `ReadableByteStreamController`.
31 |
32 |
33 | ## Examples
34 |
35 | These are a few examples of Javascript which can be used for byte streams once this is implemented:
36 |
37 |
38 | ### Reading bytes from the stream into a single memory buffer
39 |
40 | The code reads the first 1024 bytes from the stream into a single memory buffer. This is due to the fact that if a
41 | stream is a readable byte stream, you can also acquire a BYOB reader for it, which allows more precise control over
42 | buffer allocation in order to avoid copies.
43 |
44 |
45 | ```javascript
46 | const reader = readableStream.getReader({ mode: "byob" });
47 |
48 | let startingAB = new ArrayBuffer(1024);
49 | const buffer = await readInto(startingAB);
50 | console.log("The first 1024 bytes: ", buffer);
51 |
52 | async function readInto(buffer) {
53 | let offset = 0;
54 |
55 | while (offset < buffer.byteLength) {
56 | const {value: view, done} =
57 | await reader.read(new Uint8Array(buffer, offset, buffer.byteLength - offset));
58 | buffer = view.buffer;
59 | if (done) {
60 | break;
61 | }
62 | offset += view.byteLength;
63 | }
64 |
65 | return buffer;
66 | }
67 | ```
68 |
69 |
70 | Note that after this code has run, `startingAB` is detached and can no longer be used to access the data, but `buffer`
71 | points to the same region of memory.
72 |
73 |
74 | ### A readable byte stream with an underlying pull source
75 |
76 | The following function returns readable byte streams that allow efficient zero-copy reading of a randomly generated
77 | array. Instead of using a predetermined chunk size of 1024, it attempts to fill the developer-supplied buffer,
78 | allowing full control.
79 |
80 |
81 | ```javascript
82 | const DEFAULT_CHUNK_SIZE = 1024;
83 |
84 | function makeReadableByteStream() {
85 | return new ReadableStream({
86 | type: "bytes",
87 |
88 | async pull(controller) {
89 | // Even when the consumer is using the default reader, the auto-allocation
90 | // feature allocates a buffer and passes it to us via byobRequest.
91 | const v = controller.byobRequest.view;
92 | v = crypto.getRandomValues(v);
93 | controller.byobRequest.respond(v.byteLength);
94 | },
95 |
96 | autoAllocateChunkSize: DEFAULT_CHUNK_SIZE
97 | });
98 | }
99 | ```
100 |
101 |
102 | With this in hand, we can create and use BYOB readers for the returned `ReadableStream`. The adaptation between the
103 | low-level byte tracking of the underlying byte source shown here, and the higher-level chunk-based consumption of
104 | a default reader, is all taken care of automatically by the streams implementation.
105 |
106 |
107 | ## Goals
108 |
109 | * Provide a way to represent a [ReadableStream](https://streams.spec.whatwg.org/#rs-class) for bytes efficiently.
110 | * Avoid races caused by multiple access for the same buffer.
111 | * Permit buffer re-use to reduce GC churn.
112 |
113 | ## Non-Goals
114 |
115 | * Non-binary chunk types will not be supported. They can still use the default type of readable stream.
116 | * Shared array buffers will not be supported. Currently, we always detach buffers, but shared array buffers
117 | cannot be detached.
118 |
119 | ## User Benefits
120 |
121 | * By enabling developers to use the Readable Byte Streams API, this will increase speed and lower memory usage
122 | for sites that take advantage of it. Specifically, sites that handle streaming binary data will see improved
123 | performance.
124 |
125 | ## Alternatives
126 |
127 | * Some of the early versions of the standard had a specific ReadableByteStream constructor which would keep the two
128 | types of streams completely separate. However, this was unnecessary and we decided to just use separate controllers
129 | to support byte streams and non-byte streams with the same ReadableStream API to make it simpler.
130 |
--------------------------------------------------------------------------------
/demos/append-child.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Append child writable stream demo
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Append child writable stream demo
18 | Back to demo index
19 |
20 |
21 |
22 | Load JSON stream
23 | Reset
24 |
25 |
26 |
27 |
28 |
100 |
--------------------------------------------------------------------------------
/demos/append-child.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whatwg/streams/080852ccd709e063cc6af239ae07fc040e365179/demos/append-child.mp4
--------------------------------------------------------------------------------
/demos/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Streams Demos
5 |
6 |
7 | Streams Demos
8 |
9 |
10 | Append child WritableStream
11 | Piping through a series of transforms to a custom writable stream that appends children to
12 | the DOM.
13 |
14 | Streaming element
15 | Using custom element to stream into a given DOM location.
16 |
17 | Streaming element with backpressure
18 | A variation on the streaming element demo that adds backpressure to the element's writable
19 | stream, decreasing jank.
20 |
21 | Streaming fetch() call with a progress bar | (Source Code)
22 | A call to the fetch() api that uses the ReadableStream on the response body to fill a progress bar.
23 |
24 | Streaming response from a service worker that instantly populates the DOM | (Source Code)
25 | A book search app that caches responses from an API and streams them to the DOM, allowing for offline streaming responses and making use of the browser's streaming HTML parser.
26 |
27 | Progress indicators with Service Workers and Fetch | (Source Code)
28 | Streams intercept Service Worker FetchEvent
and fetch()
statements to show progress indicators. See browser support notes.
29 |
30 | Grayscale a PNG | (Source Code)
31 | A PNG is retrieved via fetch(), and the response body is piped through a TransformStream to convert the PNG data
32 | to black and white.
33 |
34 | Unpack chunks of a PNG | (Source Code)
35 | A PNG is fetched as a ReadableStream, then pipeThrough()
is used to transform the stream of
36 | Uint8Arrays to a stream of PNG chunks.
37 |
38 |
39 | Feel free to submit more demos by sending a pull request to the
40 | whatwg/streams repository!
41 |
--------------------------------------------------------------------------------
/demos/resources/commits.css:
--------------------------------------------------------------------------------
1 | #commits {
2 | font-size: 16pt;
3 | }
4 |
5 | #commits th {
6 | text-align: left;
7 | padding: 2pt 4pt 2pt 4pt;
8 | }
9 |
10 | #commits tr:nth-child(even) {
11 | background-color: rgb(248, 248, 248);
12 | }
13 |
14 | #commits td {
15 | padding: 2pt 4pt 2pt 4pt;
16 | }
17 |
18 | /* Display hashes in monospace font */
19 | #commits td:first-child {
20 | font-family: Consolas, monospace;
21 | font-size: 0.9em;
22 | }
23 |
24 | /* Don't wrap dates onto multiple lines */
25 | #commits td:nth-child(2) {
26 | white-space: nowrap;
27 | }
28 |
--------------------------------------------------------------------------------
/demos/resources/common.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-size: 14pt;
3 | font-family: sans-serif;
4 | }
5 |
6 | button {
7 | font-size: 14pt;
8 | }
9 |
10 | #buttons {
11 | margin: 14pt 0;
12 | }
13 |
14 | #back-to-index {
15 | position: absolute;
16 | right: 10px;
17 | top: 10px;
18 | font-size: smaller;
19 | }
20 |
21 | h1 {
22 | font-weight: normal;
23 | font-family: Rockwell, Georgia, serif;
24 | font-size: 1.333em;
25 | color: #3c790a;
26 | margin-top: 10px;
27 | }
28 |
29 | dd {
30 | margin-bottom: 1em;
31 | }
32 |
33 | table {
34 | border-collapse: collapse;
35 | }
36 |
37 | .highlight {
38 | border: 1px solid #ddd;
39 | padding: 14pt 28pt 14pt 28pt;
40 | font-family: Consolas, monospace;
41 | background-color: #f8f8f8;
42 | }
43 |
--------------------------------------------------------------------------------
/demos/resources/highlight-default.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Original highlight.js style (c) Ivan Sagalaev
4 |
5 | */
6 |
7 | .hljs {
8 | display: block;
9 | overflow-x: auto;
10 | padding: 0.5em;
11 | background: #F0F0F0;
12 | }
13 |
14 |
15 | /* Base color: saturation 0; */
16 |
17 | .hljs,
18 | .hljs-subst {
19 | color: #444;
20 | }
21 |
22 | .hljs-comment {
23 | color: #888888;
24 | }
25 |
26 | .hljs-keyword,
27 | .hljs-attribute,
28 | .hljs-selector-tag,
29 | .hljs-meta-keyword,
30 | .hljs-doctag,
31 | .hljs-name {
32 | font-weight: bold;
33 | }
34 |
35 |
36 | /* User color: hue: 0 */
37 |
38 | .hljs-type,
39 | .hljs-string,
40 | .hljs-number,
41 | .hljs-selector-id,
42 | .hljs-selector-class,
43 | .hljs-quote,
44 | .hljs-template-tag,
45 | .hljs-deletion {
46 | color: #880000;
47 | }
48 |
49 | .hljs-title,
50 | .hljs-section {
51 | color: #880000;
52 | font-weight: bold;
53 | }
54 |
55 | .hljs-regexp,
56 | .hljs-symbol,
57 | .hljs-variable,
58 | .hljs-template-variable,
59 | .hljs-link,
60 | .hljs-selector-attr,
61 | .hljs-selector-pseudo {
62 | color: #BC6060;
63 | }
64 |
65 |
66 | /* Language color: hue: 90; */
67 |
68 | .hljs-literal {
69 | color: #78A960;
70 | }
71 |
72 | .hljs-built_in,
73 | .hljs-bullet,
74 | .hljs-code,
75 | .hljs-addition {
76 | color: #397300;
77 | }
78 |
79 |
80 | /* Meta color: hue: 200 */
81 |
82 | .hljs-meta {
83 | color: #1f7199;
84 | }
85 |
86 | .hljs-meta-string {
87 | color: #4d99bf;
88 | }
89 |
90 |
91 | /* Misc effects */
92 |
93 | .hljs-emphasis {
94 | font-style: italic;
95 | }
96 |
97 | .hljs-strong {
98 | font-weight: bold;
99 | }
100 |
--------------------------------------------------------------------------------
/demos/resources/highlight.pack.js:
--------------------------------------------------------------------------------
1 | /*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */
2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""+t(e)+">"}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?" ":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/ /g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C=" ",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b:/,e:/(\/\w+|\w+\/)>/,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});
--------------------------------------------------------------------------------
/demos/resources/jank-meter.css:
--------------------------------------------------------------------------------
1 | #jank-meter {
2 | text-align: center;
3 | animation-direction: alternate;
4 | animation-duration: 1s;
5 | animation-iteration-count: infinite;
6 | animation-timing-function: linear;
7 | animation-name: spacing;
8 | }
9 |
10 | @keyframes spacing {
11 | 0% { letter-spacing: 0em; }
12 | 100% { letter-spacing: 2em; }
13 | }
14 |
--------------------------------------------------------------------------------
/demos/resources/web-animations.min.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["src/scope.js","src/timing-utilities.js","src/normalize-keyframes.js","src/deprecation.js","src/web-animations-bonus-cancel-events.js","src/web-animations-bonus-object-form-keyframes.js"],"names":["webAnimationsShared","webAnimations1","webAnimationsNext","webAnimationsTesting","shared","testing","cloneTimingInput","timingInput","clone","m","AnimationEffectTiming","this","_delay","_endDelay","_fill","_iterationStart","_iterations","_duration","_playbackRate","_direction","_easing","_easingFunction","linear","isInvalidTimingDeprecated","isDeprecated","makeTiming","forGroup","effect","timing","fill","duration","isNaN","Object","getOwnPropertyNames","forEach","property","fills","indexOf","directions","numericTimingToObject","normalizeTimingInput","cubic","a","b","c","d","x","f","start_gradient","end_gradient","start","end","mid","xEst","Math","abs","step","count","pos","stepSize","normalizeEasing","easing","styleForCleaning","document","createElement","style","animationTimingFunction","normalizedEasing","TypeError","parseEasingFunction","cubicData","cubicBezierRe","exec","apply","slice","map","Number","stepData","stepRe","Start","middle","Middle","End","presets","calculateActiveDuration","repeatedDuration","playbackRate","iterations","calculatePhase","activeDuration","localTime","PhaseNone","endTime","delay","endDelay","min","PhaseBefore","PhaseAfter","PhaseActive","calculateActiveTime","fillMode","phase","calculateOverallProgress","iterationDuration","activeTime","iterationStart","overallProgress","calculateSimpleIterationProgress","simpleIterationProgress","Infinity","calculateCurrentIteration","floor","calculateDirectedProgress","playbackDirection","currentIteration","currentDirection","calculateIterationProgress","directedProgress","direction","split","prototype","_setMember","member","value","_effect","_timingInput","_timing","_animation","_rebuildUnderlyingAnimation","ease","ease-in","ease-out","ease-in-out","step-start","step-middle","step-end","numberString","RegExp","antiAlias","aliases","isNotAnimatable","lastIndexOf","expandShorthandAndAntiAlias","result","longProperties","shorthandToLonghand","shorthandExpanderElem","i","longProperty","longhandValue","convertToArrayForm","effectInput","normalizedEffectInput","values","Array","isArray","keyframe","numKeyframes","length","offset","composite","push","sort","normalizeKeyframes","spaceKeyframes","keyframes","previousIndex","previousOffset","j","window","Symbol","iterator","from","originalKeyframe","memberValue","isFinite","type","DOMException","NOT_SUPPORTED_ERR","name","message","everyFrameHasOffset","filter","background","border","borderBottom","borderColor","borderLeft","borderRadius","borderRight","borderTop","borderWidth","flex","font","margin","outline","padding","createElementNS","borderWidthAliases","thin","medium","thick","borderBottomWidth","borderLeftWidth","borderRightWidth","borderTopWidth","fontSize","xx-small","x-small","small","large","x-large","xx-large","fontWeight","normal","bold","outlineWidth","textShadow","none","boxShadow","silenced","feature","date","advice","plural","auxVerb","today","Date","expiry","setMonth","getMonth","console","warn","toDateString","deprecated","Error","animate","oncancel","now","performance","AnimationCancelEvent","target","currentTime","timelineTime","bubbles","cancelable","currentTarget","defaultPrevented","eventPhase","Event","AT_TARGET","timeStamp","originalElementAnimate","Element","options","animation","call","_cancelHandlers","originalCancel","cancel","event","handlers","concat","setTimeout","handler","originalAddEventListener","addEventListener","originalRemoveEventListener","removeEventListener","index","splice","element","documentElement","animated","originalOpacity","getComputedStyle","getPropertyValue","testOpacity","opacity","error","exports"],"mappings":"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","file":"web-animations.min.js"}
--------------------------------------------------------------------------------
/demos/streaming-element-backpressure.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Streaming element with backpressure demo
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Streaming element with backpressure demo
18 | Back to demo index
19 |
20 |
21 |
22 | Load HTML stream, applying backpressure
23 | Reset
24 |
25 |
26 | JANK METER
27 |
28 |
29 |
30 |
62 |
--------------------------------------------------------------------------------
/demos/streaming-element-backpressure.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whatwg/streams/080852ccd709e063cc6af239ae07fc040e365179/demos/streaming-element-backpressure.mp4
--------------------------------------------------------------------------------
/demos/streaming-element.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Streaming element demo
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Streaming element demo
18 | Back to demo index
19 |
20 |
21 |
22 | Load HTML stream
23 | Reset
24 |
25 |
26 | JANK METER
27 |
28 |
29 |
30 |
62 |
--------------------------------------------------------------------------------
/demos/streaming-element.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/whatwg/streams/080852ccd709e063cc6af239ae07fc040e365179/demos/streaming-element.mp4
--------------------------------------------------------------------------------
/demos/tags/streaming-element-backpressure.js:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 'use strict';
15 |
16 | customElements.define('streaming-element-backpressure',
17 | class StreamingElementBackPressure extends HTMLElement {
18 | constructor() {
19 | super();
20 | this.reset();
21 | }
22 |
23 | reset() {
24 | this.innerHTML = '';
25 |
26 | const iframeReady = new Promise(resolve => {
27 | const iframe = document.createElement('iframe');
28 | iframe.style.display = 'none';
29 | document.body.appendChild(iframe);
30 |
31 | iframe.onload = () => {
32 | iframe.onload = null;
33 | iframe.contentDocument.write('');
34 | this.appendChild(iframe.contentDocument.querySelector('streaming-element-inner'));
35 | resolve(iframe);
36 | };
37 | iframe.src = '';
38 | });
39 |
40 | async function end() {
41 | let iframe = await iframeReady;
42 | iframe.contentDocument.write(' ');
43 | iframe.contentDocument.close();
44 | iframe.remove();
45 | }
46 |
47 | let idlePromise;
48 | let charactersWrittenInThisChunk = 0;
49 | // Sometimes the browser will decide to target 30fps and nothing we do will
50 | // make it change its mind. To avoid bad behaviour in this case,
51 | // always target 30fps.
52 | const MS_PER_FRAME = 1000/30;
53 | // This is dynamically adjusted according to the measured wait time.
54 | let charactersPerChunk = 4096;
55 | // Smooth over several frames to avoid overcorrection for outliers.
56 | let lastFewFrames = [];
57 | const FRAMES_TO_SMOOTH_OVER = 3;
58 |
59 | function startNewChunk() {
60 | idlePromise = new Promise(resolve => {
61 | window.requestAnimationFrame(resolve);
62 | });
63 | charactersWrittenInThisChunk = 0;
64 | }
65 |
66 | this.writable = new WritableStream({
67 | async write(chunk) {
68 | if (idlePromise === undefined) {
69 | startNewChunk();
70 | await idlePromise;
71 | startNewChunk();
72 | }
73 | let iframe = await iframeReady;
74 | let cursor = 0;
75 | while (cursor < chunk.length) {
76 | const writeCharacters = Math.min(chunk.length - cursor,
77 | charactersPerChunk - charactersWrittenInThisChunk);
78 | iframe.contentDocument.write(chunk.substr(cursor, writeCharacters));
79 | cursor += writeCharacters;
80 | charactersWrittenInThisChunk += writeCharacters;
81 | if (charactersWrittenInThisChunk === charactersPerChunk) {
82 | const timeBeforeWait = performance.now();
83 | await idlePromise;
84 | const timeElapsed = performance.now() - timeBeforeWait;
85 | lastFewFrames.push(timeElapsed);
86 | if (lastFewFrames.length > FRAMES_TO_SMOOTH_OVER) {
87 | lastFewFrames.shift();
88 | }
89 | const averageTimeElapsed = lastFewFrames.reduce((acc, val) => acc + val) / lastFewFrames.length;
90 | charactersPerChunk = Math.max(256, Math.ceil(charactersPerChunk * MS_PER_FRAME / averageTimeElapsed));
91 | console.log(`timeElapsed = ${timeElapsed}, averageTimeElapsed = ${averageTimeElapsed}, charactersPerChunk = ${charactersPerChunk}`);
92 | startNewChunk();
93 | }
94 | }
95 | },
96 | close: end,
97 | abort: end
98 | });
99 | }
100 | });
101 |
--------------------------------------------------------------------------------
/demos/tags/streaming-element.js:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 'use strict';
15 |
16 | customElements.define('streaming-element', class StreamingElement extends HTMLElement {
17 | constructor() {
18 | super();
19 | this.reset();
20 | }
21 |
22 | reset() {
23 | this.innerHTML = '';
24 |
25 | const iframeReady = new Promise(resolve => {
26 | const iframe = document.createElement('iframe');
27 | iframe.style.display = 'none';
28 | document.body.appendChild(iframe);
29 |
30 | iframe.onload = () => {
31 | iframe.onload = null;
32 | iframe.contentDocument.write('');
33 | this.appendChild(iframe.contentDocument.querySelector('streaming-element-inner'));
34 | resolve(iframe);
35 | };
36 | iframe.src = '';
37 | });
38 |
39 | async function end() {
40 | const iframe = await iframeReady;
41 | iframe.contentDocument.write(' ');
42 | iframe.contentDocument.close();
43 | iframe.remove();
44 | }
45 |
46 | this.writable = new WritableStream({
47 | async write(chunk) {
48 | const iframe = await iframeReady;
49 | iframe.contentDocument.write(chunk);
50 | },
51 | close: end,
52 | abort: end
53 | });
54 | }
55 | });
56 |
--------------------------------------------------------------------------------
/demos/tags/view-source.js:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | 'use strict';
15 |
16 | const animateOptions = {
17 | duration: 100,
18 | fill: 'forwards',
19 | easing: 'ease'
20 | };
21 |
22 | customElements.define('view-source', class ViewSource extends HTMLElement {
23 | constructor() {
24 | super();
25 | this._built = false;
26 |
27 | const shadowRoot = this.attachShadow({mode: 'open'});
28 |
29 | shadowRoot.innerHTML = `
30 |
31 |
45 |
46 | ▶
47 | View source
48 |
49 |
50 | `;
51 |
52 |
53 | this._spinnyArrow = shadowRoot.querySelector('#spinny-arrow');
54 | this._label = shadowRoot.querySelector('#label');
55 | this._source = shadowRoot.querySelector('#source');
56 |
57 | const button = shadowRoot.querySelector('button');
58 | let shown = false;
59 | button.onclick = () => {
60 | if (shown) {
61 | this.hide();
62 | shown = false;
63 | return;
64 | }
65 |
66 | this.show();
67 | shown = true;
68 | };
69 | }
70 |
71 | hide() {
72 | this._spinnyArrow.animate({
73 | transform: ['rotate(90deg)', 'rotate(0deg)']
74 | }, animateOptions);
75 |
76 | const currentHeight = this._source.clientHeight;
77 | this._source.animate({
78 | height: [`${currentHeight}px`, '0px'],
79 | paddingTop: ['0.5em', '0'],
80 | paddingBottom: ['0.5em', '0']
81 | }, animateOptions);
82 | }
83 |
84 | show() {
85 | this._spinnyArrow.animate({
86 | transform: ['rotate(0deg)', 'rotate(90deg)']
87 | }, animateOptions);
88 |
89 | const currentHeight = this._source.clientHeight;
90 | this._source.style.overflow = 'hidden';
91 | this._source.animate({
92 | height: ['0', 'auto'],
93 | paddingTop: ['0', '0.5em'],
94 | paddingBottom: ['0', '0.5em']
95 | }, animateOptions);
96 | }
97 |
98 | async connectedCallback() {
99 | if (this._built) {
100 | return;
101 | }
102 |
103 | const res = await fetch(location.href);
104 | const text = await res.text();
105 |
106 | const [,snippedText] = /\/\/ BEGIN SOURCE TO VIEW([\s\S]+)\/\/ END SOURCE TO VIEW/.exec(text);
107 |
108 | this._source.textContent = snippedText.trim();
109 | hljs.highlightBlock(this._source);
110 | this._built = true;
111 | }
112 | });
113 |
--------------------------------------------------------------------------------
/demos/transforms/parse-json.js:
--------------------------------------------------------------------------------
1 | function parseJSON() {
2 | return new TransformStream({
3 | transform(chunk, controller) {
4 | controller.enqueue(JSON.parse(chunk));
5 | }
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/demos/transforms/split-stream.js:
--------------------------------------------------------------------------------
1 | function splitStream(splitOn) {
2 | let buffer = '';
3 |
4 | return new TransformStream({
5 | transform(chunk, controller) {
6 | buffer += chunk;
7 | const parts = buffer.split(splitOn);
8 | parts.slice(0, -1).forEach(part => controller.enqueue(part));
9 | buffer = parts[parts.length - 1];
10 | },
11 | flush(controller) {
12 | if (buffer) controller.enqueue(buffer);
13 | }
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/demos/transforms/text-encode-transform.js:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Polyfill for TextEncoderStream and TextDecoderStream
16 |
17 | (function() {
18 | 'use strict';
19 |
20 | if (typeof self.TextEncoderStream === 'function' &&
21 | typeof self.TextDecoderStream === 'function') {
22 | // The constructors exist. Assume that they work and don't replace them.
23 | return;
24 | }
25 |
26 | if (typeof self.TextEncoder !== 'function') {
27 | throw new ReferenceError('TextEncoder implementation required');
28 | }
29 |
30 | if (typeof self.TextDecoder !== 'function') {
31 | throw new ReferenceError('TextDecoder implementation required');
32 | }
33 |
34 | // These symbols end up being different for every realm, so mixing objects
35 | // created in one realm with methods created in another fails.
36 | const codec = Symbol('codec');
37 | const transform = Symbol('transform');
38 |
39 | class TextEncoderStream {
40 | constructor() {
41 | this[codec] = new TextEncoder();
42 | this[transform] =
43 | new TransformStream(new TextEncodeTransformer(this[codec]));
44 | }
45 | }
46 |
47 | class TextDecoderStream {
48 | constructor(label = undefined, options = undefined) {
49 | this[codec] = new TextDecoder(label, options);
50 | this[transform] = new TransformStream(
51 | new TextDecodeTransformer(this[codec]));
52 | }
53 | }
54 |
55 | // ECMAScript class syntax will create getters that are non-enumerable, but we
56 | // need them to be enumerable in WebIDL-style, so we add them manually.
57 | // "readable" and "writable" are always delegated to the TransformStream
58 | // object. Properties specified in |properties| are delegated to the
59 | // underlying TextEncoder or TextDecoder.
60 | function addDelegatingProperties(prototype, properties) {
61 | for (const transformProperty of ['readable', 'writable']) {
62 | addGetter(prototype, transformProperty, function() {
63 | return this[transform][transformProperty];
64 | });
65 | }
66 |
67 | for (const codecProperty of properties) {
68 | addGetter(prototype, codecProperty, function() {
69 | return this[codec][codecProperty];
70 | });
71 | }
72 | }
73 |
74 | function addGetter(prototype, property, getter) {
75 | Object.defineProperty(prototype, property,
76 | {
77 | configurable: true,
78 | enumerable: true,
79 | get: getter
80 | });
81 | }
82 |
83 | addDelegatingProperties(TextEncoderStream.prototype, ['encoding']);
84 | addDelegatingProperties(TextDecoderStream.prototype,
85 | ['encoding', 'fatal', 'ignoreBOM']);
86 |
87 | class TextEncodeTransformer {
88 | constructor() {
89 | this._encoder = new TextEncoder();
90 | this._carry = undefined;
91 | }
92 |
93 | transform(chunk, controller) {
94 | chunk = String(chunk);
95 | if (this._carry !== undefined) {
96 | chunk = this._carry + chunk;
97 | this._carry = undefined;
98 | }
99 | const terminalCodeUnit = chunk.charCodeAt(chunk.length - 1);
100 | if (terminalCodeUnit >= 0xD800 && terminalCodeUnit < 0xDC00) {
101 | this._carry = chunk.substring(chunk.length - 1);
102 | chunk = chunk.substring(0, chunk.length - 1);
103 | }
104 | const encoded = this._encoder.encode(chunk);
105 | if (encoded.length > 0) {
106 | controller.enqueue(encoded);
107 | }
108 | }
109 |
110 | flush(controller) {
111 | if (this._carry !== undefined) {
112 | controller.enqueue(this._encoder.encode(this._carry));
113 | this._carry = undefined;
114 | }
115 | }
116 | }
117 |
118 | class TextDecodeTransformer {
119 | constructor(decoder) {
120 | this._decoder = new TextDecoder(decoder.encoding, {
121 | fatal: decoder.fatal,
122 | ignoreBOM: decoder.ignoreBOM
123 | });
124 | }
125 |
126 | transform(chunk, controller) {
127 | const decoded = this._decoder.decode(chunk, {stream: true});
128 | if (decoded != '') {
129 | controller.enqueue(decoded);
130 | }
131 | }
132 |
133 | flush(controller) {
134 | // If {fatal: false} is in options (the default), then the final call to
135 | // decode() can produce extra output (usually the unicode replacement
136 | // character 0xFFFD). When fatal is true, this call is just used for its
137 | // side-effect of throwing a TypeError exception if the input is
138 | // incomplete.
139 | var output = this._decoder.decode();
140 | if (output !== '') {
141 | controller.enqueue(output);
142 | }
143 | }
144 | }
145 |
146 | function exportAs(name, value) {
147 | // Make it stringify as [object ] rather than [object Object].
148 | value.prototype[Symbol.toStringTag] = name;
149 | Object.defineProperty(self, name,
150 | {
151 | configurable: true,
152 | enumerable: false,
153 | writable: true,
154 | value
155 | });
156 | }
157 |
158 | exportAs('TextEncoderStream', TextEncoderStream);
159 | exportAs('TextDecoderStream', TextDecoderStream);
160 | })();
161 |
--------------------------------------------------------------------------------
/demos/transforms/transform-stream-polyfill.js:
--------------------------------------------------------------------------------
1 | // Quick and dirty polyfill for TransformStream.
2 | // Based on the reference implementation:
3 | // https://github.com/whatwg/streams/blob/main/reference-implementation/lib/transform-stream.js
4 |
5 | (function() {
6 | 'use strict';
7 |
8 | if ('TransformStream' in self) {
9 | return;
10 | }
11 |
12 | // Methods on the transform stream controller object
13 |
14 | function TransformStreamCloseReadable(transformStream) {
15 | if (transformStream._errored === true) {
16 | throw new TypeError('TransformStream is already errored');
17 | }
18 |
19 | if (transformStream._readableClosed === true) {
20 | throw new TypeError('Readable side is already closed');
21 | }
22 |
23 | TransformStreamCloseReadableInternal(transformStream);
24 | }
25 |
26 | function TransformStreamEnqueueToReadable(transformStream, chunk) {
27 | if (transformStream._errored === true) {
28 | throw new TypeError('TransformStream is already errored');
29 | }
30 |
31 | if (transformStream._readableClosed === true) {
32 | throw new TypeError('Readable side is already closed');
33 | }
34 |
35 | // We throttle transformer.transform invocation based on the backpressure of the ReadableStream, but we still
36 | // accept TransformStreamEnqueueToReadable() calls.
37 |
38 | const controller = transformStream._readableController;
39 |
40 | try {
41 | ReadableStreamDefaultControllerEnqueue(controller, chunk);
42 | } catch (e) {
43 | // This happens when readableStrategy.size() throws.
44 | // The ReadableStream has already errored itself.
45 | transformStream._readableClosed = true;
46 | TransformStreamErrorIfNeeded(transformStream, e);
47 |
48 | throw transformStream._storedError;
49 | }
50 |
51 | const desiredSize = ReadableStreamDefaultControllerGetDesiredSize(controller);
52 | const maybeBackpressure = desiredSize <= 0;
53 |
54 | if (maybeBackpressure === true && transformStream._backpressure === false) {
55 | // This allows pull() again. When desiredSize is 0, it's possible that a pull() will happen immediately (but
56 | // asynchronously) after this because of pending read()s and set _backpressure back to false.
57 | //
58 | // If pull() could be called from inside enqueue(), then this logic would be wrong. This cannot happen
59 | // because there is always a promise pending from start() or pull() when _backpressure is false.
60 | TransformStreamSetBackpressure(transformStream, true);
61 | }
62 | }
63 |
64 | function TransformStreamError(transformStream, e) {
65 | if (transformStream._errored === true) {
66 | throw new TypeError('TransformStream is already errored');
67 | }
68 |
69 | TransformStreamErrorInternal(transformStream, e);
70 | }
71 |
72 | // Abstract operations.
73 |
74 | function TransformStreamCloseReadableInternal(transformStream) {
75 | ReadableStreamDefaultControllerClose(transformStream._readableController);
76 |
77 | transformStream._readableClosed = true;
78 | }
79 |
80 | function TransformStreamErrorIfNeeded(transformStream, e) {
81 | if (transformStream._errored === false) {
82 | TransformStreamErrorInternal(transformStream, e);
83 | }
84 | }
85 |
86 | function TransformStreamErrorInternal(transformStream, e) {
87 | transformStream._errored = true;
88 | transformStream._storedError = e;
89 |
90 | if (transformStream._writableDone === false) {
91 | WritableStreamDefaultControllerError(transformStream._writableController, e);
92 | }
93 | if (transformStream._readableClosed === false) {
94 | ReadableStreamDefaultControllerError(transformStream._readableController, e);
95 | }
96 | }
97 |
98 | // Used for preventing the next write() call on TransformStreamSink until there
99 | // is no longer backpressure.
100 | function TransformStreamReadableReadyPromise(transformStream) {
101 | if (transformStream._backpressure === false) {
102 | return Promise.resolve();
103 | }
104 |
105 | return transformStream._backpressureChangePromise;
106 | }
107 |
108 | function TransformStreamSetBackpressure(transformStream, backpressure) {
109 | // Passes also when called during construction.
110 |
111 | if (transformStream._backpressureChangePromise !== undefined) {
112 | // The fulfillment value is just for a sanity check.
113 | transformStream._backpressureChangePromise_resolve(backpressure);
114 | }
115 |
116 | transformStream._backpressureChangePromise = new Promise(resolve => {
117 | transformStream._backpressureChangePromise_resolve = resolve;
118 | });
119 |
120 | transformStream._backpressureChangePromise.then(resolution => {
121 | });
122 |
123 | transformStream._backpressure = backpressure;
124 | }
125 |
126 | function TransformStreamDefaultTransform(chunk, transformStreamController) {
127 | const transformStream = transformStreamController._controlledTransformStream;
128 | TransformStreamEnqueueToReadable(transformStream, chunk);
129 | return Promise.resolve();
130 | }
131 |
132 | function TransformStreamTransform(transformStream, chunk) {
133 | transformStream._transforming = true;
134 |
135 | const transformer = transformStream._transformer;
136 | const controller = transformStream._transformStreamController;
137 |
138 | const transformPromise = PromiseInvokeOrPerformFallback(transformer, 'transform', [chunk, controller],
139 | TransformStreamDefaultTransform, [chunk, controller]);
140 |
141 | return transformPromise.then(
142 | () => {
143 | transformStream._transforming = false;
144 |
145 | return TransformStreamReadableReadyPromise(transformStream);
146 | },
147 | e => {
148 | TransformStreamErrorIfNeeded(transformStream, e);
149 | return Promise.reject(e);
150 | });
151 | }
152 |
153 | function IsTransformStreamDefaultController(x) {
154 | if (!typeIsObject(x)) {
155 | return false;
156 | }
157 |
158 | if (!Object.prototype.hasOwnProperty.call(x, '_controlledTransformStream')) {
159 | return false;
160 | }
161 |
162 | return true;
163 | }
164 |
165 | function IsTransformStream(x) {
166 | if (!typeIsObject(x)) {
167 | return false;
168 | }
169 |
170 | if (!Object.prototype.hasOwnProperty.call(x, '_transformStreamController')) {
171 | return false;
172 | }
173 |
174 | return true;
175 | }
176 |
177 | class TransformStreamSink {
178 | constructor(transformStream, startPromise) {
179 | this._transformStream = transformStream;
180 | this._startPromise = startPromise;
181 | }
182 |
183 | start(c) {
184 | const transformStream = this._transformStream;
185 |
186 | transformStream._writableController = c;
187 |
188 | return this._startPromise.then(() => TransformStreamReadableReadyPromise(transformStream));
189 | }
190 |
191 | write(chunk) {
192 | const transformStream = this._transformStream;
193 |
194 | return TransformStreamTransform(transformStream, chunk);
195 | }
196 |
197 | abort() {
198 | const transformStream = this._transformStream;
199 | transformStream._writableDone = true;
200 | TransformStreamErrorInternal(transformStream, new TypeError('Writable side aborted'));
201 | }
202 |
203 | close() {
204 | const transformStream = this._transformStream;
205 |
206 | transformStream._writableDone = true;
207 |
208 | const flushPromise = PromiseInvokeOrNoop(transformStream._transformer,
209 | 'flush', [transformStream._transformStreamController]);
210 | // Return a promise that is fulfilled with undefined on success.
211 | return flushPromise.then(() => {
212 | if (transformStream._errored === true) {
213 | return Promise.reject(transformStream._storedError);
214 | }
215 | if (transformStream._readableClosed === false) {
216 | TransformStreamCloseReadableInternal(transformStream);
217 | }
218 | return Promise.resolve();
219 | }).catch(r => {
220 | TransformStreamErrorIfNeeded(transformStream, r);
221 | return Promise.reject(transformStream._storedError);
222 | });
223 | }
224 | }
225 |
226 | class TransformStreamSource {
227 | constructor(transformStream, startPromise) {
228 | this._transformStream = transformStream;
229 | this._startPromise = startPromise;
230 | }
231 |
232 | start(c) {
233 | const transformStream = this._transformStream;
234 |
235 | transformStream._readableController = c;
236 |
237 | return this._startPromise.then(() => {
238 | // Prevent the first pull() call until there is backpressure.
239 |
240 | if (transformStream._backpressure === true) {
241 | return Promise.resolve();
242 | }
243 |
244 | return transformStream._backpressureChangePromise;
245 | });
246 | }
247 |
248 | pull() {
249 | const transformStream = this._transformStream;
250 |
251 | // Invariant. Enforced by the promises returned by start() and pull().
252 |
253 | TransformStreamSetBackpressure(transformStream, false);
254 |
255 | // Prevent the next pull() call until there is backpressure.
256 | return transformStream._backpressureChangePromise;
257 | }
258 |
259 | cancel() {
260 | const transformStream = this._transformStream;
261 | transformStream._readableClosed = true;
262 | TransformStreamErrorInternal(transformStream, new TypeError('Readable side canceled'));
263 | }
264 | }
265 |
266 | class TransformStreamDefaultController {
267 | constructor(transformStream) {
268 | if (IsTransformStream(transformStream) === false) {
269 | throw new TypeError('TransformStreamDefaultController can only be ' +
270 | 'constructed with a TransformStream instance');
271 | }
272 |
273 | if (transformStream._transformStreamController !== undefined) {
274 | throw new TypeError('TransformStreamDefaultController instances can ' +
275 | 'only be created by the TransformStream constructor');
276 | }
277 |
278 | this._controlledTransformStream = transformStream;
279 | }
280 |
281 | get desiredSize() {
282 | if (IsTransformStreamDefaultController(this) === false) {
283 | throw defaultControllerBrandCheckException('desiredSize');
284 | }
285 |
286 | const transformStream = this._controlledTransformStream;
287 | const readableController = transformStream._readableController;
288 |
289 | return ReadableStreamDefaultControllerGetDesiredSize(readableController);
290 | }
291 |
292 | enqueue(chunk) {
293 | if (IsTransformStreamDefaultController(this) === false) {
294 | throw defaultControllerBrandCheckException('enqueue');
295 | }
296 |
297 | TransformStreamEnqueueToReadable(this._controlledTransformStream, chunk);
298 | }
299 |
300 | close() {
301 | if (IsTransformStreamDefaultController(this) === false) {
302 | throw defaultControllerBrandCheckException('close');
303 | }
304 |
305 | TransformStreamCloseReadable(this._controlledTransformStream);
306 | }
307 |
308 | error(reason) {
309 | if (IsTransformStreamDefaultController(this) === false) {
310 | throw defaultControllerBrandCheckException('error');
311 | }
312 |
313 | TransformStreamError(this._controlledTransformStream, reason);
314 | }
315 | }
316 |
317 | class TransformStream {
318 | constructor(transformer = {}) {
319 | this._transformer = transformer;
320 | const { readableStrategy, writableStrategy } = transformer;
321 |
322 | this._transforming = false;
323 | this._errored = false;
324 | this._storedError = undefined;
325 |
326 | this._writableController = undefined;
327 | this._readableController = undefined;
328 | this._transformStreamController = undefined;
329 |
330 | this._writableDone = false;
331 | this._readableClosed = false;
332 |
333 | this._backpressure = undefined;
334 | this._backpressureChangePromise = undefined;
335 | this._backpressureChangePromise_resolve = undefined;
336 |
337 | this._transformStreamController = new TransformStreamDefaultController(this);
338 |
339 | let startPromise_resolve;
340 | const startPromise = new Promise(resolve => {
341 | startPromise_resolve = resolve;
342 | });
343 |
344 | const source = new TransformStreamSource(this, startPromise);
345 |
346 | this._readable = new ReadableStream(source, readableStrategy);
347 |
348 | const sink = new TransformStreamSink(this, startPromise);
349 |
350 | this._writable = new WritableStream(sink, writableStrategy);
351 |
352 | const desiredSize = ReadableStreamDefaultControllerGetDesiredSize(this._readableController);
353 | // Set _backpressure based on desiredSize. As there is no read() at this point, we can just interpret
354 | // desiredSize being non-positive as backpressure.
355 | TransformStreamSetBackpressure(this, desiredSize <= 0);
356 |
357 | const transformStream = this;
358 | const startResult = InvokeOrNoop(transformer, 'start',
359 | [transformStream._transformStreamController]);
360 | startPromise_resolve(startResult);
361 | startPromise.catch(e => {
362 | // The underlyingSink and underlyingSource will error the readable and writable ends on their own.
363 | if (transformStream._errored === false) {
364 | transformStream._errored = true;
365 | transformStream._storedError = e;
366 | }
367 | });
368 | }
369 |
370 | get readable() {
371 | if (IsTransformStream(this) === false) {
372 | throw streamBrandCheckException('readable');
373 | }
374 |
375 | return this._readable;
376 | }
377 |
378 | get writable() {
379 | if (IsTransformStream(this) === false) {
380 | throw streamBrandCheckException('writable');
381 | }
382 |
383 | return this._writable;
384 | }
385 | }
386 |
387 | // Helper functions for the TransformStreamDefaultController.
388 |
389 | function defaultControllerBrandCheckException(name) {
390 | return new TypeError(
391 | `TransformStreamDefaultController.prototype.${name} can only be used on a TransformStreamDefaultController`);
392 | }
393 |
394 | // Stubs for abstract operations used from ReadableStream and WritableStream.
395 |
396 | function ReadableStreamDefaultControllerEnqueue(controller, chunk) {
397 | controller.enqueue(chunk);
398 | }
399 |
400 | function ReadableStreamDefaultControllerGetDesiredSize(controller) {
401 | return controller.desiredSize;
402 | }
403 |
404 | function ReadableStreamDefaultControllerClose(controller) {
405 | controller.close();
406 | }
407 |
408 | function ReadableStreamDefaultControllerError(controller, e) {
409 | controller.error(e);
410 | }
411 |
412 | function WritableStreamDefaultControllerError(controller, e) {
413 | controller.error(e);
414 | }
415 |
416 | // Helper functions for the TransformStream.
417 |
418 | function streamBrandCheckException(name) {
419 | return new TypeError(
420 | `TransformStream.prototype.${name} can only be used on a TransformStream`);
421 | }
422 |
423 | // Copied from helpers.js.
424 | function IsPropertyKey(argument) {
425 | return typeof argument === 'string' || typeof argument === 'symbol';
426 | }
427 |
428 | function typeIsObject(x) {
429 | return (typeof x === 'object' && x !== null) || typeof x === 'function';
430 | }
431 |
432 | function Call(F, V, args) {
433 | if (typeof F !== 'function') {
434 | throw new TypeError('Argument is not a function');
435 | }
436 |
437 | return Function.prototype.apply.call(F, V, args);
438 | }
439 |
440 | function InvokeOrNoop(O, P, args) {
441 | const method = O[P];
442 | if (method === undefined) {
443 | return undefined;
444 | }
445 |
446 | return Call(method, O, args);
447 | };
448 |
449 | function PromiseInvokeOrNoop(O, P, args) {
450 | try {
451 | return Promise.resolve(InvokeOrNoop(O, P, args));
452 | } catch (returnValueE) {
453 | return Promise.reject(returnValueE);
454 | }
455 | };
456 |
457 | function PromiseInvokeOrPerformFallback(O, P, args, F, argsF) {
458 | let method;
459 | try {
460 | method = O[P];
461 | } catch (methodE) {
462 | return Promise.reject(methodE);
463 | }
464 |
465 | if (method === undefined) {
466 | return F(...argsF);
467 | }
468 |
469 | try {
470 | return Promise.resolve(Call(method, O, args));
471 | } catch (e) {
472 | return Promise.reject(e);
473 | }
474 | };
475 |
476 | // DIY assert() implementation
477 | function assert(predicate, s) {
478 | class TransformStreamInternalLogicError extends Error {
479 | constructor(s) {
480 | super(s);
481 | }
482 | }
483 | if (!predicate) {
484 | console.log(`TransformStream internal logic error: assertion failed: s`);
485 | throw new TransformStreamInternalLogicError(s);
486 | }
487 | }
488 |
489 | self['TransformStream'] = TransformStream;
490 | })();
491 |
--------------------------------------------------------------------------------
/readable-stream-async-iteration-explainer.md:
--------------------------------------------------------------------------------
1 | # `ReadableStream` Async Iteration Explained
2 |
3 |
4 | ## Introduction
5 |
6 | The streams APIs provide ubiquitous, interoperable primitives for creating, composing, and consuming streams of data.
7 |
8 | This change adds support for the [async iterable protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols)
9 | to the `ReadableStream` API, enabling readable streams to be used as the source of `for await...of` loops.
10 |
11 | To consume a `ReadableStream`, developers currently acquire a reader and repeatedly call `read()`:
12 | ```javascript
13 | async function getResponseSize(url) {
14 | const response = await fetch(url);
15 | const reader = response.body.getReader();
16 | let total = 0;
17 |
18 | while (true) {
19 | const {done, value} = await reader.read();
20 | if (done) return total;
21 | total += value.length;
22 | }
23 | }
24 | ```
25 |
26 | By adding support for the async iterable protocol, web developers will be able to use the much simpler
27 | `for await...of` syntax to loop over all chunks of a `ReadableStream`.
28 |
29 | ## API
30 |
31 | The [ReadableStream definition](https://streams.spec.whatwg.org/#rs-class-definition) is extended
32 | with a [Web IDL `async iterable` declaration](https://webidl.spec.whatwg.org/#idl-async-iterable):
33 | ```
34 | interface ReadableStream {
35 | async iterable(optional ReadableStreamIteratorOptions options = {});
36 | };
37 |
38 | dictionary ReadableStreamIteratorOptions {
39 | boolean preventCancel = false;
40 | };
41 | ```
42 |
43 | This results in the following methods being added to the JavaScript binding:
44 |
45 | * `ReadableStream.prototype.values({ preventCancel = false } = {})`: returns an [AsyncIterator](https://tc39.es/ecma262/#sec-asynciterator-interface)
46 | object which locks the stream.
47 | * `iterator.next()` reads the next chunk from the stream, like `reader.read()`.
48 | If the stream becomes closed or errored, this automatically releases the lock.
49 | * `iterator.return(arg)` releases the lock, like `reader.releaseLock()`.
50 | If `preventCancel` is unset or false, then this also cancels the stream
51 | with the optional `arg` as cancel reason.
52 | * `ReadableStream.prototype[Symbol.asyncIterator]()`: same as `values()`.
53 | This method makes `ReadableStream` adhere to the [ECMAScript AsyncIterable protocol](https://tc39.es/ecma262/#sec-asynciterable-interface),
54 | and enables `for await...of` to work.
55 |
56 | ## Examples
57 |
58 | The original example can be written more succinctly using `for await...of`:
59 | ```javascript
60 | async function getResponseSize(url) {
61 | const response = await fetch(url);
62 | let total = 0;
63 | for await (const chunk of response) {
64 | total += chunk.length;
65 | }
66 | return total;
67 | }
68 | ```
69 |
70 | Finding a specific chunk or byte in a stream also becomes easier (adapted from
71 | [Jake Archibald's blog post](https://jakearchibald.com/2017/async-iterators-and-generators/#making-streams-iterate)):
72 | ```javascript
73 | async function example() {
74 | const find = 'J';
75 | const findCode = find.codePointAt(0);
76 | const response = await fetch('https://html.spec.whatwg.org');
77 | let bytes = 0;
78 |
79 | for await (const chunk of response.body) {
80 | const index = chunk.indexOf(findCode);
81 |
82 | if (index != -1) {
83 | bytes += index;
84 | console.log(`Found ${find} at byte ${bytes}.`);
85 | break;
86 | }
87 |
88 | bytes += chunk.length;
89 | }
90 | }
91 | ```
92 | Note that the stream is automatically cancelled when we `break` out of the loop.
93 | To prevent this, for example if you want to consume the remainder of the stream differently,
94 | you can instead use `response.body.values({ preventCancel: true })`.
95 |
96 |
97 | ## Goals
98 |
99 | * Permit `ReadableStream` to be used as the source of a `for await...of` loop.
100 |
101 |
102 | ## Non-goals
103 |
104 | N/A.
105 |
106 |
107 | ## End-user benefit
108 |
109 | * Reduces boilerplate for developers when manually consuming a `ReadableStream`.
110 | * Allows integration with future ECMAScript proposals, such as [Async Iterator Helpers](https://github.com/tc39/proposal-async-iterator-helpers).
111 | * Allows interoperability with other APIs that can "adapt" async iterables, such as
112 | Node.js [Readable.from](https://nodejs.org/docs/latest-v20.x/api/stream.html#streamreadablefromiterable-options).
113 |
114 |
115 | ## Alternatives
116 |
117 | * It was [initially suggested](https://github.com/whatwg/streams/issues/778#issuecomment-371711899)
118 | that we could use a `ReadableStreamDefaultReader` as an `AsyncIterator`, by adding `next()`
119 | and `return()` methods directly to the reader. However, the return values of `return()` and
120 | `releaseLock()` are different, so the choice went to adding a separate async iterator object.
121 |
--------------------------------------------------------------------------------
/reference-implementation/.eslintignore:
--------------------------------------------------------------------------------
1 | web-platform-tests/
2 | coverage/
3 | generated/
4 | bundle.js
5 |
--------------------------------------------------------------------------------
/reference-implementation/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "node": true,
5 | "browser": true,
6 | "es2020": true
7 | },
8 | "parserOptions": {
9 | "ecmaVersion": 2020
10 | },
11 | "globals": {
12 | "GCController": false,
13 | "gc": false,
14 | "globalThis": false
15 | },
16 | "rules": {
17 | // Possible errors
18 | "comma-dangle": ["error", "never"],
19 | "no-cond-assign": ["error", "except-parens"],
20 | "no-console": "error",
21 | "no-constant-condition": "error",
22 | "no-control-regex": "error",
23 | "no-debugger": "error",
24 | "no-dupe-args": "error",
25 | "no-dupe-keys": "error",
26 | "no-duplicate-case": "error",
27 | "no-empty": "error",
28 | "no-empty-character-class": "error",
29 | "no-ex-assign": "error",
30 | "no-extra-boolean-cast": "error",
31 | "no-extra-parens": ["error", "all", { "conditionalAssign": false, "nestedBinaryExpressions": false, "returnAssign": false }],
32 | "no-extra-semi": "error",
33 | "no-func-assign": "error",
34 | "no-inner-declarations": "off",
35 | "no-invalid-regexp": "error",
36 | "no-irregular-whitespace": "error",
37 | "no-negated-in-lhs": "error",
38 | "no-obj-calls": "error",
39 | "no-regex-spaces": "error",
40 | "no-sparse-arrays": "error",
41 | "no-unexpected-multiline": "error",
42 | "no-unreachable": "error",
43 | "no-unsafe-finally": "off",
44 | "use-isnan": "error",
45 | "valid-jsdoc": "off",
46 | "valid-typeof": "error",
47 |
48 | // Best practices
49 | "accessor-pairs": "error",
50 | "array-callback-return": "error",
51 | "block-scoped-var": "off",
52 | "complexity": "off",
53 | "consistent-return": "error",
54 | "curly": ["error", "all"],
55 | "default-case": "off",
56 | "dot-location": ["error", "property"],
57 | "dot-notation": "error",
58 | "eqeqeq": "error",
59 | "guard-for-in": "off",
60 | "no-alert": "error",
61 | "no-caller": "error",
62 | "no-case-declarations": "error",
63 | "no-div-regex": "off",
64 | "no-else-return": "error",
65 | "no-empty-function": "off",
66 | "no-empty-pattern": "error",
67 | "no-eq-null": "error",
68 | "no-eval": "error",
69 | "no-extend-native": "error",
70 | "no-extra-bind": "error",
71 | "no-extra-label": "error",
72 | "no-fallthrough": "error",
73 | "no-floating-decimal": "error",
74 | "no-implicit-coercion": "error",
75 | "no-implicit-globals": "error",
76 | "no-implied-eval": "off",
77 | "no-invalid-this": "off", // meh
78 | "no-iterator": "error",
79 | "no-labels": ["error", { "allowLoop": true }],
80 | "no-lone-blocks": "error",
81 | "no-loop-func": "off",
82 | "no-magic-numbers": "off",
83 | "no-multi-spaces": "error",
84 | "no-multi-str": "error",
85 | "no-native-reassign": "error",
86 | "no-new": "off",
87 | "no-new-func": "error",
88 | "no-new-wrappers": "error",
89 | "no-octal": "error",
90 | "no-octal-escape": "error",
91 | "no-param-reassign": "off",
92 | "no-process-env": "error",
93 | "no-proto": "error",
94 | "no-redeclare": "error",
95 | "no-return-assign": ["error", "except-parens"],
96 | "no-script-url": "off",
97 | "no-self-assign": "error",
98 | "no-self-compare": "error",
99 | "no-sequences": "error",
100 | "no-throw-literal": "error",
101 | "no-unmodified-loop-condition": "error",
102 | "no-unused-expressions": "error",
103 | "no-unused-labels": "error",
104 | "no-useless-call": "error",
105 | "no-useless-concat": "error",
106 | "no-useless-escape": "error",
107 | "no-void": "error",
108 | "no-warning-comments": "off",
109 | "no-with": "error",
110 | "radix": ["error", "as-needed"],
111 | "vars-on-top": "off",
112 | "wrap-iife": ["error", "outside"],
113 | "yoda": ["error", "never"],
114 |
115 | // Strict Mode
116 | "strict": ["error", "global"],
117 |
118 | // Variables
119 | "init-declarations": "off",
120 | "no-catch-shadow": "error",
121 | "no-delete-var": "error",
122 | "no-label-var": "error",
123 | "no-restricted-globals": "off",
124 | "no-shadow": "error",
125 | "no-shadow-restricted-names": "error",
126 | "no-undef": "error",
127 | "no-undef-init": "error",
128 | "no-undefined": "off",
129 | "no-unused-vars": "error",
130 | "no-use-before-define": "off",
131 |
132 | // Node.js and CommonJS
133 | "callback-return": "off",
134 | "global-require": "error",
135 | "handle-callback-err": "error",
136 | "no-mixed-requires": ["error", true],
137 | "no-new-require": "error",
138 | "no-path-concat": "error",
139 | "no-process-exit": "error",
140 | "no-restricted-imports": "off",
141 | "no-restricted-modules": "off",
142 | "no-sync": "off",
143 |
144 | // Stylistic Issues
145 | "array-bracket-spacing": ["error", "never"],
146 | "block-spacing": ["error", "always"],
147 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
148 | "camelcase": "off",
149 | "comma-spacing": ["error", { "before": false, "after": true }],
150 | "comma-style": ["error", "last"],
151 | "computed-property-spacing": ["error", "never"],
152 | "consistent-this": "off",
153 | "eol-last": "error",
154 | "func-names": "off",
155 | "func-style": "off",
156 | "id-blacklist": "off",
157 | "id-length": "off",
158 | "id-match": "off",
159 | "indent": ["error", 2, {
160 | "SwitchCase": 1,
161 | "FunctionDeclaration": { "parameters": "first" },
162 | "FunctionExpression": { "parameters": "first" },
163 | "CallExpression": { "arguments": "first" },
164 | "ArrayExpression": "first",
165 | "ObjectExpression": "first",
166 | "ImportDeclaration": "first"
167 | }],
168 | "jsx-quotes": "off",
169 | "key-spacing": ["error", { "beforeColon": false, "afterColon": true, "mode": "strict" }],
170 | "keyword-spacing": ["error", { "before": true, "after": true }],
171 | "linebreak-style": ["error", "unix"],
172 | "lines-around-comment": "off",
173 | "max-depth": "off",
174 | "max-len": ["error", 120, { "ignoreUrls": true }],
175 | "max-nested-callbacks": "off",
176 | "max-params": "off",
177 | "max-statements": "off",
178 | "max-statements-per-line": ["error", { "max": 1 }],
179 | "new-cap": "off",
180 | "new-parens": "error",
181 | "newline-after-var": "off",
182 | "newline-before-return": "off",
183 | "newline-per-chained-call": "off",
184 | "no-array-constructor": "error",
185 | "no-bitwise": "off",
186 | "no-continue": "off",
187 | "no-inline-comments": "off",
188 | "no-lonely-if": "error",
189 | "no-mixed-spaces-and-tabs": "error",
190 | "no-multiple-empty-lines": "error",
191 | "no-negated-condition": "off",
192 | "no-nested-ternary": "error",
193 | "no-new-object": "error",
194 | "no-plusplus": "off",
195 | "no-restricted-syntax": "off",
196 | "no-spaced-func": "error",
197 | "no-ternary": "off",
198 | "no-trailing-spaces": "error",
199 | "no-underscore-dangle": "off",
200 | "no-unneeded-ternary": "error",
201 | "no-whitespace-before-property": "error",
202 | "object-curly-spacing": ["error", "always"],
203 | "object-property-newline": "off",
204 | "one-var": ["error", "never"],
205 | "one-var-declaration-per-line": ["error", "initializations"],
206 | "operator-assignment": ["error", "always"],
207 | "operator-linebreak": ["error", "after"],
208 | "padded-blocks": ["error", "never"],
209 | "quote-props": ["error", "as-needed"],
210 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
211 | "require-jsdoc": "off",
212 | "semi": ["error", "always"],
213 | "semi-spacing": "error",
214 | "sort-imports": "off",
215 | "sort-vars": "off",
216 | "space-before-blocks": ["error", "always"],
217 | "space-before-function-paren": ["error", { "anonymous": "always", "named": "never" }],
218 | "space-in-parens": ["error", "never"],
219 | "space-infix-ops": "error",
220 | "space-unary-ops": ["error", { "words": true, "nonwords": false }],
221 | "spaced-comment": ["error", "always", { "markers": ["///"] }],
222 | "wrap-regex": "off",
223 |
224 | // ECMAScript 6
225 | "arrow-body-style": "off", // meh
226 | "arrow-parens": ["error", "as-needed"],
227 | "arrow-spacing": "error",
228 | "constructor-super": "error",
229 | "generator-star-spacing": ["error", "after"],
230 | "no-class-assign": "error",
231 | "no-confusing-arrow": "off",
232 | "no-const-assign": "error",
233 | "no-dupe-class-members": "error",
234 | "no-duplicate-imports": "error",
235 | "no-new-symbol": "error",
236 | "no-this-before-super": "error",
237 | "no-useless-computed-key": "error",
238 | "no-useless-constructor": "error",
239 | "no-var": "error",
240 | "object-shorthand": "error",
241 | "prefer-arrow-callback": "error",
242 | "prefer-const": ["error", { "ignoreReadBeforeAssign": true }],
243 | "prefer-reflect": "off",
244 | "prefer-rest-params": "error",
245 | "prefer-spread": "error",
246 | "prefer-template": "off",
247 | "require-yield": "error",
248 | "template-curly-spacing": ["error", "never"],
249 | "yield-star-spacing": ["error", "after"]
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/reference-implementation/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.log
3 | package-lock.json
4 |
5 | .nyc_output/
6 | coverage/
7 | generated/
8 | bundle.js
9 |
--------------------------------------------------------------------------------
/reference-implementation/COPYING.txt:
--------------------------------------------------------------------------------
1 | Creative Commons Legal Code
2 |
3 | CC0 1.0 Universal
4 |
5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
12 | HEREUNDER.
13 |
14 | Statement of Purpose
15 |
16 | The laws of most jurisdictions throughout the world automatically confer
17 | exclusive Copyright and Related Rights (defined below) upon the creator
18 | and subsequent owner(s) (each and all, an "owner") of an original work of
19 | authorship and/or a database (each, a "Work").
20 |
21 | Certain owners wish to permanently relinquish those rights to a Work for
22 | the purpose of contributing to a commons of creative, cultural and
23 | scientific works ("Commons") that the public can reliably and without fear
24 | of later claims of infringement build upon, modify, incorporate in other
25 | works, reuse and redistribute as freely as possible in any form whatsoever
26 | and for any purposes, including without limitation commercial purposes.
27 | These owners may contribute to the Commons to promote the ideal of a free
28 | culture and the further production of creative, cultural and scientific
29 | works, or to gain reputation or greater distribution for their Work in
30 | part through the use and efforts of others.
31 |
32 | For these and/or other purposes and motivations, and without any
33 | expectation of additional consideration or compensation, the person
34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she
35 | is an owner of Copyright and Related Rights in the Work, voluntarily
36 | elects to apply CC0 to the Work and publicly distribute the Work under its
37 | terms, with knowledge of his or her Copyright and Related Rights in the
38 | Work and the meaning and intended legal effect of CC0 on those rights.
39 |
40 | 1. Copyright and Related Rights. A Work made available under CC0 may be
41 | protected by copyright and related or neighboring rights ("Copyright and
42 | Related Rights"). Copyright and Related Rights include, but are not
43 | limited to, the following:
44 |
45 | i. the right to reproduce, adapt, distribute, perform, display,
46 | communicate, and translate a Work;
47 | ii. moral rights retained by the original author(s) and/or performer(s);
48 | iii. publicity and privacy rights pertaining to a person's image or
49 | likeness depicted in a Work;
50 | iv. rights protecting against unfair competition in regards to a Work,
51 | subject to the limitations in paragraph 4(a), below;
52 | v. rights protecting the extraction, dissemination, use and reuse of data
53 | in a Work;
54 | vi. database rights (such as those arising under Directive 96/9/EC of the
55 | European Parliament and of the Council of 11 March 1996 on the legal
56 | protection of databases, and under any national implementation
57 | thereof, including any amended or successor version of such
58 | directive); and
59 | vii. other similar, equivalent or corresponding rights throughout the
60 | world based on applicable law or treaty, and any national
61 | implementations thereof.
62 |
63 | 2. Waiver. To the greatest extent permitted by, but not in contravention
64 | of, applicable law, Affirmer hereby overtly, fully, permanently,
65 | irrevocably and unconditionally waives, abandons, and surrenders all of
66 | Affirmer's Copyright and Related Rights and associated claims and causes
67 | of action, whether now known or unknown (including existing as well as
68 | future claims and causes of action), in the Work (i) in all territories
69 | worldwide, (ii) for the maximum duration provided by applicable law or
70 | treaty (including future time extensions), (iii) in any current or future
71 | medium and for any number of copies, and (iv) for any purpose whatsoever,
72 | including without limitation commercial, advertising or promotional
73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
74 | member of the public at large and to the detriment of Affirmer's heirs and
75 | successors, fully intending that such Waiver shall not be subject to
76 | revocation, rescission, cancellation, termination, or any other legal or
77 | equitable action to disrupt the quiet enjoyment of the Work by the public
78 | as contemplated by Affirmer's express Statement of Purpose.
79 |
80 | 3. Public License Fallback. Should any part of the Waiver for any reason
81 | be judged legally invalid or ineffective under applicable law, then the
82 | Waiver shall be preserved to the maximum extent permitted taking into
83 | account Affirmer's express Statement of Purpose. In addition, to the
84 | extent the Waiver is so judged Affirmer hereby grants to each affected
85 | person a royalty-free, non transferable, non sublicensable, non exclusive,
86 | irrevocable and unconditional license to exercise Affirmer's Copyright and
87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the
88 | maximum duration provided by applicable law or treaty (including future
89 | time extensions), (iii) in any current or future medium and for any number
90 | of copies, and (iv) for any purpose whatsoever, including without
91 | limitation commercial, advertising or promotional purposes (the
92 | "License"). The License shall be deemed effective as of the date CC0 was
93 | applied by Affirmer to the Work. Should any part of the License for any
94 | reason be judged legally invalid or ineffective under applicable law, such
95 | partial invalidity or ineffectiveness shall not invalidate the remainder
96 | of the License, and in such case Affirmer hereby affirms that he or she
97 | will not (i) exercise any of his or her remaining Copyright and Related
98 | Rights in the Work or (ii) assert any associated claims and causes of
99 | action with respect to the Work, in either case contrary to Affirmer's
100 | express Statement of Purpose.
101 |
102 | 4. Limitations and Disclaimers.
103 |
104 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
105 | surrendered, licensed or otherwise affected by this document.
106 | b. Affirmer offers the Work as-is and makes no representations or
107 | warranties of any kind concerning the Work, express, implied,
108 | statutory or otherwise, including without limitation warranties of
109 | title, merchantability, fitness for a particular purpose, non
110 | infringement, or the absence of latent or other defects, accuracy, or
111 | the present or absence of errors, whether or not discoverable, all to
112 | the greatest extent permissible under applicable law.
113 | c. Affirmer disclaims responsibility for clearing rights of other persons
114 | that may apply to the Work or any use thereof, including without
115 | limitation any person's Copyright and Related Rights in the Work.
116 | Further, Affirmer disclaims responsibility for obtaining any necessary
117 | consents, permissions or other rights required for any use of the
118 | Work.
119 | d. Affirmer understands and acknowledges that Creative Commons is not a
120 | party to this document and has no duty or obligation with respect to
121 | this CC0 or use of the Work.
122 |
--------------------------------------------------------------------------------
/reference-implementation/LICENSE.md:
--------------------------------------------------------------------------------
1 | # WHATWG Streams Reference Implementation Licensing
2 |
3 | For the reference implementation and tests for the WHATWG Streams Standard.
4 |
5 | This code is dual-licensed under CC0 and the MIT license. You can choose which one you want to use.
6 |
7 | ## CC0
8 |
9 | To the extent possible under law, the authors have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty.
10 |
11 | You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see https://creativecommons.org/publicdomain/zero/1.0/.
12 |
13 | ## MIT
14 |
15 | Copyright (c) 2013–2015 Streams Standard Reference Implementation Authors
16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
18 |
19 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/reference-implementation/README.md:
--------------------------------------------------------------------------------
1 | # Reference Implementation and Tests
2 |
3 | This folder contains a reference implementation of the streams standard, inside `lib/`. It also contains infrastructure to run the web platform tests against the reference implementation, described below.
4 |
5 | ## Reference implementation
6 |
7 | The reference implementation is meant to be a fairly close transcription of the spec into JavaScript. It is written in modern JavaScript and primarily tested in [Node.js](https://nodejs.org/en/); at least version 6.0.0 is required.
8 |
9 | ## Tests
10 |
11 | Test coverage is not complete, but we do aim for it to be. Adding tests would be a great way to contribute to this project.
12 |
13 | You can check the test coverage at any time by running `npm run coverage` in this folder.
14 |
15 | To run all tests (and the lint step), run `npm test` in this folder.
16 |
17 | ### Web platform tests
18 |
19 | The test suite for this standard is written in [web platform tests](https://github.com/web-platform-tests/wpt) format.
20 |
21 | - To run the web platform tests, type `npm run wpt` in this folder.
22 | - To run specific test files, you can use a glob pattern, rooted at the streams directory: `npm run wpt -- "writable-streams/**"`
23 |
24 | The test runner here is a Node.js emulated-DOM environment, with the reference implementation loaded into it.
25 |
26 | **To sync your local web-platform-tests checkout with the one tracked by this repository**, type `npm run sync-wpt`. However, note that this will override any local modifications you've made, e.g. in the process of working on a spec change. It's thus good to do this before you start working on such a change.
27 |
28 | #### Upstream web platform tests
29 |
30 | The web platform tests for streams are found in the [streams directory](https://github.com/web-platform-tests/wpt/tree/master/streams) of the web platform tests repository, and maintained via pull requests to that repository. They are then pulled into this repository via a [Git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules).
31 |
32 | This means that in order to land a test change for these tests, you'll need to make a pull request to the web platform tests repository, and then update the submodule pointer in this repository (probably in the same pull request as your spec change). That can be done via the command
33 |
34 | ```
35 | git submodule update --remote web-platform-tests
36 | ```
37 |
38 | and then staging and commiting the submodule update.
39 |
40 | If you are working on a spec change and need to modify or add to these tests, what you can do is work directly in the `web-platform-tests` subdirectory. Create a branch there, where you modify the tests. You can then modify the spec and reference implementation to match your branch. Finally, you can use that branch to send a pull request to the web-platform-tests project.
41 |
42 | ## Diagnostics
43 |
44 | Diagnostic output is provided using the [debug](https://www.npmjs.com/package/debug) package. It is useful to understand the behaviour of the reference implementation, particularly in tests that exercise asynchronous behaviour. To enable, set the DEBUG environment variable. For example, in Bash,
45 |
46 | ```bash
47 | DEBUG=streams:* npm test
48 | ```
49 |
50 | See [lib/transform-stream.js](lib/transform-stream.js) for examples of how debug statements are used. Diagnostic coverage is sparse at the moment; we expect to add more diagnostics in an ad-hoc manner as they are needed.
51 |
--------------------------------------------------------------------------------
/reference-implementation/compile-idl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* eslint-disable no-console, no-process-exit */
3 | const { mkdirSync } = require('fs');
4 | const path = require('path');
5 | const Transformer = require('webidl2js');
6 |
7 | const input = path.resolve(__dirname, './lib');
8 | const output = path.resolve(__dirname, './generated');
9 |
10 | mkdirSync(output, { recursive: true });
11 |
12 | const transformer = new Transformer({
13 | implSuffix: '-impl',
14 | suppressErrors: true // until https://github.com/jsdom/webidl2js/pull/123 lands
15 | });
16 |
17 | transformer.addSource(input, input);
18 | transformer.generate(output)
19 | .catch(err => {
20 | console.error(err.stack);
21 | process.exit(1);
22 | });
23 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ByteLengthQueuingStrategy-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.implementation = class ByteLengthQueuingStrategyImpl {
4 | constructor(globalObject, [{ highWaterMark }]) {
5 | this._globalObject = globalObject;
6 | this.highWaterMark = highWaterMark;
7 | }
8 |
9 | get size() {
10 | initializeSizeFunction(this._globalObject);
11 | return sizeFunctionWeakMap.get(this._globalObject);
12 | }
13 | };
14 |
15 | const sizeFunctionWeakMap = new WeakMap();
16 | function initializeSizeFunction(globalObject) {
17 | if (sizeFunctionWeakMap.has(globalObject)) {
18 | return;
19 | }
20 |
21 | // We need to set the 'name' property:
22 | // The size function must not have a prototype property nor be a constructor
23 | const size = chunk => chunk.byteLength;
24 | sizeFunctionWeakMap.set(globalObject, size);
25 | }
26 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ByteLengthQueuingStrategy.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface ByteLengthQueuingStrategy {
3 | constructor(QueuingStrategyInit init);
4 |
5 | readonly attribute unrestricted double highWaterMark;
6 | readonly attribute Function size;
7 | };
8 |
--------------------------------------------------------------------------------
/reference-implementation/lib/CountQueuingStrategy-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.implementation = class CountQueuingStrategyImpl {
4 | constructor(globalObject, [{ highWaterMark }]) {
5 | this._globalObject = globalObject;
6 | this.highWaterMark = highWaterMark;
7 | }
8 |
9 | get size() {
10 | initializeSizeFunction(this._globalObject);
11 | return sizeFunctionWeakMap.get(this._globalObject);
12 | }
13 | };
14 |
15 | const sizeFunctionWeakMap = new WeakMap();
16 | function initializeSizeFunction(globalObject) {
17 | if (sizeFunctionWeakMap.has(globalObject)) {
18 | return;
19 | }
20 |
21 | // We need to set the 'name' property:
22 | // The size function must not have a prototype property nor be a constructor
23 | const size = () => 1;
24 | sizeFunctionWeakMap.set(globalObject, size);
25 | }
26 |
--------------------------------------------------------------------------------
/reference-implementation/lib/CountQueuingStrategy.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface CountQueuingStrategy {
3 | constructor(QueuingStrategyInit init);
4 |
5 | readonly attribute unrestricted double highWaterMark;
6 | readonly attribute Function size;
7 | };
8 |
--------------------------------------------------------------------------------
/reference-implementation/lib/QueuingStrategy.webidl:
--------------------------------------------------------------------------------
1 | dictionary QueuingStrategy {
2 | unrestricted double highWaterMark;
3 | QueuingStrategySize size;
4 | };
5 |
6 | callback QueuingStrategySize = unrestricted double (any chunk);
7 |
--------------------------------------------------------------------------------
/reference-implementation/lib/QueuingStrategyInit.webidl:
--------------------------------------------------------------------------------
1 | dictionary QueuingStrategyInit {
2 | required unrestricted double highWaterMark;
3 | };
4 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableByteStreamController-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 |
4 | const { CancelSteps, PullSteps, ReleaseSteps } = require('./abstract-ops/internal-methods.js');
5 | const { ResetQueue } = require('./abstract-ops/queue-with-sizes.js');
6 | const aos = require('./abstract-ops/readable-streams.js');
7 |
8 | exports.implementation = class ReadableByteStreamControllerImpl {
9 | get byobRequest() {
10 | return aos.ReadableByteStreamControllerGetBYOBRequest(this);
11 | }
12 |
13 | get desiredSize() {
14 | return aos.ReadableByteStreamControllerGetDesiredSize(this);
15 | }
16 |
17 | close() {
18 | if (this._closeRequested === true) {
19 | throw new TypeError('The stream has already been closed; do not close it again!');
20 | }
21 |
22 | const state = this._stream._state;
23 | if (state !== 'readable') {
24 | throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be closed`);
25 | }
26 |
27 | aos.ReadableByteStreamControllerClose(this);
28 | }
29 |
30 | enqueue(chunk) {
31 | if (chunk.byteLength === 0) {
32 | throw new TypeError('chunk must have non-zero byteLength');
33 | }
34 | if (chunk.buffer.byteLength === 0) {
35 | throw new TypeError('chunk\'s buffer must have non-zero byteLength');
36 | }
37 |
38 | if (this._closeRequested === true) {
39 | throw new TypeError('stream is closed or draining');
40 | }
41 |
42 | const state = this._stream._state;
43 | if (state !== 'readable') {
44 | throw new TypeError(`The stream (in ${state} state) is not in the readable state and cannot be enqueued to`);
45 | }
46 |
47 | aos.ReadableByteStreamControllerEnqueue(this, chunk);
48 | }
49 |
50 | error(e) {
51 | aos.ReadableByteStreamControllerError(this, e);
52 | }
53 |
54 | [CancelSteps](reason) {
55 | aos.ReadableByteStreamControllerClearPendingPullIntos(this);
56 |
57 | ResetQueue(this);
58 |
59 | const result = this._cancelAlgorithm(reason);
60 | aos.ReadableByteStreamControllerClearAlgorithms(this);
61 | return result;
62 | }
63 |
64 | [PullSteps](readRequest) {
65 | const stream = this._stream;
66 | assert(aos.ReadableStreamHasDefaultReader(stream) === true);
67 |
68 | if (this._queueTotalSize > 0) {
69 | assert(aos.ReadableStreamGetNumReadRequests(stream) === 0);
70 | aos.ReadableByteStreamControllerFillReadRequestFromQueue(this, readRequest);
71 | return;
72 | }
73 |
74 | const autoAllocateChunkSize = this._autoAllocateChunkSize;
75 | if (autoAllocateChunkSize !== undefined) {
76 | let buffer;
77 | try {
78 | buffer = new ArrayBuffer(autoAllocateChunkSize);
79 | } catch (bufferE) {
80 | readRequest.errorSteps(bufferE);
81 | return;
82 | }
83 |
84 | const pullIntoDescriptor = {
85 | buffer,
86 | bufferByteLength: autoAllocateChunkSize,
87 | byteOffset: 0,
88 | byteLength: autoAllocateChunkSize,
89 | bytesFilled: 0,
90 | minimumFill: 1,
91 | elementSize: 1,
92 | viewConstructor: Uint8Array,
93 | readerType: 'default'
94 | };
95 |
96 | this._pendingPullIntos.push(pullIntoDescriptor);
97 | }
98 |
99 | aos.ReadableStreamAddReadRequest(stream, readRequest);
100 | aos.ReadableByteStreamControllerCallPullIfNeeded(this);
101 | }
102 |
103 | [ReleaseSteps]() {
104 | if (this._pendingPullIntos.length > 0) {
105 | const firstPullInto = this._pendingPullIntos[0];
106 | firstPullInto.readerType = 'none';
107 |
108 | this._pendingPullIntos = [firstPullInto];
109 | }
110 | }
111 | };
112 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableByteStreamController.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface ReadableByteStreamController {
3 | readonly attribute ReadableStreamBYOBRequest? byobRequest;
4 | readonly attribute unrestricted double? desiredSize;
5 |
6 | undefined close();
7 | undefined enqueue(ArrayBufferView chunk);
8 | undefined error(optional any e);
9 | };
10 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStream-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 |
4 | const { newPromise, resolvePromise, rejectPromise, promiseResolvedWith, promiseRejectedWith,
5 | setPromiseIsHandledToTrue } = require('./helpers/webidl.js');
6 | const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/queuing-strategy.js');
7 | const aos = require('./abstract-ops/readable-streams.js');
8 | const wsAOs = require('./abstract-ops/writable-streams.js');
9 |
10 | const idlUtils = require('../generated/utils.js');
11 | const UnderlyingSource = require('../generated/UnderlyingSource.js');
12 |
13 | exports.implementation = class ReadableStreamImpl {
14 | constructor(globalObject, [underlyingSource, strategy]) {
15 | if (underlyingSource === undefined) {
16 | underlyingSource = null;
17 | }
18 | const underlyingSourceDict = UnderlyingSource.convert(globalObject, underlyingSource);
19 |
20 | aos.InitializeReadableStream(this);
21 |
22 | if (underlyingSourceDict.type === 'bytes') {
23 | if ('size' in strategy) {
24 | throw new RangeError('The strategy for a byte stream cannot have a size function');
25 | }
26 |
27 | const highWaterMark = ExtractHighWaterMark(strategy, 0);
28 | aos.SetUpReadableByteStreamControllerFromUnderlyingSource(
29 | this, underlyingSource, underlyingSourceDict, highWaterMark
30 | );
31 | } else {
32 | assert(!('type' in underlyingSourceDict));
33 | const sizeAlgorithm = ExtractSizeAlgorithm(strategy);
34 | const highWaterMark = ExtractHighWaterMark(strategy, 1);
35 | aos.SetUpReadableStreamDefaultControllerFromUnderlyingSource(
36 | this, underlyingSource, underlyingSourceDict, highWaterMark, sizeAlgorithm
37 | );
38 | }
39 | }
40 |
41 | get locked() {
42 | return aos.IsReadableStreamLocked(this);
43 | }
44 |
45 | cancel(reason) {
46 | if (aos.IsReadableStreamLocked(this) === true) {
47 | return promiseRejectedWith(new TypeError('Cannot cancel a stream that already has a reader'));
48 | }
49 |
50 | return aos.ReadableStreamCancel(this, reason);
51 | }
52 |
53 | getReader(options) {
54 | if (!('mode' in options)) {
55 | return aos.AcquireReadableStreamDefaultReader(this);
56 | }
57 |
58 | assert(options.mode === 'byob');
59 | return aos.AcquireReadableStreamBYOBReader(this);
60 | }
61 |
62 | pipeThrough(transform, options) {
63 | // Type checking here is needed until https://github.com/jsdom/webidl2js/issues/81 is fixed.
64 | if ('signal' in options) {
65 | if (!isAbortSignal(options.signal)) {
66 | throw new TypeError('Invalid signal argument');
67 | }
68 | }
69 |
70 | if (aos.IsReadableStreamLocked(this) === true) {
71 | throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream');
72 | }
73 | if (wsAOs.IsWritableStreamLocked(transform.writable) === true) {
74 | throw new TypeError('ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream');
75 | }
76 |
77 | const promise = aos.ReadableStreamPipeTo(
78 | this, transform.writable, options.preventClose, options.preventAbort, options.preventCancel, options.signal
79 | );
80 |
81 | setPromiseIsHandledToTrue(promise);
82 |
83 | return transform.readable;
84 | }
85 |
86 | pipeTo(destination, options) {
87 | // Type checking here is needed until https://github.com/jsdom/webidl2js/issues/81 is fixed.
88 | if ('signal' in options) {
89 | if (!isAbortSignal(options.signal)) {
90 | return promiseRejectedWith(new TypeError('Invalid signal argument'));
91 | }
92 | }
93 |
94 | if (aos.IsReadableStreamLocked(this) === true) {
95 | return promiseRejectedWith(
96 | new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream')
97 | );
98 | }
99 | if (wsAOs.IsWritableStreamLocked(destination) === true) {
100 | return promiseRejectedWith(
101 | new TypeError('ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream')
102 | );
103 | }
104 |
105 | return aos.ReadableStreamPipeTo(
106 | this, destination, options.preventClose, options.preventAbort, options.preventCancel, options.signal
107 | );
108 | }
109 |
110 | tee() {
111 | // Conversion here is only needed until https://github.com/jsdom/webidl2js/pull/108 gets merged.
112 | return aos.ReadableStreamTee(this, false).map(idlUtils.wrapperForImpl);
113 | }
114 |
115 | [idlUtils.asyncIteratorInit](iterator, [options]) {
116 | iterator._reader = aos.AcquireReadableStreamDefaultReader(this);
117 | iterator._preventCancel = options.preventCancel;
118 | }
119 |
120 | [idlUtils.asyncIteratorNext](iterator) {
121 | const reader = iterator._reader;
122 | assert(reader._stream !== undefined);
123 |
124 | const promise = newPromise();
125 | const readRequest = {
126 | chunkSteps: chunk => resolvePromise(promise, chunk),
127 | closeSteps: () => {
128 | aos.ReadableStreamDefaultReaderRelease(reader);
129 | resolvePromise(promise, idlUtils.asyncIteratorEOI);
130 | },
131 | errorSteps: e => {
132 | aos.ReadableStreamDefaultReaderRelease(reader);
133 | rejectPromise(promise, e);
134 | }
135 | };
136 | aos.ReadableStreamDefaultReaderRead(reader, readRequest);
137 | return promise;
138 | }
139 |
140 | [idlUtils.asyncIteratorReturn](iterator, arg) {
141 | const reader = iterator._reader;
142 | assert(reader._stream !== undefined);
143 | assert(reader._readRequests.length === 0);
144 |
145 | if (iterator._preventCancel === false) {
146 | const result = aos.ReadableStreamReaderGenericCancel(reader, arg);
147 | aos.ReadableStreamDefaultReaderRelease(reader);
148 | return result;
149 | }
150 |
151 | aos.ReadableStreamDefaultReaderRelease(reader);
152 | return promiseResolvedWith(undefined);
153 | }
154 |
155 | static from(asyncIterable) {
156 | return aos.ReadableStreamFromIterable(asyncIterable);
157 | }
158 | };
159 |
160 | // See pipeTo()/pipeThrough() for why this is needed.
161 | const abortedGetter = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted').get;
162 | function isAbortSignal(v) {
163 | try {
164 | abortedGetter.call(v);
165 | return true;
166 | } catch {
167 | return false;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStream.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet), Transferable]
2 | interface ReadableStream {
3 | constructor(optional object underlyingSource, optional QueuingStrategy strategy = {});
4 |
5 | static ReadableStream from(any asyncIterable);
6 |
7 | readonly attribute boolean locked;
8 |
9 | Promise cancel(optional any reason);
10 | ReadableStreamReader getReader(optional ReadableStreamGetReaderOptions options = {});
11 | ReadableStream pipeThrough(ReadableWritablePair transform, optional StreamPipeOptions options = {});
12 | Promise pipeTo(WritableStream destination, optional StreamPipeOptions options = {});
13 | sequence tee();
14 |
15 | [WebIDL2JSHasReturnSteps] async iterable(optional ReadableStreamIteratorOptions options = {});
16 | };
17 |
18 | typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader;
19 |
20 | enum ReadableStreamReaderMode { "byob" };
21 |
22 | dictionary ReadableStreamGetReaderOptions {
23 | ReadableStreamReaderMode mode;
24 | };
25 |
26 | dictionary ReadableStreamIteratorOptions {
27 | boolean preventCancel = false;
28 | };
29 |
30 | dictionary ReadableWritablePair {
31 | required ReadableStream readable;
32 | required WritableStream writable;
33 | };
34 |
35 | dictionary StreamPipeOptions {
36 | boolean preventClose = false;
37 | boolean preventAbort = false;
38 | boolean preventCancel = false;
39 | AbortSignal signal;
40 | };
41 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamBYOBReader-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { newPromise, resolvePromise, rejectPromise, promiseRejectedWith } = require('./helpers/webidl.js');
4 | const { IsDetachedBuffer } = require('./abstract-ops/ecmascript.js');
5 | const aos = require('./abstract-ops/readable-streams.js');
6 | const { mixin } = require('./helpers/miscellaneous.js');
7 | const ReadableStreamGenericReaderImpl = require('./ReadableStreamGenericReader-impl.js').implementation;
8 |
9 | class ReadableStreamBYOBReaderImpl {
10 | constructor(globalObject, [stream]) {
11 | aos.SetUpReadableStreamBYOBReader(this, stream);
12 | }
13 |
14 | read(view, options) {
15 | if (view.byteLength === 0) {
16 | return promiseRejectedWith(new TypeError('view must have non-zero byteLength'));
17 | }
18 | if (view.buffer.byteLength === 0) {
19 | return promiseRejectedWith(new TypeError('view\'s buffer must have non-zero byteLength'));
20 | }
21 | if (IsDetachedBuffer(view.buffer) === true) {
22 | return promiseRejectedWith(new TypeError('view\'s buffer has been detached'));
23 | }
24 |
25 | if (options.min === 0) {
26 | return promiseRejectedWith(
27 | new TypeError('options.min must be greater than 0')
28 | );
29 | }
30 | if (view.constructor !== DataView) {
31 | if (options.min > view.length) {
32 | return promiseRejectedWith(
33 | new RangeError('options.min must be less than or equal to view\'s length')
34 | );
35 | }
36 | } else if (options.min > view.byteLength) {
37 | return promiseRejectedWith(
38 | new RangeError('options.min must be less than or equal to view\'s byteLength')
39 | );
40 | }
41 |
42 | if (this._stream === undefined) {
43 | return promiseRejectedWith(readerLockException('read'));
44 | }
45 |
46 | const promise = newPromise();
47 | const readIntoRequest = {
48 | chunkSteps: chunk => resolvePromise(promise, { value: chunk, done: false }),
49 | closeSteps: chunk => resolvePromise(promise, { value: chunk, done: true }),
50 | errorSteps: e => rejectPromise(promise, e)
51 | };
52 | aos.ReadableStreamBYOBReaderRead(this, view, options.min, readIntoRequest);
53 | return promise;
54 | }
55 |
56 | releaseLock() {
57 | if (this._stream === undefined) {
58 | return;
59 | }
60 |
61 | aos.ReadableStreamBYOBReaderRelease(this);
62 | }
63 | }
64 |
65 | mixin(ReadableStreamBYOBReaderImpl.prototype, ReadableStreamGenericReaderImpl.prototype);
66 |
67 | exports.implementation = ReadableStreamBYOBReaderImpl;
68 |
69 | function readerLockException(name) {
70 | return new TypeError('Cannot ' + name + ' a stream using a released reader');
71 | }
72 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamBYOBReader.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface ReadableStreamBYOBReader {
3 | constructor(ReadableStream stream);
4 |
5 | Promise read(ArrayBufferView view, optional ReadableStreamBYOBReaderReadOptions options = {});
6 | undefined releaseLock();
7 | };
8 | ReadableStreamBYOBReader includes ReadableStreamGenericReader;
9 |
10 | dictionary ReadableStreamBYOBReaderReadOptions {
11 | [EnforceRange] unsigned long long min = 1;
12 | };
13 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamBYOBRequest-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 |
4 | const { IsDetachedBuffer } = require('./abstract-ops/ecmascript.js');
5 | const aos = require('./abstract-ops/readable-streams.js');
6 |
7 | exports.implementation = class ReadableStreamBYOBRequestImpl {
8 | get view() {
9 | return this._view;
10 | }
11 |
12 | respond(bytesWritten) {
13 | if (this._controller === undefined) {
14 | throw new TypeError('This BYOB request has been invalidated');
15 | }
16 |
17 | if (IsDetachedBuffer(this._view.buffer) === true) {
18 | throw new TypeError('The BYOB request\'s buffer has been detached and so cannot be used as a response');
19 | }
20 |
21 | assert(this._view.byteLength > 0);
22 | assert(this._view.buffer.byteLength > 0);
23 |
24 | aos.ReadableByteStreamControllerRespond(this._controller, bytesWritten);
25 | }
26 |
27 | respondWithNewView(view) {
28 | if (this._controller === undefined) {
29 | throw new TypeError('This BYOB request has been invalidated');
30 | }
31 |
32 | if (IsDetachedBuffer(view.buffer) === true) {
33 | throw new TypeError('The given view\'s buffer has been detached and so cannot be used as a response');
34 | }
35 |
36 | aos.ReadableByteStreamControllerRespondWithNewView(this._controller, view);
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamBYOBRequest.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface ReadableStreamBYOBRequest {
3 | readonly attribute ArrayBufferView? view;
4 |
5 | undefined respond([EnforceRange] unsigned long long bytesWritten);
6 | undefined respondWithNewView(ArrayBufferView view);
7 | };
8 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamDefaultController-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { CancelSteps, PullSteps, ReleaseSteps } = require('./abstract-ops/internal-methods.js');
4 | const { DequeueValue, ResetQueue } = require('./abstract-ops/queue-with-sizes.js');
5 | const aos = require('./abstract-ops/readable-streams.js');
6 |
7 | exports.implementation = class ReadableStreamDefaultControllerImpl {
8 | get desiredSize() {
9 | return aos.ReadableStreamDefaultControllerGetDesiredSize(this);
10 | }
11 |
12 | close() {
13 | if (aos.ReadableStreamDefaultControllerCanCloseOrEnqueue(this) === false) {
14 | throw new TypeError('The stream is not in a state that permits close');
15 | }
16 |
17 | aos.ReadableStreamDefaultControllerClose(this);
18 | }
19 |
20 | enqueue(chunk) {
21 | if (aos.ReadableStreamDefaultControllerCanCloseOrEnqueue(this) === false) {
22 | throw new TypeError('The stream is not in a state that permits enqueue');
23 | }
24 |
25 | return aos.ReadableStreamDefaultControllerEnqueue(this, chunk);
26 | }
27 |
28 | error(e) {
29 | aos.ReadableStreamDefaultControllerError(this, e);
30 | }
31 |
32 | [CancelSteps](reason) {
33 | ResetQueue(this);
34 | const result = this._cancelAlgorithm(reason);
35 | aos.ReadableStreamDefaultControllerClearAlgorithms(this);
36 | return result;
37 | }
38 |
39 | [PullSteps](readRequest) {
40 | const stream = this._stream;
41 |
42 | if (this._queue.length > 0) {
43 | const chunk = DequeueValue(this);
44 |
45 | if (this._closeRequested === true && this._queue.length === 0) {
46 | aos.ReadableStreamDefaultControllerClearAlgorithms(this);
47 | aos.ReadableStreamClose(stream);
48 | } else {
49 | aos.ReadableStreamDefaultControllerCallPullIfNeeded(this);
50 | }
51 |
52 | readRequest.chunkSteps(chunk);
53 | } else {
54 | aos.ReadableStreamAddReadRequest(stream, readRequest);
55 | aos.ReadableStreamDefaultControllerCallPullIfNeeded(this);
56 | }
57 | }
58 |
59 | [ReleaseSteps]() {}
60 | };
61 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamDefaultController.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface ReadableStreamDefaultController {
3 | readonly attribute unrestricted double? desiredSize;
4 |
5 | undefined close();
6 | undefined enqueue(optional any chunk);
7 | undefined error(optional any e);
8 | };
9 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamDefaultReader-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { newPromise, resolvePromise, rejectPromise, promiseRejectedWith } = require('./helpers/webidl.js');
4 | const aos = require('./abstract-ops/readable-streams.js');
5 | const { mixin } = require('./helpers/miscellaneous.js');
6 | const ReadableStreamGenericReaderImpl = require('./ReadableStreamGenericReader-impl.js').implementation;
7 |
8 | class ReadableStreamDefaultReaderImpl {
9 | constructor(globalObject, [stream]) {
10 | aos.SetUpReadableStreamDefaultReader(this, stream);
11 | }
12 |
13 | read() {
14 | if (this._stream === undefined) {
15 | return promiseRejectedWith(readerLockException('read from'));
16 | }
17 |
18 | const promise = newPromise();
19 | const readRequest = {
20 | chunkSteps: chunk => resolvePromise(promise, { value: chunk, done: false }),
21 | closeSteps: () => resolvePromise(promise, { value: undefined, done: true }),
22 | errorSteps: e => rejectPromise(promise, e)
23 | };
24 |
25 | aos.ReadableStreamDefaultReaderRead(this, readRequest);
26 | return promise;
27 | }
28 |
29 | releaseLock() {
30 | if (this._stream === undefined) {
31 | return;
32 | }
33 |
34 | aos.ReadableStreamDefaultReaderRelease(this);
35 | }
36 | }
37 |
38 | mixin(ReadableStreamDefaultReaderImpl.prototype, ReadableStreamGenericReaderImpl.prototype);
39 |
40 | exports.implementation = ReadableStreamDefaultReaderImpl;
41 |
42 | function readerLockException(name) {
43 | return new TypeError('Cannot ' + name + ' a stream using a released reader');
44 | }
45 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamDefaultReader.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface ReadableStreamDefaultReader {
3 | constructor(ReadableStream stream);
4 |
5 | Promise read();
6 | undefined releaseLock();
7 | };
8 | ReadableStreamDefaultReader includes ReadableStreamGenericReader;
9 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamGenericReader-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { promiseRejectedWith } = require('./helpers/webidl.js');
4 | const aos = require('./abstract-ops/readable-streams.js');
5 |
6 | exports.implementation = class ReadableStreamGenericReaderImpl {
7 | get closed() {
8 | return this._closedPromise;
9 | }
10 |
11 | cancel(reason) {
12 | if (this._stream === undefined) {
13 | return promiseRejectedWith(readerLockException('cancel'));
14 | }
15 |
16 | return aos.ReadableStreamReaderGenericCancel(this, reason);
17 | }
18 | };
19 |
20 | function readerLockException(name) {
21 | return new TypeError('Cannot ' + name + ' a stream using a released reader');
22 | }
23 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamGenericReader.webidl:
--------------------------------------------------------------------------------
1 | interface mixin ReadableStreamGenericReader {
2 | readonly attribute Promise closed;
3 |
4 | Promise cancel(optional any reason);
5 | };
6 |
--------------------------------------------------------------------------------
/reference-implementation/lib/ReadableStreamReadResult.webidl:
--------------------------------------------------------------------------------
1 | dictionary ReadableStreamReadResult {
2 | any value;
3 | boolean done;
4 | };
5 |
--------------------------------------------------------------------------------
/reference-implementation/lib/TransformStream-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { newPromise, resolvePromise } = require('./helpers/webidl.js');
4 | const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/queuing-strategy.js');
5 | const aos = require('./abstract-ops/transform-streams.js');
6 |
7 | const Transformer = require('../generated/Transformer.js');
8 |
9 | exports.implementation = class TransformStreamImpl {
10 | constructor(globalObject, [transformer, writableStrategy, readableStrategy]) {
11 | if (transformer === undefined) {
12 | transformer = null;
13 | }
14 | const transformerDict = Transformer.convert(globalObject, transformer);
15 | if ('readableType' in transformerDict) {
16 | throw new RangeError('Invalid readableType specified');
17 | }
18 | if ('writableType' in transformerDict) {
19 | throw new RangeError('Invalid writableType specified');
20 | }
21 |
22 | const readableHighWaterMark = ExtractHighWaterMark(readableStrategy, 0);
23 | const readableSizeAlgorithm = ExtractSizeAlgorithm(readableStrategy);
24 | const writableHighWaterMark = ExtractHighWaterMark(writableStrategy, 1);
25 | const writableSizeAlgorithm = ExtractSizeAlgorithm(writableStrategy);
26 |
27 | const startPromise = newPromise();
28 |
29 | aos.InitializeTransformStream(
30 | this, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm
31 | );
32 | aos.SetUpTransformStreamDefaultControllerFromTransformer(this, transformer, transformerDict);
33 |
34 | if ('start' in transformerDict) {
35 | resolvePromise(startPromise, transformerDict.start.call(transformer, this._controller));
36 | } else {
37 | resolvePromise(startPromise, undefined);
38 | }
39 | }
40 |
41 | get readable() {
42 | return this._readable;
43 | }
44 |
45 | get writable() {
46 | return this._writable;
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/reference-implementation/lib/TransformStream.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet), Transferable]
2 | interface TransformStream {
3 | constructor(optional object transformer,
4 | optional QueuingStrategy writableStrategy = {},
5 | optional QueuingStrategy readableStrategy = {});
6 |
7 | readonly attribute ReadableStream readable;
8 | readonly attribute WritableStream writable;
9 | };
10 |
--------------------------------------------------------------------------------
/reference-implementation/lib/TransformStreamDefaultController-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const aos = require('./abstract-ops/transform-streams.js');
4 | const rsAOs = require('./abstract-ops/readable-streams.js');
5 |
6 | exports.implementation = class TransformStreamDefaultController {
7 | get desiredSize() {
8 | const readableController = this._stream._readable._controller;
9 | return rsAOs.ReadableStreamDefaultControllerGetDesiredSize(readableController);
10 | }
11 |
12 | enqueue(chunk) {
13 | aos.TransformStreamDefaultControllerEnqueue(this, chunk);
14 | }
15 |
16 | error(reason) {
17 | aos.TransformStreamDefaultControllerError(this, reason);
18 | }
19 |
20 | terminate() {
21 | aos.TransformStreamDefaultControllerTerminate(this);
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/reference-implementation/lib/TransformStreamDefaultController.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface TransformStreamDefaultController {
3 | readonly attribute unrestricted double? desiredSize;
4 |
5 | undefined enqueue(optional any chunk);
6 | undefined error(optional any reason);
7 | undefined terminate();
8 | };
9 |
--------------------------------------------------------------------------------
/reference-implementation/lib/Transformer.webidl:
--------------------------------------------------------------------------------
1 | dictionary Transformer {
2 | TransformerStartCallback start;
3 | TransformerTransformCallback transform;
4 | TransformerFlushCallback flush;
5 | TransformerCancelCallback cancel;
6 | any readableType;
7 | any writableType;
8 | };
9 |
10 | callback TransformerStartCallback = any (TransformStreamDefaultController controller);
11 | callback TransformerFlushCallback = Promise (TransformStreamDefaultController controller);
12 | callback TransformerTransformCallback = Promise (any chunk, TransformStreamDefaultController controller);
13 | callback TransformerCancelCallback = Promise (any reason);
14 |
--------------------------------------------------------------------------------
/reference-implementation/lib/UnderlyingSink.webidl:
--------------------------------------------------------------------------------
1 | dictionary UnderlyingSink {
2 | UnderlyingSinkStartCallback start;
3 | UnderlyingSinkWriteCallback write;
4 | UnderlyingSinkCloseCallback close;
5 | UnderlyingSinkAbortCallback abort;
6 | any type;
7 | };
8 |
9 | callback UnderlyingSinkStartCallback = any (WritableStreamDefaultController controller);
10 | callback UnderlyingSinkWriteCallback = Promise (any chunk, WritableStreamDefaultController controller);
11 | callback UnderlyingSinkCloseCallback = Promise ();
12 | callback UnderlyingSinkAbortCallback = Promise (optional any reason);
13 |
--------------------------------------------------------------------------------
/reference-implementation/lib/UnderlyingSource.webidl:
--------------------------------------------------------------------------------
1 | dictionary UnderlyingSource {
2 | UnderlyingSourceStartCallback start;
3 | UnderlyingSourcePullCallback pull;
4 | UnderlyingSourceCancelCallback cancel;
5 | ReadableStreamType type;
6 | [EnforceRange] unsigned long long autoAllocateChunkSize;
7 | };
8 |
9 | typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController;
10 |
11 | callback UnderlyingSourceStartCallback = any (ReadableStreamController controller);
12 | callback UnderlyingSourcePullCallback = Promise (ReadableStreamController controller);
13 | callback UnderlyingSourceCancelCallback = Promise (optional any reason);
14 |
15 | enum ReadableStreamType { "bytes" };
16 |
--------------------------------------------------------------------------------
/reference-implementation/lib/WritableStream-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { promiseRejectedWith } = require('./helpers/webidl.js');
4 | const { ExtractHighWaterMark, ExtractSizeAlgorithm } = require('./abstract-ops/queuing-strategy.js');
5 | const aos = require('./abstract-ops/writable-streams.js');
6 |
7 | const UnderlyingSink = require('../generated/UnderlyingSink.js');
8 |
9 | exports.implementation = class WritableStreamImpl {
10 | constructor(globalObject, [underlyingSink, strategy]) {
11 | if (underlyingSink === undefined) {
12 | underlyingSink = null;
13 | }
14 | const underlyingSinkDict = UnderlyingSink.convert(globalObject, underlyingSink);
15 | if ('type' in underlyingSinkDict) {
16 | throw new RangeError('Invalid type is specified');
17 | }
18 |
19 | aos.InitializeWritableStream(this);
20 |
21 | const sizeAlgorithm = ExtractSizeAlgorithm(strategy);
22 | const highWaterMark = ExtractHighWaterMark(strategy, 1);
23 |
24 | aos.SetUpWritableStreamDefaultControllerFromUnderlyingSink(
25 | this, underlyingSink, underlyingSinkDict, highWaterMark, sizeAlgorithm
26 | );
27 | }
28 |
29 | get locked() {
30 | return aos.IsWritableStreamLocked(this);
31 | }
32 |
33 | abort(reason) {
34 | if (aos.IsWritableStreamLocked(this) === true) {
35 | return promiseRejectedWith(new TypeError('Cannot abort a stream that already has a writer'));
36 | }
37 |
38 | return aos.WritableStreamAbort(this, reason);
39 | }
40 |
41 | close() {
42 | if (aos.IsWritableStreamLocked(this) === true) {
43 | return promiseRejectedWith(new TypeError('Cannot close a stream that already has a writer'));
44 | }
45 |
46 | if (aos.WritableStreamCloseQueuedOrInFlight(this) === true) {
47 | return promiseRejectedWith(new TypeError('Cannot close an already-closing stream'));
48 | }
49 |
50 | return aos.WritableStreamClose(this);
51 | }
52 |
53 | getWriter() {
54 | return aos.AcquireWritableStreamDefaultWriter(this);
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/reference-implementation/lib/WritableStream.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet), Transferable]
2 | interface WritableStream {
3 | constructor(optional object underlyingSink, optional QueuingStrategy strategy = {});
4 |
5 | readonly attribute boolean locked;
6 |
7 | Promise abort(optional any reason);
8 | Promise close();
9 | WritableStreamDefaultWriter getWriter();
10 | };
11 |
--------------------------------------------------------------------------------
/reference-implementation/lib/WritableStreamDefaultController-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const aos = require('./abstract-ops/writable-streams.js');
4 | const { AbortSteps, ErrorSteps } = require('./abstract-ops/internal-methods.js');
5 | const { ResetQueue } = require('./abstract-ops/queue-with-sizes.js');
6 |
7 | exports.implementation = class WritableStreamDefaultControllerImpl {
8 | get signal() {
9 | return this._abortController.signal;
10 | }
11 |
12 | error(e) {
13 | const state = this._stream._state;
14 |
15 | if (state !== 'writable') {
16 | // The stream is closed, errored or will be soon. The sink can't do anything useful if it gets an error here, so
17 | // just treat it as a no-op.
18 | return;
19 | }
20 |
21 | aos.WritableStreamDefaultControllerError(this, e);
22 | }
23 |
24 | [AbortSteps](reason) {
25 | const result = this._abortAlgorithm(reason);
26 | aos.WritableStreamDefaultControllerClearAlgorithms(this);
27 | return result;
28 | }
29 |
30 | [ErrorSteps]() {
31 | ResetQueue(this);
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/reference-implementation/lib/WritableStreamDefaultController.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface WritableStreamDefaultController {
3 | readonly attribute AbortSignal signal;
4 | undefined error(optional any e);
5 | };
6 |
--------------------------------------------------------------------------------
/reference-implementation/lib/WritableStreamDefaultWriter-impl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 |
4 | const { promiseRejectedWith } = require('./helpers/webidl.js');
5 | const aos = require('./abstract-ops/writable-streams.js');
6 |
7 | exports.implementation = class WritableStreamDefaultWriterImpl {
8 | constructor(globalObject, [stream]) {
9 | aos.SetUpWritableStreamDefaultWriter(this, stream);
10 | }
11 |
12 | get closed() {
13 | return this._closedPromise;
14 | }
15 |
16 | get desiredSize() {
17 | if (this._stream === undefined) {
18 | throw defaultWriterLockException('desiredSize');
19 | }
20 |
21 | return aos.WritableStreamDefaultWriterGetDesiredSize(this);
22 | }
23 |
24 | get ready() {
25 | return this._readyPromise;
26 | }
27 |
28 | abort(reason) {
29 | if (this._stream === undefined) {
30 | return promiseRejectedWith(defaultWriterLockException('abort'));
31 | }
32 |
33 | return aos.WritableStreamDefaultWriterAbort(this, reason);
34 | }
35 |
36 | close() {
37 | const stream = this._stream;
38 |
39 | if (stream === undefined) {
40 | return promiseRejectedWith(defaultWriterLockException('close'));
41 | }
42 |
43 | if (aos.WritableStreamCloseQueuedOrInFlight(stream) === true) {
44 | return promiseRejectedWith(new TypeError('Cannot close an already-closing stream'));
45 | }
46 |
47 | return aos.WritableStreamDefaultWriterClose(this);
48 | }
49 |
50 | releaseLock() {
51 | const stream = this._stream;
52 |
53 | if (stream === undefined) {
54 | return;
55 | }
56 |
57 | assert(stream._writer !== undefined);
58 |
59 | aos.WritableStreamDefaultWriterRelease(this);
60 | }
61 |
62 | write(chunk) {
63 | if (this._stream === undefined) {
64 | return promiseRejectedWith(defaultWriterLockException('write to'));
65 | }
66 |
67 | return aos.WritableStreamDefaultWriterWrite(this, chunk);
68 | }
69 | };
70 |
71 | function defaultWriterLockException(name) {
72 | return new TypeError('Cannot ' + name + ' a stream using a released writer');
73 | }
74 |
--------------------------------------------------------------------------------
/reference-implementation/lib/WritableStreamDefaultWriter.webidl:
--------------------------------------------------------------------------------
1 | [Exposed=(Window,Worker,Worklet)]
2 | interface WritableStreamDefaultWriter {
3 | constructor(WritableStream stream);
4 |
5 | readonly attribute Promise closed;
6 | readonly attribute unrestricted double? desiredSize;
7 | readonly attribute Promise ready;
8 |
9 | Promise abort(optional any reason);
10 | Promise close();
11 | undefined releaseLock();
12 | Promise write(optional any chunk);
13 | };
14 |
--------------------------------------------------------------------------------
/reference-implementation/lib/abstract-ops/ecmascript.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 |
4 | const isFakeDetached = Symbol('is "detached" for our purposes');
5 |
6 | exports.typeIsObject = x => (typeof x === 'object' && x !== null) || typeof x === 'function';
7 |
8 | exports.CreateArrayFromList = elements => {
9 | // We use arrays to represent lists, so this is basically a no-op.
10 | // Do a slice though just in case we happen to depend on the unique-ness.
11 | return elements.slice();
12 | };
13 |
14 | exports.CopyDataBlockBytes = (dest, destOffset, src, srcOffset, n) => {
15 | new Uint8Array(dest).set(new Uint8Array(src, srcOffset, n), destOffset);
16 | };
17 |
18 | // Not implemented correctly
19 | exports.TransferArrayBuffer = O => {
20 | assert(!exports.IsDetachedBuffer(O));
21 | const transferredIshVersion = O.slice();
22 |
23 | // This is specifically to fool tests that test "is transferred" by taking a non-zero-length
24 | // ArrayBuffer and checking if its byteLength starts returning 0.
25 | Object.defineProperty(O, 'byteLength', {
26 | get() {
27 | return 0;
28 | }
29 | });
30 | O[isFakeDetached] = true;
31 |
32 | return transferredIshVersion;
33 | };
34 |
35 | // Not implemented correctly
36 | exports.CanTransferArrayBuffer = O => {
37 | return !exports.IsDetachedBuffer(O);
38 | };
39 |
40 | // Not implemented correctly
41 | exports.IsDetachedBuffer = O => {
42 | return isFakeDetached in O;
43 | };
44 |
45 | exports.Call = (F, V, args = []) => {
46 | if (typeof F !== 'function') {
47 | throw new TypeError('Argument is not a function');
48 | }
49 |
50 | return Reflect.apply(F, V, args);
51 | };
52 |
53 | exports.GetMethod = (V, P) => {
54 | const func = V[P];
55 | if (func === undefined || func === null) {
56 | return undefined;
57 | }
58 | if (typeof func !== 'function') {
59 | throw new TypeError(`${P} is not a function`);
60 | }
61 | return func;
62 | };
63 |
64 | exports.CreateAsyncFromSyncIterator = syncIteratorRecord => {
65 | // Instead of re-implementing CreateAsyncFromSyncIterator and %AsyncFromSyncIteratorPrototype%,
66 | // we use yield* inside an async generator function to achieve the same result.
67 |
68 | // Wrap the sync iterator inside a sync iterable, so we can use it with yield*.
69 | const syncIterable = {
70 | [Symbol.iterator]: () => syncIteratorRecord.iterator
71 | };
72 | // Create an async generator function and immediately invoke it.
73 | const asyncIterator = (async function* () {
74 | return yield* syncIterable;
75 | }());
76 | // Return as an async iterator record.
77 | const nextMethod = asyncIterator.next;
78 | return { iterator: asyncIterator, nextMethod, done: false };
79 | };
80 |
81 | exports.GetIterator = (obj, hint = 'sync', method) => {
82 | assert(hint === 'sync' || hint === 'async');
83 | if (method === undefined) {
84 | if (hint === 'async') {
85 | method = exports.GetMethod(obj, Symbol.asyncIterator);
86 | if (method === undefined) {
87 | const syncMethod = exports.GetMethod(obj, Symbol.iterator);
88 | const syncIteratorRecord = exports.GetIterator(obj, 'sync', syncMethod);
89 | return exports.CreateAsyncFromSyncIterator(syncIteratorRecord);
90 | }
91 | } else {
92 | method = exports.GetMethod(obj, Symbol.iterator);
93 | }
94 | }
95 | const iterator = exports.Call(method, obj);
96 | if (!exports.typeIsObject(iterator)) {
97 | throw new TypeError('The iterator method must return an object');
98 | }
99 | const nextMethod = iterator.next;
100 | return { iterator, nextMethod, done: false };
101 | };
102 |
103 | exports.IteratorNext = (iteratorRecord, value) => {
104 | let result;
105 | if (value === undefined) {
106 | result = exports.Call(iteratorRecord.nextMethod, iteratorRecord.iterator);
107 | } else {
108 | result = exports.Call(iteratorRecord.nextMethod, iteratorRecord.iterator, [value]);
109 | }
110 | if (!exports.typeIsObject(result)) {
111 | throw new TypeError('The iterator.next() method must return an object');
112 | }
113 | return result;
114 | };
115 |
116 | exports.IteratorComplete = iterResult => {
117 | assert(exports.typeIsObject(iterResult));
118 | return Boolean(iterResult.done);
119 | };
120 |
121 | exports.IteratorValue = iterResult => {
122 | assert(exports.typeIsObject(iterResult));
123 | return iterResult.value;
124 | };
125 |
--------------------------------------------------------------------------------
/reference-implementation/lib/abstract-ops/internal-methods.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.AbortSteps = Symbol('[[AbortSteps]]');
4 | exports.ErrorSteps = Symbol('[[ErrorSteps]]');
5 | exports.CancelSteps = Symbol('[[CancelSteps]]');
6 | exports.PullSteps = Symbol('[[PullSteps]]');
7 | exports.ReleaseSteps = Symbol('[[ReleaseSteps]]');
8 |
--------------------------------------------------------------------------------
/reference-implementation/lib/abstract-ops/miscellaneous.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { IsDetachedBuffer } = require('./ecmascript');
3 |
4 | exports.IsNonNegativeNumber = v => {
5 | if (typeof v !== 'number') {
6 | return false;
7 | }
8 |
9 | if (Number.isNaN(v)) {
10 | return false;
11 | }
12 |
13 | if (v < 0) {
14 | return false;
15 | }
16 |
17 | return true;
18 | };
19 |
20 | exports.CloneAsUint8Array = O => {
21 | const buffer = O.buffer.slice(O.byteOffset, O.byteOffset + O.byteLength);
22 | return new Uint8Array(buffer);
23 | };
24 |
25 | exports.CanCopyDataBlockBytes = (toBuffer, toIndex, fromBuffer, fromIndex, count) => {
26 | if (toBuffer === fromBuffer) {
27 | return false;
28 | }
29 | if (IsDetachedBuffer(toBuffer) === true) {
30 | return false;
31 | }
32 | if (IsDetachedBuffer(fromBuffer) === true) {
33 | return false;
34 | }
35 | if (toIndex + count > toBuffer.byteLength) {
36 | return false;
37 | }
38 | if (fromIndex + count > fromBuffer.byteLength) {
39 | return false;
40 | }
41 | return true;
42 | };
43 |
--------------------------------------------------------------------------------
/reference-implementation/lib/abstract-ops/queue-with-sizes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 | const { IsNonNegativeNumber } = require('./miscellaneous.js');
4 |
5 | exports.DequeueValue = container => {
6 | assert('_queue' in container && '_queueTotalSize' in container);
7 | assert(container._queue.length > 0);
8 |
9 | const pair = container._queue.shift();
10 | container._queueTotalSize -= pair.size;
11 | if (container._queueTotalSize < 0) {
12 | container._queueTotalSize = 0;
13 | }
14 |
15 | return pair.value;
16 | };
17 |
18 | exports.EnqueueValueWithSize = (container, value, size) => {
19 | assert('_queue' in container && '_queueTotalSize' in container);
20 |
21 | if (!IsNonNegativeNumber(size)) {
22 | throw new RangeError('Size must be a finite, non-NaN, non-negative number.');
23 | }
24 | if (size === Infinity) {
25 | throw new RangeError('Size must be a finite, non-NaN, non-negative number.');
26 | }
27 |
28 | container._queue.push({ value, size });
29 | container._queueTotalSize += size;
30 | };
31 |
32 | exports.PeekQueueValue = container => {
33 | assert('_queue' in container && '_queueTotalSize' in container);
34 | assert(container._queue.length > 0);
35 |
36 | const pair = container._queue[0];
37 | return pair.value;
38 | };
39 |
40 | exports.ResetQueue = container => {
41 | assert('_queue' in container && '_queueTotalSize' in container);
42 |
43 | container._queue = [];
44 | container._queueTotalSize = 0;
45 | };
46 |
--------------------------------------------------------------------------------
/reference-implementation/lib/abstract-ops/queuing-strategy.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.ExtractHighWaterMark = (strategy, defaultHWM) => {
4 | if (!('highWaterMark' in strategy)) {
5 | return defaultHWM;
6 | }
7 |
8 | const { highWaterMark } = strategy;
9 | if (Number.isNaN(highWaterMark) || highWaterMark < 0) {
10 | throw new RangeError('Invalid highWaterMark');
11 | }
12 |
13 | return highWaterMark;
14 | };
15 |
16 | exports.ExtractSizeAlgorithm = strategy => {
17 | const { size } = strategy;
18 |
19 | if (!size) {
20 | return () => 1;
21 | }
22 |
23 | // This is silly, but more obviously matches the spec (which distinguishes between algorithms and JS functions).
24 | return chunk => size(chunk);
25 | };
26 |
--------------------------------------------------------------------------------
/reference-implementation/lib/abstract-ops/transform-streams.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 | const verbose = require('debug')('streams:transform-stream:verbose');
4 |
5 | const { promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, uponPromise,
6 | transformPromiseWith } = require('../helpers/webidl.js');
7 | const { CreateReadableStream, ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue,
8 | ReadableStreamDefaultControllerError, ReadableStreamDefaultControllerHasBackpressure,
9 | ReadableStreamDefaultControllerCanCloseOrEnqueue } = require('./readable-streams.js');
10 | const { CreateWritableStream, WritableStreamDefaultControllerErrorIfNeeded } = require('./writable-streams.js');
11 |
12 | const TransformStream = require('../../generated/TransformStream.js');
13 | const TransformStreamDefaultController = require('../../generated/TransformStreamDefaultController.js');
14 |
15 | Object.assign(exports, {
16 | InitializeTransformStream,
17 | SetUpTransformStreamDefaultControllerFromTransformer,
18 | TransformStreamDefaultControllerEnqueue,
19 | TransformStreamDefaultControllerError,
20 | TransformStreamDefaultControllerTerminate
21 | });
22 |
23 | // Working with transform streams
24 |
25 | // CreateTransformStream is not implemented since it is only meant for external specs.
26 |
27 | function InitializeTransformStream(
28 | stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm) {
29 | function startAlgorithm() {
30 | return startPromise;
31 | }
32 |
33 | function writeAlgorithm(chunk) {
34 | return TransformStreamDefaultSinkWriteAlgorithm(stream, chunk);
35 | }
36 |
37 | function abortAlgorithm(reason) {
38 | return TransformStreamDefaultSinkAbortAlgorithm(stream, reason);
39 | }
40 |
41 | function closeAlgorithm() {
42 | return TransformStreamDefaultSinkCloseAlgorithm(stream);
43 | }
44 |
45 | stream._writable = CreateWritableStream(
46 | startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, writableHighWaterMark, writableSizeAlgorithm
47 | );
48 |
49 | function pullAlgorithm() {
50 | return TransformStreamDefaultSourcePullAlgorithm(stream);
51 | }
52 |
53 | function cancelAlgorithm(reason) {
54 | return TransformStreamDefaultSourceCancelAlgorithm(stream, reason);
55 | }
56 |
57 | stream._readable = CreateReadableStream(
58 | startAlgorithm, pullAlgorithm, cancelAlgorithm, readableHighWaterMark, readableSizeAlgorithm
59 | );
60 |
61 | // The [[backpressure]] slot is set to undefined so that it can be initialised by TransformStreamSetBackpressure.
62 | stream._backpressure = undefined;
63 | stream._backpressureChangePromise = undefined;
64 | TransformStreamSetBackpressure(stream, true);
65 |
66 | stream._controller = undefined;
67 | }
68 |
69 | function TransformStreamError(stream, e) {
70 | verbose('TransformStreamError()');
71 |
72 | ReadableStreamDefaultControllerError(stream._readable._controller, e);
73 | TransformStreamErrorWritableAndUnblockWrite(stream, e);
74 | }
75 |
76 | function TransformStreamErrorWritableAndUnblockWrite(stream, e) {
77 | TransformStreamDefaultControllerClearAlgorithms(stream._controller);
78 | WritableStreamDefaultControllerErrorIfNeeded(stream._writable._controller, e);
79 | TransformStreamUnblockWrite(stream);
80 | }
81 |
82 | function TransformStreamUnblockWrite(stream) {
83 | if (stream._backpressure === true) {
84 | // Pretend that pull() was called to permit any pending write() calls to complete. TransformStreamSetBackpressure()
85 | // cannot be called from enqueue() or pull() once the ReadableStream is errored, so this will will be the final time
86 | // _backpressure is set.
87 | TransformStreamSetBackpressure(stream, false);
88 | }
89 | }
90 |
91 | function TransformStreamSetBackpressure(stream, backpressure) {
92 | verbose(`TransformStreamSetBackpressure() [backpressure = ${backpressure}]`);
93 |
94 | // Passes also when called during construction.
95 | assert(stream._backpressure !== backpressure);
96 |
97 | if (stream._backpressureChangePromise !== undefined) {
98 | resolvePromise(stream._backpressureChangePromise, undefined);
99 | }
100 |
101 | stream._backpressureChangePromise = newPromise();
102 |
103 | stream._backpressure = backpressure;
104 | }
105 |
106 | // Default controllers
107 |
108 | function SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm,
109 | cancelAlgorithm) {
110 | assert(TransformStream.isImpl(stream));
111 | assert(stream._controller === undefined);
112 |
113 | controller._stream = stream;
114 | stream._controller = controller;
115 |
116 | controller._transformAlgorithm = transformAlgorithm;
117 | controller._flushAlgorithm = flushAlgorithm;
118 | controller._cancelAlgorithm = cancelAlgorithm;
119 | }
120 |
121 | function SetUpTransformStreamDefaultControllerFromTransformer(stream, transformer, transformerDict) {
122 | const controller = TransformStreamDefaultController.new(globalThis);
123 |
124 | let transformAlgorithm = chunk => {
125 | try {
126 | TransformStreamDefaultControllerEnqueue(controller, chunk);
127 | return promiseResolvedWith(undefined);
128 | } catch (transformResultE) {
129 | return promiseRejectedWith(transformResultE);
130 | }
131 | };
132 |
133 | let flushAlgorithm = () => promiseResolvedWith(undefined);
134 | let cancelAlgorithm = () => promiseResolvedWith(undefined);
135 |
136 | if ('transform' in transformerDict) {
137 | transformAlgorithm = chunk => transformerDict.transform.call(transformer, chunk, controller);
138 | }
139 | if ('flush' in transformerDict) {
140 | flushAlgorithm = () => transformerDict.flush.call(transformer, controller);
141 | }
142 | if ('cancel' in transformerDict) {
143 | cancelAlgorithm = reason => transformerDict.cancel.call(transformer, reason);
144 | }
145 |
146 | SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm, cancelAlgorithm);
147 | }
148 |
149 | function TransformStreamDefaultControllerClearAlgorithms(controller) {
150 | controller._transformAlgorithm = undefined;
151 | controller._flushAlgorithm = undefined;
152 | controller._cancelAlgorithm = undefined;
153 | }
154 |
155 | function TransformStreamDefaultControllerEnqueue(controller, chunk) {
156 | verbose('TransformStreamDefaultControllerEnqueue()');
157 |
158 | const stream = controller._stream;
159 | const readableController = stream._readable._controller;
160 | if (ReadableStreamDefaultControllerCanCloseOrEnqueue(readableController) === false) {
161 | throw new TypeError('Readable side is not in a state that permits enqueue');
162 | }
163 |
164 | // We throttle transform invocations based on the backpressure of the ReadableStream, but we still
165 | // accept TransformStreamDefaultControllerEnqueue() calls.
166 |
167 | try {
168 | ReadableStreamDefaultControllerEnqueue(readableController, chunk);
169 | } catch (e) {
170 | // This happens when readableStrategy.size() throws.
171 | TransformStreamErrorWritableAndUnblockWrite(stream, e);
172 |
173 | throw stream._readable._storedError;
174 | }
175 |
176 | const backpressure = ReadableStreamDefaultControllerHasBackpressure(readableController);
177 | if (backpressure !== stream._backpressure) {
178 | assert(backpressure === true);
179 | TransformStreamSetBackpressure(stream, true);
180 | }
181 | }
182 |
183 | function TransformStreamDefaultControllerError(controller, e) {
184 | TransformStreamError(controller._stream, e);
185 | }
186 |
187 | function TransformStreamDefaultControllerPerformTransform(controller, chunk) {
188 | const transformPromise = controller._transformAlgorithm(chunk);
189 | return transformPromiseWith(transformPromise, undefined, r => {
190 | TransformStreamError(controller._stream, r);
191 | throw r;
192 | });
193 | }
194 |
195 | function TransformStreamDefaultControllerTerminate(controller) {
196 | verbose('TransformStreamDefaultControllerTerminate()');
197 |
198 | const stream = controller._stream;
199 | const readableController = stream._readable._controller;
200 |
201 | ReadableStreamDefaultControllerClose(readableController);
202 |
203 | const error = new TypeError('TransformStream terminated');
204 | TransformStreamErrorWritableAndUnblockWrite(stream, error);
205 | }
206 |
207 | // Default sinks
208 |
209 | function TransformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
210 | verbose('TransformStreamDefaultSinkWriteAlgorithm()');
211 |
212 | assert(stream._writable._state === 'writable');
213 |
214 | const controller = stream._controller;
215 |
216 | if (stream._backpressure === true) {
217 | const backpressureChangePromise = stream._backpressureChangePromise;
218 | assert(backpressureChangePromise !== undefined);
219 | return transformPromiseWith(backpressureChangePromise, () => {
220 | const writable = stream._writable;
221 | const state = writable._state;
222 | if (state === 'erroring') {
223 | throw writable._storedError;
224 | }
225 | assert(state === 'writable');
226 | return TransformStreamDefaultControllerPerformTransform(controller, chunk);
227 | });
228 | }
229 |
230 | return TransformStreamDefaultControllerPerformTransform(controller, chunk);
231 | }
232 |
233 | function TransformStreamDefaultSinkAbortAlgorithm(stream, reason) {
234 | verbose('TransformStreamDefaultSinkAbortAlgorithm()');
235 |
236 | const controller = stream._controller;
237 | if (controller._finishPromise !== undefined) {
238 | return controller._finishPromise;
239 | }
240 |
241 | // stream._readable cannot change after construction, so caching it across a call to user code is safe.
242 | const readable = stream._readable;
243 |
244 | // Assign the _finishPromise now so that if _cancelAlgorithm calls readable.cancel() internally,
245 | // we don't run the _cancelAlgorithm again.
246 | controller._finishPromise = newPromise();
247 |
248 | const cancelPromise = controller._cancelAlgorithm(reason);
249 | TransformStreamDefaultControllerClearAlgorithms(controller);
250 |
251 | uponPromise(cancelPromise, () => {
252 | if (readable._state === 'errored') {
253 | rejectPromise(controller._finishPromise, readable._storedError);
254 | } else {
255 | ReadableStreamDefaultControllerError(readable._controller, reason);
256 | resolvePromise(controller._finishPromise);
257 | }
258 | }, r => {
259 | ReadableStreamDefaultControllerError(readable._controller, r);
260 | rejectPromise(controller._finishPromise, r);
261 | });
262 |
263 | return controller._finishPromise;
264 | }
265 |
266 | function TransformStreamDefaultSinkCloseAlgorithm(stream) {
267 | verbose('TransformStreamDefaultSinkCloseAlgorithm()');
268 |
269 | const controller = stream._controller;
270 | if (controller._finishPromise !== undefined) {
271 | return controller._finishPromise;
272 | }
273 |
274 | // stream._readable cannot change after construction, so caching it across a call to user code is safe.
275 | const readable = stream._readable;
276 |
277 | // Assign the _finishPromise now so that if _flushAlgorithm calls readable.cancel() internally,
278 | // we don't also run the _cancelAlgorithm.
279 | controller._finishPromise = newPromise();
280 |
281 | const flushPromise = controller._flushAlgorithm();
282 | TransformStreamDefaultControllerClearAlgorithms(controller);
283 |
284 | uponPromise(flushPromise, () => {
285 | if (readable._state === 'errored') {
286 | rejectPromise(controller._finishPromise, readable._storedError);
287 | } else {
288 | ReadableStreamDefaultControllerClose(readable._controller);
289 | resolvePromise(controller._finishPromise);
290 | }
291 | }, r => {
292 | ReadableStreamDefaultControllerError(readable._controller, r);
293 | rejectPromise(controller._finishPromise, r);
294 | });
295 |
296 | return controller._finishPromise;
297 | }
298 |
299 | // Default sources
300 |
301 | function TransformStreamDefaultSourcePullAlgorithm(stream) {
302 | verbose('TransformStreamDefaultSourcePullAlgorithm()');
303 |
304 | // Invariant. Enforced by the promises returned by start() and pull().
305 | assert(stream._backpressure === true);
306 |
307 | assert(stream._backpressureChangePromise !== undefined);
308 |
309 | TransformStreamSetBackpressure(stream, false);
310 |
311 | // Prevent the next pull() call until there is backpressure.
312 | return stream._backpressureChangePromise;
313 | }
314 |
315 | function TransformStreamDefaultSourceCancelAlgorithm(stream, reason) {
316 | verbose('TransformStreamDefaultSourceCancelAlgorithm()');
317 |
318 | const controller = stream._controller;
319 | if (controller._finishPromise !== undefined) {
320 | return controller._finishPromise;
321 | }
322 |
323 | // stream._writable cannot change after construction, so caching it across a call to user code is safe.
324 | const writable = stream._writable;
325 |
326 | // Assign the _finishPromise now so that if _flushAlgorithm calls writable.abort() or
327 | // writable.cancel() internally, we don't run the _cancelAlgorithm again, or also run the
328 | // _flushAlgorithm.
329 | controller._finishPromise = newPromise();
330 |
331 | const cancelPromise = controller._cancelAlgorithm(reason);
332 | TransformStreamDefaultControllerClearAlgorithms(controller);
333 |
334 | uponPromise(cancelPromise, () => {
335 | if (writable._state === 'errored') {
336 | rejectPromise(controller._finishPromise, writable._storedError);
337 | } else {
338 | WritableStreamDefaultControllerErrorIfNeeded(writable._controller, reason);
339 | TransformStreamUnblockWrite(stream);
340 | resolvePromise(controller._finishPromise);
341 | }
342 | }, r => {
343 | WritableStreamDefaultControllerErrorIfNeeded(writable._controller, r);
344 | TransformStreamUnblockWrite(stream);
345 | rejectPromise(controller._finishPromise, r);
346 | });
347 |
348 | return controller._finishPromise;
349 | }
350 |
--------------------------------------------------------------------------------
/reference-implementation/lib/helpers/miscellaneous.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 |
4 | exports.rethrowAssertionErrorRejection = e => {
5 | // Used throughout the reference implementation, as `.catch(rethrowAssertionErrorRejection)`, to ensure any errors
6 | // get shown. There are places in the spec where we do promise transformations and purposefully ignore or don't
7 | // expect any errors, but assertion errors are always problematic.
8 | if (e && e instanceof assert.AssertionError) {
9 | setTimeout(() => {
10 | throw e;
11 | }, 0);
12 | }
13 | };
14 |
15 | exports.mixin = (target, source) => {
16 | const keys = Reflect.ownKeys(source);
17 | for (let i = 0; i < keys.length; ++i) {
18 | if (keys[i] in target) {
19 | continue;
20 | }
21 |
22 | Object.defineProperty(target, keys[i], Object.getOwnPropertyDescriptor(source, keys[i]));
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/reference-implementation/lib/helpers/webidl.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const assert = require('assert');
3 | const { rethrowAssertionErrorRejection } = require('./miscellaneous.js');
4 |
5 | const originalPromise = Promise;
6 | const originalPromiseThen = Promise.prototype.then;
7 | const originalPromiseReject = Promise.reject;
8 |
9 | const promiseSideTable = new WeakMap();
10 |
11 | // https://webidl.spec.whatwg.org/#a-new-promise
12 | function newPromise() {
13 | // The stateIsPending tracking only works if we never resolve the promises with other promises.
14 | // In this spec, that happens to be true for the promises in question; they are always resolved with undefined.
15 | const sideData = { stateIsPending: true };
16 | const promise = new originalPromise((resolve, reject) => {
17 | sideData.resolve = resolve;
18 | sideData.reject = reject;
19 | });
20 |
21 | promiseSideTable.set(promise, sideData);
22 | return promise;
23 | }
24 |
25 | // https://webidl.spec.whatwg.org/#resolve
26 | function resolvePromise(p, value) {
27 | // We intend to only resolve or reject promises that are still pending.
28 | // When this is not the case, it usually means there's a bug in the specification that we want to fix.
29 | // This assertion is NOT a normative requirement. It is part of the reference implementation only
30 | // to help detect bugs in the specification. Other implementors MUST NOT replicate this assertion.
31 | assert(stateIsPending(p) === true);
32 | promiseSideTable.get(p).resolve(value);
33 | promiseSideTable.get(p).stateIsPending = false;
34 | }
35 |
36 | // https://webidl.spec.whatwg.org/#reject
37 | function rejectPromise(p, reason) {
38 | assert(stateIsPending(p) === true);
39 | promiseSideTable.get(p).reject(reason);
40 | promiseSideTable.get(p).stateIsPending = false;
41 | }
42 |
43 | // https://webidl.spec.whatwg.org/#a-promise-resolved-with
44 | function promiseResolvedWith(value) {
45 | // Cannot use original Promise.resolve since that will return value itself sometimes, unlike Web IDL.
46 | const promise = new originalPromise(resolve => resolve(value));
47 | promiseSideTable.set(promise, { stateIsPending: false });
48 | return promise;
49 | }
50 |
51 | // https://webidl.spec.whatwg.org/#a-promise-rejected-with
52 | function promiseRejectedWith(reason) {
53 | const promise = originalPromiseReject.call(originalPromise, reason);
54 | promiseSideTable.set(promise, { stateIsPending: false });
55 | return promise;
56 | }
57 |
58 | function PerformPromiseThen(promise, onFulfilled, onRejected) {
59 | // There doesn't appear to be any way to correctly emulate the behaviour from JavaScript, so this is just an
60 | // approximation.
61 | return originalPromiseThen.call(promise, onFulfilled, onRejected);
62 | }
63 |
64 | // https://webidl.spec.whatwg.org/#dfn-perform-steps-once-promise-is-settled
65 | function uponPromise(promise, onFulfilled, onRejected) {
66 | PerformPromiseThen(
67 | PerformPromiseThen(promise, onFulfilled, onRejected),
68 | undefined,
69 | rethrowAssertionErrorRejection
70 | );
71 | }
72 |
73 | function uponFulfillment(promise, onFulfilled) {
74 | uponPromise(promise, onFulfilled);
75 | }
76 |
77 | function uponRejection(promise, onRejected) {
78 | uponPromise(promise, undefined, onRejected);
79 | }
80 |
81 | function transformPromiseWith(promise, fulfillmentHandler, rejectionHandler) {
82 | return PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler);
83 | }
84 |
85 | function setPromiseIsHandledToTrue(promise) {
86 | PerformPromiseThen(promise, undefined, rethrowAssertionErrorRejection);
87 | }
88 |
89 | function stateIsPending(promise) {
90 | return promiseSideTable.get(promise).stateIsPending;
91 | }
92 |
93 | Object.assign(exports, {
94 | newPromise,
95 | resolvePromise,
96 | rejectPromise,
97 | promiseResolvedWith,
98 | promiseRejectedWith,
99 | uponPromise,
100 | uponFulfillment,
101 | uponRejection,
102 | transformPromiseWith,
103 | setPromiseIsHandledToTrue,
104 | stateIsPending
105 | });
106 |
107 | // https://webidl.spec.whatwg.org/#wait-for-all
108 | function waitForAll(promises, successSteps, failureSteps) {
109 | let fulfilledCount = 0;
110 | let rejected = false;
111 | const rejectionHandler = arg => {
112 | if (rejected === false) {
113 | rejected = true;
114 | failureSteps(arg);
115 | }
116 | };
117 | let index = 0;
118 | const total = promises.length;
119 | const result = new Array(total);
120 | if (total === 0) {
121 | queueMicrotask(() => successSteps(result));
122 | return;
123 | }
124 | for (const promise of promises) {
125 | const promiseIndex = index;
126 | const fulfillmentHandler = arg => {
127 | result[promiseIndex] = arg;
128 | ++fulfilledCount;
129 | if (fulfilledCount === total) {
130 | successSteps(result);
131 | }
132 | };
133 | PerformPromiseThen(promise, fulfillmentHandler, rejectionHandler);
134 | ++index;
135 | }
136 | }
137 |
138 | // https://webidl.spec.whatwg.org/#waiting-for-all-promise
139 | exports.waitForAllPromise = promises => {
140 | const promise = newPromise();
141 | const successSteps = results => {
142 | resolvePromise(promise, results);
143 | };
144 | const failureSteps = reason => {
145 | rejectPromise(promise, reason);
146 | };
147 | waitForAll(promises, successSteps, failureSteps);
148 | return promise;
149 | };
150 |
--------------------------------------------------------------------------------
/reference-implementation/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | // This file is used as the entry point for browserifying the reference implementation to allow it
3 | // to run inside the wpt-runner "browser like" context.
4 |
5 | window.gc = gc;
6 |
7 | require('../generated/ByteLengthQueuingStrategy.js').install(window, ['Window']);
8 | require('../generated/CountQueuingStrategy.js').install(window, ['Window']);
9 |
10 | require('../generated/ReadableStream.js').install(window, ['Window']);
11 | require('../generated/ReadableStreamDefaultReader.js').install(window, ['Window']);
12 | require('../generated/ReadableStreamBYOBReader.js').install(window, ['Window']);
13 | require('../generated/ReadableStreamDefaultController.js').install(window, ['Window']);
14 | require('../generated/ReadableByteStreamController.js').install(window, ['Window']);
15 | require('../generated/ReadableStreamBYOBRequest.js').install(window, ['Window']);
16 |
17 | require('../generated/WritableStream.js').install(window, ['Window']);
18 | require('../generated/WritableStreamDefaultWriter.js').install(window, ['Window']);
19 | require('../generated/WritableStreamDefaultController.js').install(window, ['Window']);
20 |
21 | require('../generated/TransformStream.js').install(window, ['Window']);
22 | require('../generated/TransformStreamDefaultController.js').install(window, ['Window']);
23 |
--------------------------------------------------------------------------------
/reference-implementation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "description": "Reference implementation and tests for the WHATWG Streams Standard",
4 | "scripts": {
5 | "pretest": "node compile-idl.js && browserify lib/index.js -d -o bundle.js",
6 | "test": "npm run lint && npm run wpt",
7 | "wpt": "npm run pretest && node --expose_gc run-web-platform-tests.js",
8 | "sync-wpt": "git submodule update --init",
9 | "lint": "eslint .",
10 | "coverage": "c8 --reporter=lcov -- npm run wpt && opener coverage/lcov-report/index.html"
11 | },
12 | "author": "Domenic Denicola (https://domenic.me/)",
13 | "license": "(CC0-1.0 OR MIT)",
14 | "devDependencies": {
15 | "browserify": "^17.0.1",
16 | "c8": "^10.1.2",
17 | "debug": "^4.1.1",
18 | "eslint": "^6.8.0",
19 | "minimatch": "^10.0.1",
20 | "opener": "^1.5.2",
21 | "webidl2js": "^18.0.0",
22 | "wpt-runner": "^6.0.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/reference-implementation/run-web-platform-tests.js:
--------------------------------------------------------------------------------
1 | // This runs the web platform tests against the reference implementation, in Node.js using jsdom, for easier rapid
2 | // development of the reference implementation and the web platform tests.
3 | /* eslint-disable no-console */
4 | 'use strict';
5 | const path = require('path');
6 | const fs = require('fs');
7 | const { promisify } = require('util');
8 | const wptRunner = require('wpt-runner');
9 | const { minimatch } = require('minimatch');
10 | const readFileAsync = promisify(fs.readFile);
11 |
12 | // wpt-runner does not yet support unhandled rejection tracking a la
13 | // https://github.com/w3c/testharness.js/commit/7716e2581a86dfd9405a9c00547a7504f0c7fe94
14 | // So we emulate it with Node.js events
15 | const rejections = new Map();
16 | process.on('unhandledRejection', (reason, promise) => {
17 | rejections.set(promise, reason);
18 | });
19 |
20 | process.on('rejectionHandled', promise => {
21 | rejections.delete(promise);
22 | });
23 |
24 | main().catch(e => {
25 | console.error(e.stack);
26 | process.exitCode = 1;
27 | });
28 |
29 | async function main() {
30 | const bundlePath = path.resolve(__dirname, 'bundle.js');
31 | const wptPath = path.resolve(__dirname, 'web-platform-tests');
32 | const testsPath = path.resolve(wptPath, 'streams');
33 |
34 | const filterGlobs = process.argv.length >= 3 ? process.argv.slice(2) : ['**/*.html'];
35 | const excludeGlobs = [
36 | 'piping/general-addition.any.html', // FIXME: reenable this test as part of https://github.com/whatwg/streams/issues/1243.
37 | // These tests use ArrayBuffers backed by WebAssembly.Memory objects, which *should* be non-transferable.
38 | // However, our TransferArrayBuffer implementation cannot detect these, and will incorrectly "transfer" them anyway.
39 | 'readable-byte-streams/non-transferable-buffers.any.html',
40 | 'readable-streams/owning-type-message-port.any.html', // disabled due to MessagePort use.
41 | 'readable-streams/owning-type-video-frame.any.html', // disabled due to VideoFrame use.
42 | 'readable-streams/owning-type.any.html', // FIXME: reenable this test once owning type PR lands.
43 | 'transferable/transform-stream-members.any.html' // FIXME: reenable if structuredClone is aligned.
44 | ];
45 | const anyTestPattern = /\.any\.html$/;
46 |
47 | let bundledJS = await readFileAsync(bundlePath, { encoding: 'utf8' });
48 | bundledJS = `${bundledJS}\n//# sourceURL=file://${bundlePath}`;
49 |
50 | const failures = await wptRunner(testsPath, {
51 | rootURL: 'streams/',
52 | setup(window) {
53 | window.queueMicrotask = queueMicrotask;
54 | window.structuredClone = globalThis.structuredClone;
55 | window.fetch = async function (url) {
56 | const filePath = path.join(wptPath, url);
57 | if (!filePath.startsWith(wptPath)) {
58 | throw new TypeError('Invalid URL');
59 | }
60 | return {
61 | ok: true,
62 | async text() {
63 | return await readFileAsync(filePath, { encoding: 'utf8' });
64 | }
65 | };
66 | };
67 | window.eval(bundledJS);
68 | },
69 | filter(testPath) {
70 | // Ignore the window-specific and worker-specific tests
71 | if (!anyTestPattern.test(testPath)) {
72 | return false;
73 | }
74 |
75 | return filterGlobs.some(glob => minimatch(testPath, glob)) &&
76 | !excludeGlobs.some(glob => minimatch(testPath, glob));
77 | }
78 | });
79 |
80 | process.exitCode = failures;
81 |
82 | if (rejections.size > 0) {
83 | if (failures === 0) {
84 | process.exitCode = 1;
85 | }
86 |
87 | for (const reason of rejections.values()) {
88 | console.error('Unhandled promise rejection: ', reason.stack);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/streams-for-raw-video-explainer.md:
--------------------------------------------------------------------------------
1 | # Transferring Ownership Streams Explained
2 |
3 |
4 | ## Introduction
5 |
6 | The streams APIs provide a convenient way to build processing pipelines.
7 | Using streams with `VideoFrame` objects has known shortcomings, as illustrated by https://github.com/whatwg/streams/issues/1155 or https://github.com/whatwg/streams/issues/1185 for instance.
8 | This proposal addresses these shortcomings by improving streams support for chunks similar but not limited to WebCodec `VideoFrame`.
9 | It could also be useful for streams making use of `ArrayBuffer` or chunks that own `ArrayBuffer` like `RTCEncodedVideoChunk`.
10 |
11 | Streams APIs can create coupling between the processing units of the pipeline when chunks in the pipe are mutable:
12 | a processing unit A might pass a chunk O to another unit B through a stream (say a `WritableStream`) W.
13 | If A keeps a reference to O, it might mutate O while B is doing processing based on O.
14 |
15 | This is especially an issue with chunks like `VideoFrame` objects that need to be closed explicitly.
16 | Taking the previous example, if A decides to close a `VideoFrame` after passing it to W but before B gets it, B will receive a closed `VideoFrame`, which is probably considered a bug.
17 | If A does not close `VideoFrame`, it is the responsibility of the remaining of the pipeline to close it.
18 | There is a need to clearly identify who is owning these chunks and who is responsible to close these chunks at any point in time.
19 |
20 | The proposed solution is to transfer ownership of a chunk to the stream when it gets written or enqueueud to the stream.
21 | By doing so, the processing unit that enqueues/writes chunks will not be able to mutate chunks manipulated by the stream and is relieved of the lifetime management of these chunks.
22 | Conversely, processing units take ownership of chunks when they receive them from a stream.
23 |
24 | Transferring ownership should be opt-in. For that purpose, a new streams type, named 'owning' in this document, would be added.
25 |
26 | ## Example
27 |
28 | Below is an example of JavaScript that shows how this can be used.
29 | The example creates a processing pipe starting with a VideoFrame stream and applying two transforms, one for doing a processing like logging every 30 frame, and one for doing background blur.
30 |
31 | ```worker.js javascript
32 | function doBackgroundBlurOnVideoFrames(videoFrameStream, doLogging)
33 | {
34 | // JavaScript custom transform.
35 | let frameCount = 0;
36 | const frameCountTransform = new TransformStream({
37 | transform: async (videoFrame, controller) => {
38 | try {
39 | // videoFrame is under the responsibility of the script and must be closed when no longer needed.
40 | controller.enqueue(videoFrame, { transfer: [videoFrame] });
41 | // controller.enqueue was called, videoFrame is transferred.
42 | if (!(++frameCount % 30) && doLogging)
43 | doLogging(frameCount);
44 | } catch (e) {
45 | // In case of exception, let's make sure videoFrame is closed. This is a no-op if videoFrame was previously transferred.
46 | videoFrame.close();
47 | // If exception is unrecoverable, let's error the pipe.
48 | controller.error(e);
49 | }
50 | },
51 | readableType: 'owning',
52 | writableType: 'owning'
53 | });
54 | // Native transform is of type 'owning'
55 | const backgroundBlurTransform = new BackgroundBlurTransform();
56 |
57 | return videoFrameStream.pipeThrough(backgroundBlurTransform)
58 | .pipeThrough(frameCountTransform);
59 | }
60 | ```
61 |
62 | ## Goals
63 |
64 | * Permit `ReadableStream`, `WritableStream` and `TransformStream` objects to take ownership of chunks they manipulate.
65 | * Permit to build a safe and optimal video pipeline using `ReadableStream`, `WritableStream` and `TransformStream` objects that manipulate `VideoFrame` objects.
66 | * Permit both native and JavaScript-based streams of type 'owning'.
67 | * Permit to optimize streams pipelines of transferable chunks like `ArrayBuffer`, `RTCEncodedVideoFrame` or `RTCEncodedAudioFrame`.
68 | * Permit to tee a `ReadableStream` of `VideoFrame` objects without tight coupling between the teed branches.
69 |
70 | ## Non-goals
71 |
72 | * Add support for transferring and closing of arbitrary JavaScript chunks.
73 |
74 | ## Use cases
75 |
76 | * Performing realtime transformations of `VideoFrame` objects, for instance taking a camera `MediaStreamTrack` and applying
77 | a background blur effect as a `TransformStream` on each `VideoFrame` of the `MediaStreamTrack`.
78 |
79 | ## End-user benefit
80 |
81 | * `VideoFrame` needs specific management and be closed as quickly as possible, without relying on garbage collection.
82 | This is important to not create hangs/stutters in the processing pipeline. By building support for safe patterns
83 | directly in streams, this will allow web developers to optimize `VideoFrame` management, and allow user experience
84 | to be more consistent accross devices.
85 |
86 | ## Principles
87 |
88 | The envisioned changes to the streams specification could look like the following:
89 | * Add a new 'owning' value that can be passed to `ReadableStream` type, `WritableStream` type and `TransformStream` readableType/writableType.
90 | For streams that do not use the 'owning' type, nothing changes.
91 | * Streams of the 'transfer' type can only manipulate chunks that are marked both as Transferable and Serializable.
92 | * If a chunk that is either not Transferable or not Serializable is enqueued or written, the chunk is ignored as if it was never enqueued/written.
93 | * If a Transferable and Serializable chunk is enqueueud/written in a 'owning' type `ReadableStreamDefaultController`, `TransformStreamDefaultController`
94 | or `WritableStreamDefaultWriter`, create a transferred version of the chunk using StructuredSerializeWithTransfer/StructuredDeserializeWithTransfer.
95 | Proceed with the regular stream algorithm by using the transferred chunk instead of the chunk itself.
96 | * Introduce a WhatWG streams 'close-able' concept. A chunk that is 'close-able' defines closing steps.
97 | For instance `VideoFrame` closing steps could be defined using https://www.w3.org/TR/webcodecs/#close-videoframe.
98 | `ArrayBuffer` closing steps could be defined using https://tc39.es/ecma262/#sec-detacharraybuffer.
99 | The 'close-able' steps should be a no-op on a transferred chunk.
100 | * Execute the closing steps of a 'close-able' chunk for streams with the 'owning' type when resetting the queue of `ReadableStreamDefaultController`
101 | or emptying `WritableStream`.[[writeRequests]] in case of abort/error.
102 | * When calling tee() on a `ReadableStream` of the 'owning' type, call ReadableStream with cloneForBranch2 equal to true.
103 | * To solve https://github.com/whatwg/streams/issues/1186, tee() on a `ReadableStream` of the 'owning' type can take a 'realtime' parameter.
104 | When the 'realtime' parameter is used, chunks will be dropped on the branch that consumes more slowly to keep buffering limited to one chunk.
105 | The closing steps should be called for any chunk that gets dropped in that situation.
106 |
107 | ## Alternatives
108 |
109 | * It is difficult to emulate neutering/closing of chunks especially in case of teeing or aborting a stream.
110 | * As discussed in https://github.com/whatwg/streams/issues/1155, lifetime management of chunks could potentially be done at the source level.
111 | But this is difficult to make it work without introducing tight coupling between producers and consumers.
112 | * The main alternative would be to design a VideoFrame specific API outside of WhatWG streams, which is feasible, as examplified by WebCodecs API.
113 |
114 | ## Future Work
115 |
116 | * Evaluate what to do when enqueuing/writing a chunk that is not Transferable or not Serializable. We might want to reject the related promise without erroring the stream.
117 | * Evaluate the usefulness of supporting Serializable but not Transferable chunks, we might just need to create a copy through serialization steps then explicitly call closeable steps on the chunk.
118 | * Evaluate the usefulness of supporting Transferable but not Serializable chunks, in particular in how to handle `ReadableStream` tee().
119 | If `ReadableStream` tee() exposes a parameter to enable structured cloning, it might sometimes fail with such chunks and we could piggy back on this behavior.
120 | * Evaluate the usefulness of adding a `TransformStream` type to set readableType and writableType to the same value.
121 | * Envision extending this support for arbitrary JavaScript chunks, for both transferring and explicit closing.
122 | * Envision to introduce close-able concept in WebIDL.
123 | * We might want to mention that, if we use detach steps for `ArrayBuffer`, implementations can directly deallocate the corresponding memory.
124 |
--------------------------------------------------------------------------------
/transferable-streams-explainer.md:
--------------------------------------------------------------------------------
1 | # Transferable Streams Explained
2 |
3 |
4 | ## Introduction
5 |
6 | The streams APIs provide ubiquitous, interoperable primitives for creating, composing, and consuming streams of data. A
7 | natural thing to want to do with a stream is pass it off to a web worker. This provides a building block that is easy
8 | and natural to use for offloading work onto another thread. Once you can speak the language of streams, you can use that
9 | same language to make use of a Worker.
10 |
11 | This work will permit streams to be transferred between workers, frames and anywhere else that `postMessage()` can be
12 | used. Chunks can be anything which is cloneable by `postMessage()`.
13 |
14 | Initially chunks enqueued in such a stream will always be cloned, ie. all data will be copied. Future work will extend
15 | the Streams APIs to support transferring objects (ie. zero copy).
16 |
17 | This is an example of JavaScript which will work once this is implemented:
18 |
19 | In the main page:
20 |
21 | ```javascript
22 | const rs = new ReadableStream({
23 | start(controller) {
24 | controller.enqueue('hello');
25 | }
26 | });
27 | const w = new Worker('worker.js');
28 | w.postMessage(rs, [rs]);
29 | ```
30 |
31 | In worker.js:
32 |
33 | ```javascript
34 | onmessage = async (evt) => {
35 | const rs = evt.data;
36 | const reader = rs.getReader();
37 | const {value, done} = await reader.read();
38 | console.log(value); // logs 'hello'.
39 | };
40 | ```
41 |
42 | Note that `w.postMessage(rs)` would not work. Streams can only be _transferred_, not _cloned_. Attempts to clone a
43 | stream will throw a DataCloneError.
44 |
45 | Once a stream has been transferred with `postMessage()` the original stream is locked and cannot be read or written.
46 | This is similar to how ArrayBuffers are neutered after they are transferred. However, the code of the underlying source
47 | or sink is still running in the original context. The benefits to user experience can be seen in [this demo of streaming
48 | digits of PI](https://glitch.com/edit/#!/streaming-pi?path=pi.js:1:0) (if you have a browser that supports transferable
49 | streams, you can see it live at https://streaming-pi.glitch.me/).
50 |
51 | Transferable streams are also useful in constructing responses for a service worker. See
52 | https://gist.github.com/domenic/ea5ebedffcee27f552e103963cf8585c/ for an example.
53 |
54 | ## Goals
55 |
56 | * Permit `ReadableStream`, `WritableStream` and `TransformStream` objects to be transferred between workers and other
57 | contexts where `postMessage()` can be used.
58 | * Provide a primitive that is amenable to future optimisations, for example automatic thread offload to bypass the
59 | main thread.
60 |
61 | ## Non-goals
62 |
63 | * Avoiding cloning the chunks is not a goal at this stage; see "future work".
64 | * As such, Transfer-only objects (such as `ImageBitmap`) will not be supported yet; only serializable objects and the
65 | built-in types supported by the structured serialization algorithm.
66 | * A concise syntax for creating a `TransformStream` and a worker to run it on in one operation is not part of this
67 | work.
68 |
69 | ## Use cases
70 |
71 | * Performing expensive transformations off the main thread. Transcoding, for example.
72 | * Synthesizing responses from a service worker. For example, generating a PDF from data in the DOM and streaming it to
73 | the service worker where it can then be downloaded as a file.
74 | * Processing a stream of data from an input device only accessible on the main thread. For example, you could use
75 | `MediaRecorder` to capture the audio of a user's microphone and/or the video of a user's camera, pipe the captured
76 | data through a off-thread `TransformStream` to transcode it into a hypothetical new experimental media format and
77 | then upload the resulting stream to a server.
78 | * Displaying a stream of data that is expensive to generate on a web page. Inverting the previous example: you
79 | download a stream of a video file encoded in an experimental media format, you transcode it to a natively supported
80 | format in a worker, and finally you transfer the resulting stream to the main thread to play back the video using
81 | `` and `MediaSource`.
82 |
83 | ## End-user benefit
84 |
85 | * By enabling developers to easily offload work onto other threads, this will increase the availability of responsive,
86 | stutter-free experiences to end users. For example, a page that transcoded video using a new, CPU-intensive codec
87 | could still respond snappily to user input by offloading the transcoding to another thread.
88 | * The power multiplier of streams and threads could unlock whole new applications.
89 |
90 | ## Alternatives
91 |
92 | * It's possible to emulate this behaviour by using `postMessage()` directly. See for example
93 | [remote-web-streams](https://github.com/MattiasBuelens/remote-web-streams). Existing techniques for moving work
94 | off the main thread often resemble a subset of this functionality. However, it is clumsy and hard to work with. When
95 | in future the API is optimized, transferring a browser created stream (e.g., a fetch response body from the network)
96 | will be more efficient than manually using `postMessage()` on each chunk due to reduced copies and JavaScript calls.
97 |
98 | ## Future Work
99 |
100 | * Transferring rather than cloning chunks is an optimisation we will obviously need in future. This is expected to
101 | require extensions to the Streams APIs.
102 |
--------------------------------------------------------------------------------
/writable-stream-abort-signal-explainer.md:
--------------------------------------------------------------------------------
1 | # `WritableStream` controller `AbortSignal` Explainer
2 |
3 |
4 | ## Introduction
5 |
6 | The streams APIs provide ubiquitous, interoperable primitives for creating, composing, and consuming streams of data.
7 |
8 | This change permits an underlying sink to rapidly abort an ongoing write or close when requested by the writer.
9 |
10 | Previously, when `writer.abort()` was called, a long-running write would still have to continue to completion before
11 | the stream could be aborted. With this change, the write can be aborted immediately.
12 |
13 | An underlying sink which doesn't observe the `controller.signal` will continue to have the existing behavior.
14 |
15 | In addition to being exposed to streams authored in JavaScript, this facility will also be used by platform-provided
16 | streams such as [WebTransport](https://w3c.github.io/webtransport/).
17 |
18 |
19 | ## API Proposed
20 |
21 | On [WritableStreamDefaultController](https://streams.spec.whatwg.org/#writablestreamdefaultcontroller)
22 | (the controller argument that is passed to underlying sinks):
23 |
24 | * [`signal`](https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-signal): An AbortSignal. By using
25 | `signal.addEventListener('abort', …)` an underlying sink can abort the pending write or close operation when the
26 | stream is aborted.
27 |
28 | The `WritableStream` API does not change. Instead, the existing `abort()` operation will now signal abort.
29 |
30 |
31 | ## Examples
32 |
33 | These are some examples of JavaScript which can be used for writable streams once this is implemented:
34 |
35 | In this example, the underlying sink write waits 1 second to simulate a long-running operation. However, if abort() is
36 | called it stops immediately.
37 |
38 |
39 | ```javascript
40 | const ws = new WritableStream({
41 | write(controller) {
42 | return new Promise((resolve, reject) => {
43 | setTimeout(resolve, 1000);
44 | controller.signal.addEventListener('abort',
45 | () => reject(controller.signal.reason));
46 | });
47 | }
48 | });
49 | const writer = ws.getWriter();
50 |
51 | writer.write(99);
52 | await writer.abort();
53 | ```
54 |
55 |
56 | This example shows integration with an existing API that uses `AbortSignal`. In this case, each `write()` triggers a
57 | POST to a remote endpoint using [the fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). The signal
58 | is used to abort the ongoing fetch when the stream is aborted.
59 |
60 |
61 | ```javascript
62 | const endpoint = 'https://endpoint/api';
63 | const ws = new WritableStream({
64 | async write(controller, chunk) {
65 | const response = await fetch(endpoint, { signal: controller.signal,
66 | method: 'POST',
67 | body: chunk });
68 | await response.text();
69 | }
70 | });
71 | const writer = ws.getWriter();
72 |
73 | writer.write('some data');
74 | await writer.abort();
75 | ```
76 |
77 | This example shows a use case of this feature with WebTransport.
78 |
79 | ```javascript
80 | const wt = new WebTransport(...);
81 | await wt.ready;
82 | const ws = await wt.createUnidirectionalStream();
83 | // `ws` is a WritableStream.
84 |
85 | const reallyBigArrayBuffer = …;
86 | writer.write(reallyBigArrayBuffer);
87 | // Send RESET_STREAM to the server without waiting for `reallyBigArrayBuffer` to
88 | // be transmitted.
89 | await writer.abort();
90 | ```
91 |
92 |
93 |
94 | ## Goals
95 |
96 | * Allow writes to be aborted more quickly and efficiently.
97 | * WebTransport will be able to use WritableStreamDefaultController.signal to make
98 | [`SendStream`'s `write()`](https://w3c.github.io/webtransport/#sendstream-write) and
99 | [`close()`](https://w3c.github.io/webtransport/#sendstream-close) abortable.
100 |
101 |
102 | ## Non-Goals
103 |
104 | * Exposing a method to abort individual operations without aborting the stream as a whole. The semantics of this
105 | would be unclear and confusing.
106 |
107 |
108 | ## User Benefits
109 |
110 | * Allows the abort operation to complete more quickly, which avoids wasted resources for sites that take advantage of it.
111 |
112 |
113 | ## Alternatives
114 |
115 | * It was initially proposed that an `AbortSignal` could be passed to each sink `write()` call. However, since the
116 | abort signal does not need to change between two `write()` calls, it was thought better to just add a `signal` property
117 | on `WritableStreamDefaultController`.
118 | * Previously, [WritableStreamDefaultController](https://streams.spec.whatwg.org/#writablestreamdefaultcontroller) had
119 | an `abortReason` property that was an argument given to
120 | [WritableStreamAbort](https://streams.spec.whatwg.org/#writable-stream-abort). However, after some discussion, it was thought better to just add the `reason` property to the `AbortSignal`.
121 |
--------------------------------------------------------------------------------