Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "c512e77c4d7211f153913d1bdcf14903a9a15024"
},
"path_in_vcs": ""
}
+72
View File
@@ -0,0 +1,72 @@
# Changelog
## v0.5.0 (2026-01-22)
* Added support for `panic=unwind`. ([#26](https://github.com/MattiasBuelens/wasm-streams/issues/26), [#28](https://github.com/MattiasBuelens/wasm-streams/pull/28))
* Persist the error state of `writable::IntoSink`. ([#27](https://github.com/MattiasBuelens/wasm-streams/pull/27))
* Fixed lifetime elision warnings. ([#29](https://github.com/MattiasBuelens/wasm-streams/pull/29))
* Updated to `wasm-bindgen` 0.2.108 and `web-sys` 0.3.85.
## v0.4.2 (2024-10-25)
* Updated to `wasm-bindgen` 0.2.95 and `web-sys` 0.3.72.
* Used more `web-sys` types directly for the crate's internals.
## v0.4.1 (2024-09-28)
* Fixed "closure invoked recursively or after being dropped" when dropping `IntoStream` and `IntoAsyncRead`. ([#24](https://github.com/MattiasBuelens/wasm-streams/issues/24), [#25](https://github.com/MattiasBuelens/wasm-streams/pull/25))
## v0.4.0 (2023-10-31)
* Added `ReadableStream::from(async_iterable)` and `try_from(async_iterable)`. ([#23](https://github.com/MattiasBuelens/wasm-streams/pull/23))
* Stop calling `byobRequest.respond(0)` on cancel ([#16](https://github.com/MattiasBuelens/wasm-streams/pull/16))
***Breaking change:** The system modules (`readable::sys`, `writable::sys` and `transform::sys`) now re-export directly from [the `web-sys` crate](https://docs.rs/web-sys/latest/web_sys/). This should make it easier to use `from_raw()`, `as_raw()` and `into_raw()`. ([#22](https://github.com/MattiasBuelens/wasm-streams/pull/22))
## v0.3.0 (2022-10-16)
* Added support for web workers, by removing usage of [JavaScript snippets](https://wasm-bindgen.github.io/wasm-bindgen/reference/js-snippets.html). ([#13](https://github.com/MattiasBuelens/wasm-streams/issues/13), [#14](https://github.com/MattiasBuelens/wasm-streams/pull/14))
***Breaking change:** This removes a workaround for [Chromium bug #1187774](https://crbug.com/1187774) that was previously needed for `ReadableStream::from_async_read`. This bug was fixed upstream in March 2021 with Chrome 91. ([#14](https://github.com/MattiasBuelens/wasm-streams/pull/14))
* Updated documentation of `ReadableStream(Default|BYOB)Reader::release_lock()` around the expected behavior when there are pending read requests.
See the corresponding [Streams specification change](https://github.com/whatwg/streams/commit/d5f92d9f17306d31ba6b27424d23d58e89bf64a5) for details.
([#15](https://github.com/MattiasBuelens/wasm-streams/pull/15))
## v0.2.3 (2022-05-18)
* Replaced `futures` dependency with `futures-util` to improve compilation times ([#11](https://github.com/MattiasBuelens/wasm-streams/pull/11), [#12](https://github.com/MattiasBuelens/wasm-streams/pull/12))
## v0.2.2 (2021-12-09)
* Added `WritableStream::into_async_write()` to turn a `WritableStream` accepting `Uint8Array`s
into an `AsyncWrite` ([#9](https://github.com/MattiasBuelens/wasm-streams/issues/9),
[#10](https://github.com/MattiasBuelens/wasm-streams/pull/10))
* Added `IntoSink::abort()` ([#10](https://github.com/MattiasBuelens/wasm-streams/pull/10))
## v0.2.1 (2021-09-23)
* `ReadableStream::into_stream()` and `ReadableStream::into_async_read()` now automatically
cancel the stream when dropped ([#7](https://github.com/MattiasBuelens/wasm-streams/issues/7), [#8](https://github.com/MattiasBuelens/wasm-streams/pull/8))
* Added `IntoStream::cancel()` and `IntoAsyncRead::cancel()` ([#8](https://github.com/MattiasBuelens/wasm-streams/pull/8))
## v0.2.0 (2021-06-22)
* Add support for readable byte streams ([#6](https://github.com/MattiasBuelens/wasm-streams/pull/6))
* Add `ReadableStream::(try_)get_byob_reader` to acquire
a [BYOB reader](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader).
* Add `ReadableStream::from_async_read` to turn
an [`AsyncRead`](https://docs.rs/futures/0.3.15/futures/io/trait.AsyncRead.html)
into a readable byte stream.
* Add `ReadableStream::(try_)into_async_read` to turn a readable byte stream into
an [`AsyncRead`](https://docs.rs/futures/0.3.15/futures/io/trait.AsyncRead.html).
* Improve error handling and drop behavior of `ReadableStream::from_stream()`
## v0.1.2 (2020-10-31)
* Include license files in repository ([#5](https://github.com/MattiasBuelens/wasm-streams/issues/5))
## v0.1.1 (2020-08-08)
* Specify TypeScript type for raw streams ([#1](https://github.com/MattiasBuelens/wasm-streams/pull/1))
## v0.1.0 (2020-06-15)
First release! 🎉
+16
View File
@@ -0,0 +1,16 @@
# Testing
The tests use [wasm-pack](https://drager.github.io/wasm-pack/).
See the [wasm-bindgen guide](https://wasm-bindgen.github.io/wasm-bindgen/wasm-bindgen-test/usage.html) for more information.
We run the tests in Node.js, Chrome and Firefox:
```
wasm-pack test --node
WASM_BINDGEN_USE_BROWSER=1 wasm-pack test --headless --chrome
WASM_BINDGEN_USE_BROWSER=1 wasm-pack test --headless --firefox
```
When debugging the browser tests, remove the `--headless` flag:
```
WASM_BINDGEN_USE_BROWSER=1 wasm-pack test --chrome
```
+517
View File
@@ -0,0 +1,517 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "find-msvc-tools"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "gloo-timers"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "itoa"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "js-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "minicov"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
dependencies = [
"cc",
"walkdir",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "pin-project"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"pin-project-lite",
"tokio-macros",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f"
dependencies = [
"cfg-if",
"futures-util",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
version = "0.3.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45649196a53b0b7a15101d845d44d2dda7374fc1b5b5e2bbf58b7577ff4b346d"
dependencies = [
"async-trait",
"cast",
"js-sys",
"libm",
"minicov",
"nu-ansi-term",
"num-traits",
"oorandom",
"serde",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test-macro",
"wasm-bindgen-test-shared",
]
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f579cdd0123ac74b94e1a4a72bd963cf30ebac343f2df347da0b8df24cdebed2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "wasm-bindgen-test-shared"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8145dd1593bf0fb137dbfa85b8be79ec560a447298955877804640e40c2d6ea"
[[package]]
name = "wasm-streams"
version = "0.5.0"
dependencies = [
"futures-util",
"gloo-timers",
"js-sys",
"pin-project",
"tokio",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test",
"web-sys",
]
[[package]]
name = "web-sys"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
+127
View File
@@ -0,0 +1,127 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "wasm-streams"
version = "0.5.0"
authors = ["Mattias Buelens <mattias@buelens.com>"]
build = false
exclude = [
".github/",
"tests/panic-unwind/",
]
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = """
Bridging between web streams and Rust streams using WebAssembly
"""
readme = "README.md"
license = "MIT OR Apache-2.0"
repository = "https://github.com/MattiasBuelens/wasm-streams/"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[lib]
name = "wasm_streams"
crate-type = [
"cdylib",
"rlib",
]
path = "src/lib.rs"
[[example]]
name = "fetch_as_stream"
path = "examples/fetch_as_stream.rs"
[[test]]
name = "unit"
path = "tests/unit.rs"
[[test]]
name = "web"
path = "tests/web.rs"
[dependencies.futures-util]
version = "^0.3.31"
features = [
"io",
"sink",
]
[dependencies.js-sys]
version = "^0.3.85"
[dependencies.wasm-bindgen]
version = "0.2.108"
[dependencies.wasm-bindgen-futures]
version = "^0.4.58"
[dependencies.web-sys]
version = "^0.3.85"
features = [
"AbortSignal",
"QueuingStrategy",
"ReadableStream",
"ReadableStreamType",
"ReadableWritablePair",
"ReadableStreamByobReader",
"ReadableStreamReaderMode",
"ReadableStreamReadResult",
"ReadableStreamByobRequest",
"ReadableStreamDefaultReader",
"ReadableByteStreamController",
"ReadableStreamGetReaderOptions",
"ReadableStreamDefaultController",
"StreamPipeOptions",
"TransformStream",
"TransformStreamDefaultController",
"Transformer",
"UnderlyingSink",
"UnderlyingSource",
"WritableStream",
"WritableStreamDefaultController",
"WritableStreamDefaultWriter",
]
[dev-dependencies.gloo-timers]
version = "^0.3.0"
features = ["futures"]
[dev-dependencies.pin-project]
version = "^1"
[dev-dependencies.tokio]
version = "^1"
features = [
"macros",
"rt",
]
[dev-dependencies.wasm-bindgen-test]
version = "0.3.58"
[dev-dependencies.web-sys]
version = "^0.3.85"
features = [
"console",
"AbortSignal",
"ErrorEvent",
"PromiseRejectionEvent",
"Response",
"ReadableStream",
"Window",
]
+77
View File
@@ -0,0 +1,77 @@
[workspace]
members = ["."]
exclude = ["tests/panic-unwind"]
[package]
name = "wasm-streams"
version = "0.5.0"
authors = ["Mattias Buelens <mattias@buelens.com>"]
edition = "2021"
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/MattiasBuelens/wasm-streams/"
description = """
Bridging between web streams and Rust streams using WebAssembly
"""
exclude = [
".github/",
"tests/panic-unwind/",
]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
js-sys = "^0.3.85"
wasm-bindgen = "0.2.108"
wasm-bindgen-futures = "^0.4.58"
futures-util = { version = "^0.3.31", features = ["io", "sink"] }
[dependencies.web-sys]
version = "^0.3.85"
features = [
"AbortSignal",
"QueuingStrategy",
"ReadableStream",
"ReadableStreamType",
"ReadableWritablePair",
"ReadableStreamByobReader",
"ReadableStreamReaderMode",
"ReadableStreamReadResult",
"ReadableStreamByobRequest",
"ReadableStreamDefaultReader",
"ReadableByteStreamController",
"ReadableStreamGetReaderOptions",
"ReadableStreamDefaultController",
"StreamPipeOptions",
"TransformStream",
"TransformStreamDefaultController",
"Transformer",
"UnderlyingSink",
"UnderlyingSource",
"WritableStream",
"WritableStreamDefaultController",
"WritableStreamDefaultWriter",
]
[dev-dependencies]
wasm-bindgen-test = "0.3.58"
tokio = { version = "^1", features = ["macros", "rt"] }
pin-project = "^1"
gloo-timers = { version = "^0.3.0", features = ["futures"] }
[dev-dependencies.web-sys]
version = "^0.3.85"
features = [
"console",
"AbortSignal",
"ErrorEvent",
"PromiseRejectionEvent",
"Response",
"ReadableStream",
"Window",
]
[package.metadata.docs.rs]
# https://blog.rust-lang.org/2020/03/15/docs-rs-opt-into-fewer-targets.html
targets = ["x86_64-unknown-linux-gnu"]
+176
View File
@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+23
View File
@@ -0,0 +1,23 @@
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:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
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.
+27
View File
@@ -0,0 +1,27 @@
# wasm-streams
[![Build Status](https://img.shields.io/github/actions/workflow/status/MattiasBuelens/wasm-streams/ci.yml?branch=main)](https://github.com/MattiasBuelens/wasm-streams)
[![Crates.io Version](https://img.shields.io/crates/v/wasm-streams.svg)](https://crates.io/crates/wasm-streams)
[![Docs.rs](https://img.shields.io/badge/docs-latest-blue.svg)](https://docs.rs/wasm-streams)
This crate bridges the gap between [web streams](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
and [Rust streams from the futures crate](https://docs.rs/futures/latest/futures/stream).
It provides Rust APIs for interacting with a JavaScript `ReadableStream`, `WritableStream` or `TransformStream`.
It also allows converting between a `ReadableStream` and a Rust `Stream`,
as well as between a `WritableStream` and a Rust `Sink`.
See the [API documentation](https://docs.rs/wasm-streams) for more information,
or check out the [examples](https://github.com/MattiasBuelens/wasm-streams/tree/main/examples).
## License
Licensed under either of
* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you,
as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
@@ -0,0 +1,36 @@
//! ## Reading a streaming fetch response
//!
//! This example makes an HTTP request using `fetch()` from `web-sys`,
//! and then consumes the response body as a Rust `Stream`.
use futures_util::StreamExt;
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{console, window, Response};
use wasm_streams::ReadableStream;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Make a fetch request
let url = "https://drager.github.io/wasm-pack/public/img/wasm-ferris.png";
let window = window().unwrap_throw();
let resp_value = JsFuture::from(window.fetch_with_str(url))
.await
.map_err(|_| "fetch failed")?;
let resp: Response = resp_value.dyn_into().unwrap_throw();
// Get the response's body as a JS ReadableStream
let raw_body = resp.body().unwrap_throw();
let body = ReadableStream::from_raw(raw_body);
// Convert the JS ReadableStream to a Rust stream
let mut stream = body.into_stream();
// Consume the stream, logging each individual chunk
while let Some(Ok(chunk)) = stream.next().await {
console::log_1(&chunk);
}
Ok(())
}
+19
View File
@@ -0,0 +1,19 @@
//! Working with the Web [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
//! in Rust.
//!
//! This crate provides wrappers around [`ReadableStream`], [`WritableStream`] and [`TransformStream`].
//! It also supports converting from and into [`Stream`]s and [`Sink`]s from the [futures] crate.
//!
//! [`Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html
//! [`Sink`]: https://docs.rs/futures/0.3.30/futures/sink/trait.Sink.html
//! [futures]: https://docs.rs/futures/0.3.30/futures/index.html
pub use readable::ReadableStream;
pub use transform::TransformStream;
pub use writable::WritableStream;
pub(crate) mod queuing_strategy;
pub mod readable;
pub mod transform;
pub(crate) mod util;
pub mod writable;
@@ -0,0 +1,19 @@
pub mod sys;
#[derive(Debug)]
pub(crate) struct QueuingStrategy {
raw: sys::QueuingStrategy,
}
impl QueuingStrategy {
pub fn new(high_water_mark: f64) -> Self {
let raw = sys::QueuingStrategy::new();
raw.set_high_water_mark(high_water_mark);
Self { raw }
}
#[inline]
pub fn into_raw(self) -> web_sys::QueuingStrategy {
self.raw
}
}
@@ -0,0 +1,4 @@
//! Raw bindings to JavaScript objects used
//! by a [`QueuingStrategy`](https://developer.mozilla.org/en-US/docs/Web/API/CountQueuingStrategy).
//! These are re-exported from [web-sys](https://docs.rs/web-sys/0.3.70/web_sys/struct.QueuingStrategy.html).
pub(crate) use web_sys::QueuingStrategy;
@@ -0,0 +1,202 @@
use std::marker::PhantomData;
use js_sys::{Object, Uint8Array};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use crate::util::{checked_cast_to_usize, clamp_to_u32, promise_to_void_future};
use super::{sys, IntoAsyncRead, ReadableStream};
/// A [`ReadableStreamBYOBReader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader)
/// that can be used to read chunks from a [`ReadableStream`](ReadableStream).
///
/// This is returned by the [`get_byob_reader`](ReadableStream::get_byob_reader) method.
///
/// When the reader is dropped, it automatically [releases its lock](https://streams.spec.whatwg.org/#release-a-lock).
#[derive(Debug)]
pub struct ReadableStreamBYOBReader<'stream> {
raw: sys::ReadableStreamBYOBReader,
_stream: PhantomData<&'stream mut ReadableStream>,
}
impl<'stream> ReadableStreamBYOBReader<'stream> {
pub(crate) fn new(stream: &mut ReadableStream) -> Result<Self, js_sys::Error> {
let reader_options = sys::ReadableStreamGetReaderOptions::new();
reader_options.set_mode(sys::ReadableStreamReaderMode::Byob);
Ok(Self {
raw: stream
.as_raw()
.unchecked_ref::<sys::ReadableStreamExt>()
.try_get_reader_with_options(&reader_options)?
.unchecked_into(),
_stream: PhantomData,
})
}
/// Acquires a reference to the underlying [JavaScript reader](sys::ReadableStreamBYOBReader).
#[inline]
pub fn as_raw(&self) -> &sys::ReadableStreamBYOBReader {
&self.raw
}
/// Waits for the stream to become closed.
///
/// This returns an error if the stream ever errors, or if the reader's lock is
/// [released](https://streams.spec.whatwg.org/#release-a-lock) before the stream finishes
/// closing.
pub async fn closed(&self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().closed()).await
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
///
/// Equivalent to [`ReadableStream.cancel`](ReadableStream::cancel).
pub async fn cancel(&mut self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().cancel()).await
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
///
/// Equivalent to [`ReadableStream.cancel_with_reason`](ReadableStream::cancel_with_reason).
pub async fn cancel_with_reason(&mut self, reason: &JsValue) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().cancel_with_reason(reason)).await
}
/// Reads the next chunk from the stream's internal queue into `dst`,
/// and returns the number of bytes read.
///
/// * If some bytes were read into `dst`, this returns `Ok(bytes_read)`.
/// * If the stream closes and no more bytes are available, this returns `Ok(0)`.
/// * If the stream cancels, this returns `Ok(0)`.
/// * If the stream encounters an `error`, this returns `Err(error)`.
///
/// This always allocated a new temporary `Uint8Array` with the same size as `dst` to hold
/// the result before copying to `dst`. We cannot pass a view on the backing WebAssembly memory
/// directly, because:
/// * `reader.read(view)` needs to transfer `view.buffer`, but `WebAssembly.Memory` buffers
/// are non-transferable.
/// * `view.buffer` can be invalidated if the WebAssembly memory grows while `read(view)`
/// is still in progress.
///
/// Therefore, it is necessary to use a separate buffer living in the JavaScript heap.
/// To avoid repeated allocations for repeated reads,
/// use [`read_with_buffer`](Self::read_with_buffer).
pub async fn read(&mut self, dst: &mut [u8]) -> Result<usize, JsValue> {
let buffer = Uint8Array::new_with_length(clamp_to_u32(dst.len()));
let (bytes_read, _) = self.read_with_buffer(dst, buffer).await?;
Ok(bytes_read)
}
/// Reads the next chunk from the stream's internal queue into `dst`,
/// and returns the number of bytes read.
///
/// The given `buffer` is used to store the bytes before they are copied to `dst`.
/// This buffer is returned back together with the result, so it can be re-used for subsequent
/// reads without extra allocations. Note that the underlying `ArrayBuffer` is transferred
/// in the process, so any other views on the original buffer will become unusable.
///
/// * If some bytes were read into `dst`, this returns `Ok((bytes_read, Some(buffer)))`.
/// * If the stream closes and no more bytes are available, this returns `Ok((0, Some(buffer)))`.
/// * If the stream cancels, this returns `Ok((0, None))`. In this case, the given buffer is
/// not returned.
/// * If the stream encounters an `error`, this returns `Err(error)`.
pub async fn read_with_buffer(
&mut self,
dst: &mut [u8],
buffer: Uint8Array,
) -> Result<(usize, Option<Uint8Array>), JsValue> {
// Save the original buffer's byte offset and length.
let buffer_offset = buffer.byte_offset();
let buffer_len = buffer.byte_length();
// Limit view to destination slice's length.
let dst_len = clamp_to_u32(dst.len());
let view = buffer.subarray(0, dst_len).unchecked_into::<Object>();
// Read into view. This transfers `buffer.buffer()`.
let promise = self.as_raw().read_with_array_buffer_view(&view);
let js_result = JsFuture::from(promise).await?;
let result = sys::ReadableStreamReadResult::from(js_result);
let js_value = result.get_value();
let filled_view = if js_value.is_undefined() {
// No new view was returned. The stream must have been canceled.
assert!(result.get_done().unwrap_or_default());
return Ok((0, None));
} else {
js_value.unchecked_into::<Uint8Array>()
};
let filled_len = checked_cast_to_usize(filled_view.byte_length());
debug_assert!(filled_len <= dst.len());
// Re-construct the original Uint8Array with the new ArrayBuffer.
let new_buffer = Uint8Array::new_with_byte_offset_and_length(
&filled_view.buffer(),
buffer_offset,
buffer_len,
);
if result.get_done().unwrap_or_default() {
debug_assert_eq!(filled_len, 0);
} else {
filled_view.copy_to(&mut dst[0..filled_len]);
}
Ok((filled_len, Some(new_buffer)))
}
/// [Releases](https://streams.spec.whatwg.org/#release-a-lock) this reader's lock on the
/// corresponding stream.
///
/// [As of January 2022](https://github.com/whatwg/streams/commit/d5f92d9f17306d31ba6b27424d23d58e89bf64a5),
/// the Streams standard allows the lock to be released even when there are still pending read
/// requests. Such requests will automatically become rejected, and this function will always
/// succeed.
///
/// However, if the Streams implementation is not yet up-to-date with this change, then
/// releasing the lock while there are pending read requests will **panic**. For a non-panicking
/// variant, use [`try_release_lock`](Self::try_release_lock).
#[inline]
pub fn release_lock(mut self) {
self.release_lock_mut()
}
fn release_lock_mut(&mut self) {
self.as_raw().release_lock()
}
/// Try to [release](https://streams.spec.whatwg.org/#release-a-lock) this reader's lock on the
/// corresponding stream.
///
/// [As of January 2022](https://github.com/whatwg/streams/commit/d5f92d9f17306d31ba6b27424d23d58e89bf64a5),
/// the Streams standard allows the lock to be released even when there are still pending read
/// requests. Such requests will automatically become rejected, and this function will always
/// return `Ok(())`.
///
/// However, if the Streams implementation is not yet up-to-date with this change, then
/// the lock cannot be released while there are pending read requests. Attempting to do so will
/// return an error and leave the reader locked to the stream.
#[inline]
pub fn try_release_lock(self) -> Result<(), (js_sys::Error, Self)> {
self.as_raw()
.unchecked_ref::<sys::ReadableStreamReaderExt>()
.try_release_lock()
.map_err(|err| (err, self))
}
/// Converts this `ReadableStreamBYOBReader` into an [`AsyncRead`].
///
/// This is similar to [`ReadableStream.into_async_read`](ReadableStream::into_async_read),
/// except that after the returned `AsyncRead` is dropped, the original `ReadableStream` is
/// still usable. This allows reading only a few bytes from the `AsyncRead`, while still
/// allowing another reader to read the remaining bytes later on.
///
/// [`AsyncRead`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncRead.html
#[inline]
pub fn into_async_read(self) -> IntoAsyncRead<'stream> {
IntoAsyncRead::new(self, false)
}
}
impl Drop for ReadableStreamBYOBReader<'_> {
fn drop(&mut self) {
self.release_lock_mut();
}
}
@@ -0,0 +1,139 @@
use std::marker::PhantomData;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use crate::util::promise_to_void_future;
use super::{sys, IntoStream, ReadableStream};
/// A [`ReadableStreamDefaultReader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader)
/// that can be used to read chunks from a [`ReadableStream`](ReadableStream).
///
/// This is returned by the [`get_reader`](ReadableStream::get_reader) method.
///
/// When the reader is dropped, it automatically [releases its lock](https://streams.spec.whatwg.org/#release-a-lock).
#[derive(Debug)]
pub struct ReadableStreamDefaultReader<'stream> {
raw: sys::ReadableStreamDefaultReader,
_stream: PhantomData<&'stream mut ReadableStream>,
}
impl<'stream> ReadableStreamDefaultReader<'stream> {
pub(crate) fn new(stream: &mut ReadableStream) -> Result<Self, js_sys::Error> {
Ok(Self {
raw: stream
.as_raw()
.unchecked_ref::<sys::ReadableStreamExt>()
.try_get_reader()?
.unchecked_into(),
_stream: PhantomData,
})
}
/// Acquires a reference to the underlying [JavaScript reader](sys::ReadableStreamDefaultReader).
#[inline]
pub fn as_raw(&self) -> &sys::ReadableStreamDefaultReader {
&self.raw
}
/// Waits for the stream to become closed.
///
/// This returns an error if the stream ever errors, or if the reader's lock is
/// [released](https://streams.spec.whatwg.org/#release-a-lock) before the stream finishes
/// closing.
pub async fn closed(&self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().closed()).await
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
///
/// Equivalent to [`ReadableStream.cancel`](ReadableStream::cancel).
pub async fn cancel(&mut self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().cancel()).await
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
///
/// Equivalent to [`ReadableStream.cancel_with_reason`](ReadableStream::cancel_with_reason).
pub async fn cancel_with_reason(&mut self, reason: &JsValue) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().cancel_with_reason(reason)).await
}
/// Reads the next chunk from the stream's internal queue.
///
/// * If a next `chunk` becomes available, this returns `Ok(Some(chunk))`.
/// * If the stream closes and no more chunks are available, this returns `Ok(None)`.
/// * If the stream encounters an `error`, this returns `Err(error)`.
pub async fn read(&mut self) -> Result<Option<JsValue>, JsValue> {
let promise = self.as_raw().read();
let js_result = JsFuture::from(promise).await?;
let result = sys::ReadableStreamReadResult::from(js_result);
if result.get_done().unwrap_or_default() {
Ok(None)
} else {
Ok(Some(result.get_value()))
}
}
/// [Releases](https://streams.spec.whatwg.org/#release-a-lock) this reader's lock on the
/// corresponding stream.
///
/// [As of January 2022](https://github.com/whatwg/streams/commit/d5f92d9f17306d31ba6b27424d23d58e89bf64a5),
/// the Streams standard allows the lock to be released even when there are still pending read
/// requests. Such requests will automatically become rejected, and this function will always
/// succeed.
///
/// However, if the Streams implementation is not yet up-to-date with this change, then
/// releasing the lock while there are pending read requests will **panic**. For a non-panicking
/// variant, use [`try_release_lock`](Self::try_release_lock).
#[inline]
pub fn release_lock(mut self) {
self.release_lock_mut()
}
fn release_lock_mut(&mut self) {
self.as_raw().release_lock()
}
/// Try to [release](https://streams.spec.whatwg.org/#release-a-lock) this reader's lock on the
/// corresponding stream.
///
/// [As of January 2022](https://github.com/whatwg/streams/commit/d5f92d9f17306d31ba6b27424d23d58e89bf64a5),
/// the Streams standard allows the lock to be released even when there are still pending read
/// requests. Such requests will automatically become rejected, and this function will always
/// return `Ok(())`.
///
/// However, if the Streams implementation is not yet up-to-date with this change, then
/// the lock cannot be released while there are pending read requests. Attempting to do so will
/// return an error and leave the reader locked to the stream.
#[inline]
pub fn try_release_lock(self) -> Result<(), (js_sys::Error, Self)> {
self.as_raw()
.unchecked_ref::<sys::ReadableStreamReaderExt>()
.try_release_lock()
.map_err(|err| (err, self))
}
/// Converts this `ReadableStreamDefaultReader` into a [`Stream`].
///
/// This is similar to [`ReadableStream.into_stream`](ReadableStream::into_stream),
/// except that after the returned `Stream` is dropped, the original `ReadableStream` is still
/// usable. This allows reading only a few chunks from the `Stream`, while still allowing
/// another reader to read the remaining chunks later on.
///
/// [`Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html
#[inline]
pub fn into_stream(self) -> IntoStream<'stream> {
IntoStream::new(self, false)
}
}
impl Drop for ReadableStreamDefaultReader<'_> {
fn drop(&mut self) {
self.release_lock_mut();
}
}
@@ -0,0 +1,146 @@
use core::pin::Pin;
use core::task::{Context, Poll};
use futures_util::io::{AsyncRead, Error};
use futures_util::ready;
use futures_util::FutureExt;
use js_sys::{Object, Uint8Array};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use crate::util::{checked_cast_to_usize, clamp_to_u32, js_to_io_error};
use super::sys::ReadableStreamReadResult;
use super::ReadableStreamBYOBReader;
/// An [`AsyncRead`] for the [`into_async_read`](super::ReadableStream::into_async_read) method.
///
/// This `AsyncRead` holds a reader, and therefore locks the [`ReadableStream`](super::ReadableStream).
/// When this `AsyncRead` is dropped, it also drops its reader which in turn
/// [releases its lock](https://streams.spec.whatwg.org/#release-a-lock).
///
/// [`AsyncRead`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncRead.html
#[must_use = "readers do nothing unless polled"]
#[derive(Debug)]
pub struct IntoAsyncRead<'reader> {
reader: Option<ReadableStreamBYOBReader<'reader>>,
buffer: Option<Uint8Array>,
fut: Option<JsFuture>,
cancel_on_drop: bool,
}
impl<'reader> IntoAsyncRead<'reader> {
#[inline]
pub(super) fn new(reader: ReadableStreamBYOBReader, cancel_on_drop: bool) -> IntoAsyncRead {
IntoAsyncRead {
reader: Some(reader),
buffer: None,
fut: None,
cancel_on_drop,
}
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
pub async fn cancel(mut self) -> Result<(), JsValue> {
match self.reader.take() {
Some(mut reader) => reader.cancel().await,
None => Ok(()),
}
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
pub async fn cancel_with_reason(mut self, reason: &JsValue) -> Result<(), JsValue> {
match self.reader.take() {
Some(mut reader) => reader.cancel_with_reason(reason).await,
None => Ok(()),
}
}
#[inline]
fn discard_reader(mut self: Pin<&mut Self>) {
self.reader = None;
self.buffer = None;
}
}
impl<'reader> AsyncRead for IntoAsyncRead<'reader> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<Result<usize, Error>> {
let read_fut = match self.fut.as_mut() {
Some(fut) => fut,
None => {
// No pending read, start reading the next bytes
let buf_len = clamp_to_u32(buf.len());
let buffer = match self.buffer.take() {
// Re-use the internal buffer if it is large enough,
// otherwise allocate a new one
Some(buffer) if buffer.byte_length() >= buf_len => buffer,
_ => Uint8Array::new_with_length(buf_len),
};
// Limit to output buffer size
let buffer = buffer.subarray(0, buf_len).unchecked_into::<Object>();
match &self.reader {
Some(reader) => {
// Read into internal buffer and store its future
let fut =
JsFuture::from(reader.as_raw().read_with_array_buffer_view(&buffer));
self.fut.insert(fut)
}
None => {
// Reader was already dropped
return Poll::Ready(Ok(0));
}
}
}
};
// Poll the future for the pending read
let js_result = ready!(read_fut.poll_unpin(cx));
self.fut = None;
// Read completed
Poll::Ready(match js_result {
Ok(js_value) => {
let result = ReadableStreamReadResult::from(js_value);
if result.get_done().unwrap_or_default() {
// End of stream
self.discard_reader();
Ok(0)
} else {
// Cannot be canceled, so view must exist
let filled_view = result.get_value().unchecked_into::<Uint8Array>();
// Copy bytes to output buffer
let filled_len = checked_cast_to_usize(filled_view.byte_length());
debug_assert!(filled_len <= buf.len());
filled_view.copy_to(&mut buf[0..filled_len]);
// Re-construct internal buffer with the new ArrayBuffer
self.buffer = Some(Uint8Array::new(&filled_view.buffer()));
Ok(filled_len)
}
}
Err(js_value) => {
// Error
self.discard_reader();
Err(js_to_io_error(js_value))
}
})
}
}
impl<'reader> Drop for IntoAsyncRead<'reader> {
fn drop(&mut self) {
if self.cancel_on_drop {
if let Some(reader) = self.reader.take() {
let on_rejected = Closure::once(|_| {});
let _ = reader.as_raw().cancel().catch(&on_rejected);
on_rejected.forget();
}
}
}
}
@@ -0,0 +1,118 @@
use core::pin::Pin;
use core::task::{Context, Poll};
use futures_util::ready;
use futures_util::stream::{FusedStream, Stream};
use futures_util::FutureExt;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use super::sys::ReadableStreamReadResult;
use super::ReadableStreamDefaultReader;
/// A [`Stream`] for the [`into_stream`](super::ReadableStream::into_stream) method.
///
/// This `Stream` holds a reader, and therefore locks the [`ReadableStream`](super::ReadableStream).
/// When this `Stream` is dropped, it also drops its reader which in turn
/// [releases its lock](https://streams.spec.whatwg.org/#release-a-lock).
///
/// [`Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html
#[must_use = "streams do nothing unless polled"]
#[derive(Debug)]
pub struct IntoStream<'reader> {
reader: Option<ReadableStreamDefaultReader<'reader>>,
fut: Option<JsFuture>,
cancel_on_drop: bool,
}
impl<'reader> IntoStream<'reader> {
#[inline]
pub(super) fn new(reader: ReadableStreamDefaultReader, cancel_on_drop: bool) -> IntoStream {
IntoStream {
reader: Some(reader),
fut: None,
cancel_on_drop,
}
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
pub async fn cancel(mut self) -> Result<(), JsValue> {
match self.reader.take() {
Some(mut reader) => reader.cancel().await,
None => Ok(()),
}
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
pub async fn cancel_with_reason(mut self, reason: &JsValue) -> Result<(), JsValue> {
match self.reader.take() {
Some(mut reader) => reader.cancel_with_reason(reason).await,
None => Ok(()),
}
}
}
impl FusedStream for IntoStream<'_> {
fn is_terminated(&self) -> bool {
self.reader.is_none() && self.fut.is_none()
}
}
impl<'reader> Stream for IntoStream<'reader> {
type Item = Result<JsValue, JsValue>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let read_fut = match self.fut.as_mut() {
Some(fut) => fut,
None => match &self.reader {
Some(reader) => {
// No pending read
// Start reading the next chunk and create future from read promise
let fut = JsFuture::from(reader.as_raw().read());
self.fut.insert(fut)
}
None => {
// Reader was already dropped
return Poll::Ready(None);
}
},
};
// Poll the future for the pending read
let js_result = ready!(read_fut.poll_unpin(cx));
self.fut = None;
// Read completed
Poll::Ready(match js_result {
Ok(js_value) => {
let result = ReadableStreamReadResult::from(js_value);
if result.get_done().unwrap_or_default() {
// End of stream, drop reader
self.reader = None;
None
} else {
Some(Ok(result.get_value()))
}
}
Err(js_value) => {
// Error, drop reader
self.reader = None;
Some(Err(js_value))
}
})
}
}
impl<'reader> Drop for IntoStream<'reader> {
fn drop(&mut self) {
if self.cancel_on_drop {
if let Some(reader) = self.reader.take() {
let on_rejected = Closure::once(|_| {});
let _ = reader.as_raw().cancel().catch(&on_rejected);
on_rejected.forget();
}
}
}
}
@@ -0,0 +1,151 @@
use std::cell::RefCell;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::rc::Rc;
use futures_util::future::{abortable, AbortHandle, TryFutureExt};
use futures_util::io::{AsyncRead, AsyncReadExt};
use js_sys::{Error as JsError, Promise, Uint8Array};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use crate::util::{checked_cast_to_u32, clamp_to_usize};
use super::sys;
#[wasm_bindgen]
pub(crate) struct IntoUnderlyingByteSource {
inner: Rc<RefCell<Inner>>,
default_buffer_len: usize,
controller: Option<sys::ReadableByteStreamController>,
pull_handle: Option<AbortHandle>,
}
impl IntoUnderlyingByteSource {
pub fn new(async_read: Box<dyn AsyncRead>, default_buffer_len: usize) -> Self {
IntoUnderlyingByteSource {
inner: Rc::new(RefCell::new(Inner::new(async_read))),
default_buffer_len,
controller: None,
pull_handle: None,
}
}
}
#[allow(clippy::await_holding_refcell_ref)]
#[wasm_bindgen]
impl IntoUnderlyingByteSource {
#[wasm_bindgen(getter, js_name = type)]
pub fn type_(&self) -> sys::ReadableStreamType {
sys::ReadableStreamType::Bytes
}
#[wasm_bindgen(getter, js_name = autoAllocateChunkSize)]
pub fn auto_allocate_chunk_size(&self) -> usize {
self.default_buffer_len
}
pub fn start(&mut self, controller: sys::ReadableByteStreamController) {
self.controller = Some(controller);
}
pub fn pull(&mut self, controller: sys::ReadableByteStreamController) -> Promise {
let inner = self.inner.clone();
let fut = async move {
// This mutable borrow can never panic, since the ReadableStream always queues
// each operation on the underlying source.
let mut inner = inner.try_borrow_mut().unwrap_throw();
inner.pull(controller).await
};
// Allow aborting the future from cancel().
let (fut, handle) = abortable(fut);
// Ignore errors from aborting the future.
let fut = fut.unwrap_or_else(|_| Ok(JsValue::undefined()));
self.pull_handle = Some(handle);
// SAFETY: We use the take-and-replace pattern in Inner::pull() to ensure
// that if a panic occurs, the async_read is already taken out of the Option,
// leaving it in a clean None state. This prevents use of corrupted state
// after a panic is caught.
future_to_promise(AssertUnwindSafe(fut))
}
pub fn cancel(self) {
// The stream has been canceled, drop everything.
drop(self);
}
}
impl Drop for IntoUnderlyingByteSource {
fn drop(&mut self) {
// Abort the pending pull, if any.
if let Some(handle) = self.pull_handle.take() {
handle.abort();
}
}
}
struct Inner {
async_read: Option<Pin<Box<dyn AsyncRead>>>,
buffer: Vec<u8>,
}
impl Inner {
fn new(async_read: Box<dyn AsyncRead>) -> Self {
Inner {
async_read: Some(async_read.into()),
buffer: Vec::new(),
}
}
async fn pull(
&mut self,
controller: sys::ReadableByteStreamController,
) -> Result<JsValue, JsValue> {
// We set autoAllocateChunkSize, so there should always be a BYOB request.
let request = controller.byob_request().unwrap_throw();
// Resize the buffer to fit the BYOB request.
let request_view = request.view().unwrap_throw().unchecked_into::<Uint8Array>();
let request_len = clamp_to_usize(request_view.byte_length());
if self.buffer.len() < request_len {
self.buffer.resize(request_len, 0);
}
// Take the async_read out before the fallible/panickable operation.
// This ensures that if a panic occurs, self.async_read is already None,
// so any subsequent call will fail cleanly instead of using corrupted state.
let mut async_read = self.async_read.take().unwrap_throw();
match async_read.read(&mut self.buffer[0..request_len]).await {
Ok(0) => {
// Stream closed: don't put it back, clear buffer, close controller
self.buffer = Vec::new();
controller.close()?;
request.respond_with_u32(0)?;
}
Ok(bytes_read) => {
// Success: put the async_read back for reuse
self.async_read = Some(async_read);
// Copy read bytes from buffer to BYOB request view
debug_assert!(bytes_read <= request_len);
let bytes_read_u32 = checked_cast_to_u32(bytes_read);
let dest = Uint8Array::new_with_byte_offset_and_length(
&request_view.buffer(),
request_view.byte_offset(),
bytes_read_u32,
);
dest.copy_from(&self.buffer[0..bytes_read]);
// Respond to BYOB request
request.respond_with_u32(bytes_read_u32)?;
}
Err(err) => {
// Error: don't put it back, clear buffer, return error
self.buffer = Vec::new();
return Err(JsError::new(&err.to_string()).into());
}
};
// Panic: async_read is dropped during unwind, self.async_read remains None
Ok(JsValue::undefined())
}
}
@@ -0,0 +1,109 @@
use std::cell::RefCell;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::rc::Rc;
use futures_util::future::{abortable, AbortHandle, TryFutureExt};
use futures_util::stream::{Stream, TryStreamExt};
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
use super::sys;
type JsValueStream = dyn Stream<Item = Result<JsValue, JsValue>>;
#[wasm_bindgen]
pub(crate) struct IntoUnderlyingSource {
inner: Rc<RefCell<Inner>>,
pull_handle: Option<AbortHandle>,
}
impl IntoUnderlyingSource {
pub fn new(stream: Box<JsValueStream>) -> Self {
IntoUnderlyingSource {
inner: Rc::new(RefCell::new(Inner::new(stream))),
pull_handle: None,
}
}
}
#[allow(clippy::await_holding_refcell_ref)]
#[wasm_bindgen]
impl IntoUnderlyingSource {
pub fn pull(&mut self, controller: sys::ReadableStreamDefaultController) -> Promise {
let inner = self.inner.clone();
let fut = async move {
// This mutable borrow can never panic, since the ReadableStream always queues
// each operation on the underlying source.
let mut inner = inner.try_borrow_mut().unwrap_throw();
inner.pull(controller).await
};
// Allow aborting the future from cancel().
let (fut, handle) = abortable(fut);
// Ignore errors from aborting the future.
let fut = fut.unwrap_or_else(|_| Ok(JsValue::undefined()));
self.pull_handle = Some(handle);
// SAFETY: We use the take-and-replace pattern in Inner::pull() to ensure
// that if a panic occurs, the stream is already taken out of the Option,
// leaving it in a clean None state. This prevents use of corrupted state
// after a panic is caught.
future_to_promise(AssertUnwindSafe(fut))
}
pub fn cancel(self) {
// The stream has been canceled, drop everything.
drop(self);
}
}
impl Drop for IntoUnderlyingSource {
fn drop(&mut self) {
// Abort the pending pull, if any.
if let Some(handle) = self.pull_handle.take() {
handle.abort();
}
}
}
struct Inner {
stream: Option<Pin<Box<JsValueStream>>>,
}
impl Inner {
fn new(stream: Box<JsValueStream>) -> Self {
Inner {
stream: Some(stream.into()),
}
}
async fn pull(
&mut self,
controller: sys::ReadableStreamDefaultController,
) -> Result<JsValue, JsValue> {
// Take the stream out before the fallible/panickable operation.
// This ensures that if a panic occurs, self.stream is already None,
// so any subsequent call will fail cleanly instead of using corrupted state.
let mut stream = self.stream.take().unwrap_throw();
match stream.try_next().await {
Ok(Some(chunk)) => {
// Success with chunk: put the stream back and enqueue
self.stream = Some(stream);
controller.enqueue_with_chunk(&chunk)?;
}
Ok(None) => {
// Stream closed: don't put it back (it's exhausted), close controller
controller.close()?;
}
Err(err) => {
// Error: don't put it back, return the error
return Err(err);
}
};
// Panic: stream is dropped during unwind, self.stream remains None
Ok(JsValue::undefined())
}
}
+382
View File
@@ -0,0 +1,382 @@
//! Bindings and conversions for
//! [readable streams](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
use futures_util::io::AsyncRead;
use futures_util::Stream;
use js_sys::Object;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
pub use byob_reader::ReadableStreamBYOBReader;
pub use default_reader::ReadableStreamDefaultReader;
pub use into_async_read::IntoAsyncRead;
pub use into_stream::IntoStream;
use into_underlying_source::IntoUnderlyingSource;
pub use pipe_options::PipeOptions;
use crate::queuing_strategy::QueuingStrategy;
use crate::readable::into_underlying_byte_source::IntoUnderlyingByteSource;
use crate::util::promise_to_void_future;
use crate::writable::WritableStream;
mod byob_reader;
mod default_reader;
mod into_async_read;
mod into_stream;
mod into_underlying_byte_source;
mod into_underlying_source;
mod pipe_options;
pub mod sys;
/// A [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
///
/// `ReadableStream`s can be created from a [raw JavaScript stream](sys::ReadableStream) with
/// [`from_raw`](Self::from_raw), or from a Rust [`Stream`] with [`from_stream`](Self::from_stream).
///
/// They can be converted into a [raw JavaScript stream](sys::ReadableStream) with
/// [`into_raw`](Self::into_raw), or into a Rust [`Stream`] with [`into_stream`](Self::into_stream).
///
/// If the browser supports [readable byte streams](https://streams.spec.whatwg.org/#readable-byte-stream),
/// then they can be created from a Rust [`AsyncRead`] with [`from_async_read`](Self::from_async_read),
/// or converted into one with [`into_async_read`](Self::into_async_read).
///
/// [`Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html
/// [`AsyncRead`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncRead.html
#[derive(Debug)]
pub struct ReadableStream {
raw: sys::ReadableStream,
}
impl ReadableStream {
/// Creates a new `ReadableStream` from a [JavaScript stream](sys::ReadableStream).
#[inline]
pub fn from_raw(raw: sys::ReadableStream) -> Self {
Self { raw }
}
/// Creates a new `ReadableStream` from a [`Stream`].
///
/// Items and errors must be represented as raw [`JsValue`]s.
/// Use [`map`], [`map_ok`] and/or [`map_err`] to convert a stream's items to a `JsValue`
/// before passing it to this function.
///
/// [`Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html
/// [`map`]: https://docs.rs/futures/0.3.30/futures/stream/trait.StreamExt.html#method.map
/// [`map_ok`]: https://docs.rs/futures/0.3.30/futures/stream/trait.TryStreamExt.html#method.map_ok
/// [`map_err`]: https://docs.rs/futures/0.3.30/futures/stream/trait.TryStreamExt.html#method.map_err
pub fn from_stream<St>(stream: St) -> Self
where
St: Stream<Item = Result<JsValue, JsValue>> + 'static,
{
let source = IntoUnderlyingSource::new(Box::new(stream));
// Set HWM to 0 to prevent the JS ReadableStream from buffering chunks in its queue,
// since the original Rust stream is better suited to handle that.
let strategy = QueuingStrategy::new(0.0);
let raw =
sys::ReadableStreamExt::new_with_into_underlying_source(source, strategy.into_raw())
.unchecked_into();
Self::from_raw(raw)
}
/// Creates a new `ReadableStream` from an [`AsyncRead`].
///
/// This creates a readable byte stream whose `autoAllocateChunkSize` is `default_buffer_len`.
/// Therefore, if a default reader is used to consume the stream, the given `async_read`
/// will be [polled][AsyncRead::poll_read] with a buffer of this size. If a BYOB reader is used,
/// then it will be polled with a buffer of the same size as the BYOB read request instead.
///
/// **Panics** if readable byte streams are not supported by the browser.
///
/// [`AsyncRead`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncRead.html
/// [AsyncRead::poll_read]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncRead.html#tymethod.poll_read
// TODO Non-panicking variant?
pub fn from_async_read<R>(async_read: R, default_buffer_len: usize) -> Self
where
R: AsyncRead + 'static,
{
let source = IntoUnderlyingByteSource::new(Box::new(async_read), default_buffer_len);
let raw = sys::ReadableStreamExt::new_with_into_underlying_byte_source(source)
.expect_throw("readable byte streams not supported")
.unchecked_into();
Self::from_raw(raw)
}
/// Creates a new `ReadableStream` wrapping the provided [iterable] or [async iterable].
///
/// This can be used to adapt various kinds of objects into a readable stream,
/// such as an [array], an [async generator] or a [Node.js readable stream][Readable].
///
/// **Panics** if `ReadableStream.from()` is not supported by the browser,
/// or if the given object is not a valid iterable or async iterable.
/// For a non-panicking variant, use [`try_from`](Self::try_from).
///
/// [iterable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
/// [async iterable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
/// [array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
/// [async generator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator
/// [Readable]: https://nodejs.org/api/stream.html#class-streamreadable
pub fn from(async_iterable: Object) -> Self {
Self::try_from(async_iterable).unwrap_throw()
}
/// Try to create a new `ReadableStream` wrapping the provided [iterable] or [async iterable].
///
/// This can be used to adapt various kinds of objects into a readable stream,
/// such as an [array], an [async generator] or a [Node.js readable stream][Readable].
///
/// If `ReadableStream.from()` is not supported by the browser,
/// or if the given object is not a valid iterable or async iterable,
/// then this returns an error.
///
/// [iterable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
/// [async iterable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
/// [array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
/// [async generator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator
/// [Readable]: https://nodejs.org/api/stream.html#class-streamreadable
pub fn try_from(async_iterable: Object) -> Result<Self, js_sys::Error> {
let raw = sys::ReadableStreamExt::from_async_iterable(&async_iterable)?.unchecked_into();
Ok(Self::from_raw(raw))
}
/// Acquires a reference to the underlying [JavaScript stream](sys::ReadableStream).
#[inline]
pub fn as_raw(&self) -> &sys::ReadableStream {
&self.raw
}
/// Consumes this `ReadableStream`, returning the underlying [JavaScript stream](sys::ReadableStream).
#[inline]
pub fn into_raw(self) -> sys::ReadableStream {
self.raw
}
/// Returns `true` if the stream is [locked to a reader](https://streams.spec.whatwg.org/#lock).
#[inline]
pub fn is_locked(&self) -> bool {
self.as_raw().locked()
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
///
/// If the stream is currently locked to a reader, then this returns an error.
pub async fn cancel(&mut self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().cancel()).await
}
/// [Cancels](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// signaling a loss of interest in the stream by a consumer.
///
/// The supplied `reason` will be given to the underlying source, which may or may not use it.
///
/// If the stream is currently locked to a reader, then this returns an error.
pub async fn cancel_with_reason(&mut self, reason: &JsValue) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().cancel_with_reason(reason)).await
}
/// Creates a [default reader](ReadableStreamDefaultReader) and
/// [locks](https://streams.spec.whatwg.org/#lock) the stream to the new reader.
///
/// While the stream is locked, no other reader can be acquired until this one is released.
///
/// **Panics** if the stream is already locked to a reader. For a non-panicking variant,
/// use [`try_get_reader`](Self::try_get_reader).
#[inline]
pub fn get_reader(&mut self) -> ReadableStreamDefaultReader<'_> {
self.try_get_reader()
.expect_throw("already locked to a reader")
}
/// Try to create a [default reader](ReadableStreamDefaultReader) and
/// [lock](https://streams.spec.whatwg.org/#lock) the stream to the new reader.
///
/// While the stream is locked, no other reader can be acquired until this one is released.
///
/// If the stream is already locked to a reader, then this returns an error.
pub fn try_get_reader(&mut self) -> Result<ReadableStreamDefaultReader<'_>, js_sys::Error> {
ReadableStreamDefaultReader::new(self)
}
/// Creates a [BYOB reader](ReadableStreamBYOBReader) and
/// [locks](https://streams.spec.whatwg.org/#lock) the stream to the new reader.
///
/// While the stream is locked, no other reader can be acquired until this one is released.
///
/// **Panics** if the stream is already locked to a reader, or if this stream is not a readable
/// byte stream. For a non-panicking variant, use [`try_get_reader`](Self::try_get_reader).
pub fn get_byob_reader(&mut self) -> ReadableStreamBYOBReader<'_> {
self.try_get_byob_reader()
.expect_throw("already locked to a reader, or not a readable byte stream")
}
/// Try to create a [BYOB reader](ReadableStreamBYOBReader) and
/// [lock](https://streams.spec.whatwg.org/#lock) the stream to the new reader.
///
/// While the stream is locked, no other reader can be acquired until this one is released.
///
/// If the stream is already locked to a reader, then this returns an error.
pub fn try_get_byob_reader(&mut self) -> Result<ReadableStreamBYOBReader<'_>, js_sys::Error> {
ReadableStreamBYOBReader::new(self)
}
/// [Pipes](https://streams.spec.whatwg.org/#piping) this readable stream to a given
/// writable stream.
///
/// Piping a stream will [lock](https://streams.spec.whatwg.org/#lock) it for the duration
/// of the pipe, preventing any other consumer from acquiring a reader.
///
/// This returns `()` if the pipe completes successfully, or `Err(error)` if any `error`
/// was encountered during the process.
pub async fn pipe_to<'a>(&'a mut self, dest: &'a mut WritableStream) -> Result<(), JsValue> {
self.pipe_to_with_options(dest, &PipeOptions::default())
.await
}
/// [Pipes](https://streams.spec.whatwg.org/#piping) this readable stream to a given
/// writable stream.
///
/// Piping a stream will [lock](https://streams.spec.whatwg.org/#lock) it for the duration
/// of the pipe, preventing any other consumer from acquiring a reader.
///
/// Errors and closures of the source and destination streams propagate as follows:
/// * An error in the source readable stream will [abort](https://streams.spec.whatwg.org/#abort-a-writable-stream)
/// the destination writable stream, unless [`options.prevent_abort`](PipeOptions::prevent_abort)
/// is `true`.
/// * An error in the destination writable stream will [cancel](https://streams.spec.whatwg.org/#cancel-a-readable-stream)
/// the source readable stream, unless [`options.prevent_cancel`](PipeOptions::prevent_cancel)
/// is `true`.
/// * When the source readable stream closes, the destination writable stream will be closed,
/// unless [`options.prevent_close`](PipeOptions::prevent_close) is `true`.
/// * If the destination writable stream starts out closed or closing, the source readable stream
/// will be [canceled](https://streams.spec.whatwg.org/#cancel-a-readable-stream),
/// unless unless [`options.prevent_cancel`](PipeOptions::prevent_cancel) is `true`.
///
/// This returns `()` if the pipe completes successfully, or `Err(error)` if any `error`
/// was encountered during the process.
pub async fn pipe_to_with_options<'a>(
&'a mut self,
dest: &'a mut WritableStream,
options: &PipeOptions,
) -> Result<(), JsValue> {
let promise = self
.as_raw()
.pipe_to_with_options(dest.as_raw(), &options.clone().into_raw());
promise_to_void_future(promise).await
}
/// [Tees](https://streams.spec.whatwg.org/#tee-a-readable-stream) this readable stream,
/// returning the two resulting branches as new [`ReadableStream`] instances.
///
/// Teeing a stream will [lock](https://streams.spec.whatwg.org/#lock) it, preventing any other
/// consumer from acquiring a reader.
/// To [cancel](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// cancel both of the resulting branches; a composite cancellation reason will then be
/// propagated to the stream's underlying source.
///
/// Note that the chunks seen in each branch will be the same object.
/// If the chunks are not immutable, this could allow interference between the two branches.
///
/// **Panics** if the stream is already locked to a reader. For a non-panicking variant,
/// use [`try_tee`](Self::try_tee).
pub fn tee(self) -> (ReadableStream, ReadableStream) {
self.try_tee().expect_throw("already locked to a reader")
}
/// Tries to [tee](https://streams.spec.whatwg.org/#tee-a-readable-stream) this readable stream,
/// returning the two resulting branches as new [`ReadableStream`] instances.
///
/// Teeing a stream will [lock](https://streams.spec.whatwg.org/#lock) it, preventing any other
/// consumer from acquiring a reader.
/// To [cancel](https://streams.spec.whatwg.org/#cancel-a-readable-stream) the stream,
/// cancel both of the resulting branches; a composite cancellation reason will then be
/// propagated to the stream's underlying source.
///
/// Note that the chunks seen in each branch will be the same object.
/// If the chunks are not immutable, this could allow interference between the two branches.
///
/// If the stream is already locked to a reader, then this returns an error
/// along with the original `ReadableStream`.
pub fn try_tee(self) -> Result<(ReadableStream, ReadableStream), (js_sys::Error, Self)> {
let branches = self
.as_raw()
.unchecked_ref::<sys::ReadableStreamExt>()
.try_tee()
.map_err(|err| (err, self))?;
debug_assert_eq!(branches.length(), 2);
let (left, right) = (branches.get(0), branches.get(1));
Ok((
Self::from_raw(left.unchecked_into()),
Self::from_raw(right.unchecked_into()),
))
}
/// Converts this `ReadableStream` into a [`Stream`].
///
/// Items and errors are represented by their raw [`JsValue`].
/// Use [`map`], [`map_ok`] and/or [`map_err`] on the returned stream to convert them to a more
/// appropriate type.
///
/// **Panics** if the stream is already locked to a reader. For a non-panicking variant,
/// use [`try_into_stream`](Self::try_into_stream).
///
/// [`Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html
/// [`map`]: https://docs.rs/futures/0.3.30/futures/stream/trait.StreamExt.html#method.map
/// [`map_ok`]: https://docs.rs/futures/0.3.30/futures/stream/trait.TryStreamExt.html#method.map_ok
/// [`map_err`]: https://docs.rs/futures/0.3.30/futures/stream/trait.TryStreamExt.html#method.map_err
#[inline]
pub fn into_stream(self) -> IntoStream<'static> {
self.try_into_stream()
.expect_throw("already locked to a reader")
}
/// Try to convert this `ReadableStream` into a [`Stream`].
///
/// Items and errors are represented by their raw [`JsValue`].
/// Use [`map`], [`map_ok`] and/or [`map_err`] on the returned stream to convert them to a more
/// appropriate type.
///
/// If the stream is already locked to a reader, then this returns an error
/// along with the original `ReadableStream`.
///
/// [`Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html
/// [`map`]: https://docs.rs/futures/0.3.30/futures/stream/trait.StreamExt.html#method.map
/// [`map_ok`]: https://docs.rs/futures/0.3.30/futures/stream/trait.TryStreamExt.html#method.map_ok
/// [`map_err`]: https://docs.rs/futures/0.3.30/futures/stream/trait.TryStreamExt.html#method.map_err
pub fn try_into_stream(mut self) -> Result<IntoStream<'static>, (js_sys::Error, Self)> {
let reader = ReadableStreamDefaultReader::new(&mut self).map_err(|err| (err, self))?;
Ok(IntoStream::new(reader, true))
}
/// Converts this `ReadableStream` into an [`AsyncRead`].
///
/// **Panics** if the stream is already locked to a reader, or if this stream is not a readable
/// byte stream. For a non-panicking variant, use [`try_into_async_read`](Self::try_into_async_read).
///
/// [`AsyncRead`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncRead.html
#[inline]
pub fn into_async_read(self) -> IntoAsyncRead<'static> {
self.try_into_async_read()
.expect_throw("already locked to a reader, or not a readable byte stream")
}
/// Try to convert this `ReadableStream` into an [`AsyncRead`].
///
/// If the stream is already locked to a reader, or if this stream is not a readable byte
/// stream, then this returns an error along with the original `ReadableStream`.
///
/// [`AsyncRead`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncRead.html
pub fn try_into_async_read(mut self) -> Result<IntoAsyncRead<'static>, (js_sys::Error, Self)> {
let reader = ReadableStreamBYOBReader::new(&mut self).map_err(|err| (err, self))?;
Ok(IntoAsyncRead::new(reader, true))
}
}
impl<St> From<St> for ReadableStream
where
St: Stream<Item = Result<JsValue, JsValue>> + 'static,
{
/// Equivalent to [`from_stream`](Self::from_stream).
#[inline]
fn from(stream: St) -> Self {
Self::from_stream(stream)
}
}
@@ -0,0 +1,61 @@
use web_sys::AbortSignal;
use super::sys;
/// Options for [`pipe_to_with_options`](super::ReadableStream::pipe_to_with_options).
#[derive(Clone, Debug, Default)]
pub struct PipeOptions {
raw: sys::PipeOptions,
}
impl PipeOptions {
/// Creates a blank new set of pipe options.
///
/// Equivalent to [`PipeOptions::default`](Default::default).
pub fn new() -> Self {
Default::default()
}
/// Creates a set of pipe options from a raw [`PipeOptions`](sys::PipeOptions) object.
#[inline]
pub fn from_raw(raw: sys::PipeOptions) -> Self {
Self { raw }
}
/// Convert this to a raw [`PipeOptions`](sys::PipeOptions) object.
#[inline]
pub fn into_raw(self) -> sys::PipeOptions {
self.raw
}
/// Sets whether the destination writable stream should be closed
/// when the source readable stream closes.
pub fn prevent_close(&mut self, prevent_close: bool) -> &mut Self {
self.raw.set_prevent_close(prevent_close);
self
}
/// Sets whether the source readable stream should be [canceled](https://streams.spec.whatwg.org/#cancel-a-readable-stream)
/// when the destination writable stream errors.
pub fn prevent_cancel(&mut self, prevent_cancel: bool) -> &mut Self {
self.raw.set_prevent_cancel(prevent_cancel);
self
}
/// Sets whether the destination writable stream should be [aborted](https://streams.spec.whatwg.org/#abort-a-writable-stream)
/// when the source readable stream errors.
pub fn prevent_abort(&mut self, prevent_abort: bool) -> &mut Self {
self.raw.set_prevent_abort(prevent_abort);
self
}
/// Sets an abort signal to abort the ongoing pipe operation.
/// When the signal is aborted, the source readable stream will be canceled
/// and the destination writable stream will be aborted
/// unless the respective options [`prevent_cancel`](Self::prevent_cancel)
/// or [`prevent_abort`](Self::prevent_abort) are set.
pub fn signal(&mut self, signal: AbortSignal) -> &mut Self {
self.raw.set_signal(&signal);
self
}
}
+64
View File
@@ -0,0 +1,64 @@
//! Raw bindings to JavaScript objects used
//! by a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
//! These are re-exported from [web-sys](https://docs.rs/web-sys/0.3.70/web_sys/struct.ReadableStream.html).
use js_sys::{Array, Error, Object};
use wasm_bindgen::prelude::*;
pub use web_sys::ReadableByteStreamController;
// Re-export from web-sys
pub use web_sys::ReadableStream;
pub use web_sys::ReadableStreamByobReader as ReadableStreamBYOBReader;
pub use web_sys::ReadableStreamByobRequest as ReadableStreamBYOBRequest;
pub use web_sys::ReadableStreamDefaultController;
pub use web_sys::ReadableStreamDefaultReader;
pub use web_sys::ReadableStreamGetReaderOptions;
pub use web_sys::ReadableStreamReadResult;
pub use web_sys::ReadableStreamReaderMode;
pub use web_sys::ReadableStreamType;
pub use web_sys::StreamPipeOptions as PipeOptions;
use crate::queuing_strategy::sys::QueuingStrategy;
use crate::readable::into_underlying_byte_source::IntoUnderlyingByteSource;
use crate::readable::into_underlying_source::IntoUnderlyingSource;
#[wasm_bindgen]
extern "C" {
/// Additional methods for [`ReadableStream`](web_sys::ReadableStream).
#[wasm_bindgen(js_name = ReadableStream, typescript_type = "ReadableStream")]
pub(crate) type ReadableStreamExt;
#[wasm_bindgen(constructor, js_class = ReadableStream)]
pub(crate) fn new_with_into_underlying_source(
source: IntoUnderlyingSource,
strategy: QueuingStrategy,
) -> ReadableStreamExt;
#[wasm_bindgen(constructor, catch, js_class = ReadableStream)]
pub(crate) fn new_with_into_underlying_byte_source(
source: IntoUnderlyingByteSource,
) -> Result<ReadableStreamExt, Error>;
#[wasm_bindgen(method, catch, js_class = ReadableStream, js_name = getReader)]
pub(crate) fn try_get_reader(this: &ReadableStreamExt) -> Result<Object, Error>;
#[wasm_bindgen(method, catch, js_class = ReadableStream, js_name = getReader)]
pub(crate) fn try_get_reader_with_options(
this: &ReadableStreamExt,
options: &ReadableStreamGetReaderOptions,
) -> Result<Object, Error>;
#[wasm_bindgen(method, catch, js_class = ReadableStream, js_name = tee)]
pub(crate) fn try_tee(this: &ReadableStreamExt) -> Result<Array, Error>;
#[wasm_bindgen(catch, static_method_of = ReadableStreamExt, js_class = ReadableStream, js_name = from)]
pub(crate) fn from_async_iterable(async_iterable: &Object) -> Result<ReadableStreamExt, Error>;
}
#[wasm_bindgen]
extern "C" {
/// Additional methods for [`ReadableStreamDefaultReader`](web_sys::ReadableStreamDefaultReader)
/// and [`ReadableStreamByobReader`](web_sys::ReadableStreamByobReader).
pub(crate) type ReadableStreamReaderExt;
#[wasm_bindgen(method, catch, js_name = releaseLock)]
pub(crate) fn try_release_lock(this: &ReadableStreamReaderExt) -> Result<(), Error>;
}
+56
View File
@@ -0,0 +1,56 @@
//! Bindings and conversions for
//! [transform streams](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream).
use crate::readable::ReadableStream;
use crate::writable::WritableStream;
pub mod sys;
/// A [`TransformStream`](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream).
///
/// `TransformStream`s can be created from a [raw JavaScript stream](sys::TransformStream) with
/// [`from_raw`](Self::from_raw), and can be converted back with [`into_raw`](Self::into_raw).
///
/// Use [`readable`](Self::readable) and [`writable`](Self::writable) to access the readable and
/// writable side of the transform stream.
/// These can then be converted into a Rust [`Stream`] and [`Sink`] respectively
/// using [`into_stream`](super::ReadableStream::into_stream)
/// and [`into_sink`](super::WritableStream::into_sink).
///
/// [`Stream`]: https://docs.rs/futures/0.3.30/futures/stream/trait.Stream.html
/// [`Sink`]: https://docs.rs/futures/0.3.30/futures/sink/trait.Sink.html
#[derive(Debug)]
pub struct TransformStream {
raw: sys::TransformStream,
}
impl TransformStream {
/// Creates a new `TransformStream` from a [JavaScript stream](sys::TransformStream).
#[inline]
pub fn from_raw(raw: sys::TransformStream) -> Self {
Self { raw }
}
/// Acquires a reference to the underlying [JavaScript stream](sys::TransformStream).
#[inline]
pub fn as_raw(&self) -> &sys::TransformStream {
&self.raw
}
/// Consumes this `TransformStream`, returning the underlying [JavaScript stream](sys::TransformStream).
#[inline]
pub fn into_raw(self) -> sys::TransformStream {
self.raw
}
/// Returns the readable side of the transform stream.
#[inline]
pub fn readable(&self) -> ReadableStream {
ReadableStream::from_raw(self.as_raw().readable())
}
/// Returns the writable side of the transform stream.
#[inline]
pub fn writable(&self) -> WritableStream {
WritableStream::from_raw(self.as_raw().writable())
}
}
@@ -0,0 +1,4 @@
//! Raw bindings to JavaScript objects used
//! by a [`TransformStream`](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream).
//! These are re-exported from [web-sys](https://docs.rs/web-sys/0.3.70/web_sys/struct.TransformStream.html).
pub use web_sys::TransformStream;
+54
View File
@@ -0,0 +1,54 @@
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
pub(crate) async fn promise_to_void_future(promise: Promise) -> Result<(), JsValue> {
let js_value = JsFuture::from(promise).await?;
debug_assert!(js_value.is_undefined());
let _ = js_value;
Ok(())
}
pub(crate) fn clamp_to_u32(value: usize) -> u32 {
let wrapped = value as u32;
let overflow = value != (wrapped as usize);
if overflow {
u32::MAX
} else {
wrapped
}
}
pub(crate) fn clamp_to_usize(value: u32) -> usize {
let wrapped = value as usize;
let overflow = value != (wrapped as u32);
if overflow {
usize::MAX
} else {
wrapped
}
}
pub(crate) fn checked_cast_to_u32(value: usize) -> u32 {
let wrapped = value as u32;
debug_assert_eq!(value, wrapped as usize);
wrapped
}
pub(crate) fn checked_cast_to_usize(value: u32) -> usize {
let wrapped = value as usize;
debug_assert_eq!(value, wrapped as u32);
wrapped
}
pub(crate) fn js_to_io_error(js_value: JsValue) -> std::io::Error {
let message = js_to_string(&js_value).unwrap_or_else(|| "Unknown error".to_string());
std::io::Error::other(message)
}
fn js_to_string(js_value: &JsValue) -> Option<String> {
js_value.as_string().or_else(|| {
js_sys::Object::try_from(js_value)
.map(|js_object| js_object.to_string().as_string().unwrap_throw())
})
}
@@ -0,0 +1,146 @@
use std::marker::PhantomData;
use wasm_bindgen::{throw_val, JsValue};
use crate::util::promise_to_void_future;
use super::{sys, IntoAsyncWrite, IntoSink, WritableStream};
/// A [`WritableStreamDefaultWriter`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStreamDefaultWriter)
/// that can be used to write chunks to a [`WritableStream`](WritableStream).
///
/// This is returned by the [`get_writer`](WritableStream::get_writer) method.
///
/// When the writer is dropped, it automatically [releases its lock](https://streams.spec.whatwg.org/#release-a-lock).
#[derive(Debug)]
pub struct WritableStreamDefaultWriter<'stream> {
raw: sys::WritableStreamDefaultWriter,
_stream: PhantomData<&'stream mut WritableStream>,
}
impl<'stream> WritableStreamDefaultWriter<'stream> {
pub(crate) fn new(stream: &mut WritableStream) -> Result<Self, js_sys::Error> {
Ok(Self {
raw: stream.as_raw().get_writer()?,
_stream: PhantomData,
})
}
/// Acquires a reference to the underlying [JavaScript writer](sys::WritableStreamDefaultWriter).
#[inline]
pub fn as_raw(&self) -> &sys::WritableStreamDefaultWriter {
&self.raw
}
/// Waits for the stream to become closed.
///
/// This returns an error if the stream ever errors, or if the writer's lock is
/// [released](https://streams.spec.whatwg.org/#release-a-lock) before the stream finishes
/// closing.
pub async fn closed(&self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().closed()).await
}
/// Returns the desired size to fill the stream's internal queue.
///
/// * It can be negative, if the queue is over-full.
/// A producer can use this information to determine the right amount of data to write.
/// * It will be `None` if the stream cannot be successfully written to
/// (due to either being errored, or having an abort queued up).
/// * It will return zero if the stream is closed.
#[inline]
pub fn desired_size(&self) -> Option<f64> {
self.as_raw()
.desired_size()
.unwrap_or_else(|error| throw_val(error))
}
/// Waits until the desired size to fill the stream's internal queue transitions
/// from non-positive to positive, signaling that it is no longer applying backpressure.
///
/// Once the desired size to fill the stream's internal queue dips back to zero or below,
/// this will return a new future that stays pending until the next transition.
///
/// This returns an error if the stream ever errors, or if the writer's lock is
/// [released](https://streams.spec.whatwg.org/#release-a-lock) before the stream finishes
/// closing.
pub async fn ready(&self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().ready()).await
}
/// [Aborts](https://streams.spec.whatwg.org/#abort-a-writable-stream) the stream,
/// signaling that the producer can no longer successfully write to the stream.
///
/// Equivalent to [`WritableStream.abort`](WritableStream::abort).
pub async fn abort(&mut self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().abort()).await
}
/// [Aborts](https://streams.spec.whatwg.org/#abort-a-writable-stream) the stream with the
/// given `reason`, signaling that the producer can no longer successfully write to the stream.
///
/// Equivalent to [`WritableStream.abort_with_reason`](WritableStream::abort_with_reason).
pub async fn abort_with_reason(&mut self, reason: &JsValue) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().abort_with_reason(reason)).await
}
/// Writes the given `chunk` to the writable stream, by waiting until any previous writes
/// have finished successfully, and then sending the chunk to the underlying sink's `write()`
/// method.
///
/// This returns `Ok(())` upon a successful write, or `Err(error)` if the write fails or stream
/// becomes errored before the writing process is initiated.
///
/// Note that what "success" means is up to the underlying sink; it might indicate simply
/// that the chunk has been accepted, and not necessarily that it is safely saved to
/// its ultimate destination.
pub async fn write(&mut self, chunk: JsValue) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().write_with_chunk(&chunk)).await
}
/// Closes the stream.
///
/// The underlying sink will finish processing any previously-written chunks, before invoking
/// its close behavior. During this time any further attempts to write will fail
/// (without erroring the stream).
///
/// This returns `Ok(())` if all remaining chunks are successfully written and the stream
/// successfully closes, or `Err(error)` if an error is encountered during this process.
pub async fn close(&mut self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().close()).await
}
/// Converts this `WritableStreamDefaultWriter` into a [`Sink`].
///
/// This is similar to [`WritableStream.into_sink`](WritableStream::into_sink),
/// except that after the returned `Sink` is dropped, the original `WritableStream` is still
/// usable. This allows writing only a few chunks through the `Sink`, while still allowing
/// another writer to write more chunks later on.
///
/// [`Sink`]: https://docs.rs/futures/0.3.30/futures/sink/trait.Sink.html
#[inline]
pub fn into_sink(self) -> IntoSink<'stream> {
IntoSink::new(self)
}
/// Converts this `WritableStreamDefaultWriter` into an [`AsyncWrite`].
///
/// The writable stream must accept [`Uint8Array`](js_sys::Uint8Array) chunks.
///
/// This is similar to [`WritableStream.into_async_write`](WritableStream::into_async_write),
/// except that after the returned `AsyncWrite` is dropped, the original `WritableStream` is
/// still usable. This allows writing only a few bytes through the `AsyncWrite`, while still
/// allowing another writer to write more bytes later on.
///
/// [`AsyncWrite`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncWrite.html
#[inline]
pub fn into_async_write(self) -> IntoAsyncWrite<'stream> {
IntoAsyncWrite::new(self.into_sink())
}
}
impl Drop for WritableStreamDefaultWriter<'_> {
fn drop(&mut self) {
self.as_raw().release_lock()
}
}
@@ -0,0 +1,77 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use futures_util::io::AsyncWrite;
use futures_util::ready;
use futures_util::sink::SinkExt;
use js_sys::Uint8Array;
use wasm_bindgen::JsValue;
use crate::util::js_to_io_error;
use super::IntoSink;
/// An [`AsyncWrite`] for the [`into_async_write`](super::WritableStream::into_async_write) method.
///
/// This `AsyncWrite` holds a writer, and therefore locks the [`WritableStream`](super::WritableStream).
/// When this `AsyncWrite` is dropped, it also drops its writer which in turn
/// [releases its lock](https://streams.spec.whatwg.org/#release-a-lock).
///
/// [`AsyncWrite`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncWrite.html
#[must_use = "writers do nothing unless polled"]
#[derive(Debug)]
pub struct IntoAsyncWrite<'writer> {
sink: IntoSink<'writer>,
}
impl<'writer> IntoAsyncWrite<'writer> {
#[inline]
pub(super) fn new(sink: IntoSink<'writer>) -> Self {
Self { sink }
}
/// [Aborts](https://streams.spec.whatwg.org/#abort-a-writable-stream) the stream,
/// signaling that the producer can no longer successfully write to the stream.
pub async fn abort(self) -> Result<(), JsValue> {
self.sink.abort().await
}
/// [Aborts](https://streams.spec.whatwg.org/#abort-a-writable-stream) the stream,
/// signaling that the producer can no longer successfully write to the stream.
pub async fn abort_with_reason(self, reason: &JsValue) -> Result<(), JsValue> {
self.sink.abort_with_reason(reason).await
}
}
impl<'writer> AsyncWrite for IntoAsyncWrite<'writer> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
ready!(self
.as_mut()
.sink
.poll_ready_unpin(cx)
.map_err(js_to_io_error))?;
self.as_mut()
.sink
.start_send_unpin(Uint8Array::from(buf).into())
.map_err(js_to_io_error)?;
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
self.as_mut()
.sink
.poll_flush_unpin(cx)
.map_err(js_to_io_error)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
self.as_mut()
.sink
.poll_close_unpin(cx)
.map_err(js_to_io_error)
}
}
@@ -0,0 +1,182 @@
use core::pin::Pin;
use core::task::{Context, Poll};
use futures_util::Sink;
use futures_util::{ready, FutureExt};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use super::WritableStreamDefaultWriter;
/// A [`Sink`] for the [`into_sink`](super::WritableStream::into_sink) method.
///
/// This sink holds a writer, and therefore locks the [`WritableStream`](super::WritableStream).
/// When this sink is dropped, it also drops its writer which in turn
/// [releases its lock](https://streams.spec.whatwg.org/#release-a-lock).
///
/// [`Sink`]: https://docs.rs/futures/0.3.30/futures/sink/trait.Sink.html
#[must_use = "sinks do nothing unless polled"]
#[derive(Debug)]
pub struct IntoSink<'writer> {
writer: Option<WritableStreamDefaultWriter<'writer>>,
/// If an error occurred, this holds the error to return on subsequent operations.
error: Option<JsValue>,
ready_fut: Option<JsFuture>,
write_fut: Option<JsFuture>,
close_fut: Option<JsFuture>,
}
impl<'writer> IntoSink<'writer> {
#[inline]
pub(super) fn new(writer: WritableStreamDefaultWriter) -> IntoSink {
IntoSink {
writer: Some(writer),
error: None,
ready_fut: None,
write_fut: None,
close_fut: None,
}
}
/// [Aborts](https://streams.spec.whatwg.org/#abort-a-writable-stream) the stream,
/// signaling that the producer can no longer successfully write to the stream.
pub async fn abort(mut self) -> Result<(), JsValue> {
match self.writer.take() {
Some(mut writer) => writer.abort().await,
None => Ok(()),
}
}
/// [Aborts](https://streams.spec.whatwg.org/#abort-a-writable-stream) the stream,
/// signaling that the producer can no longer successfully write to the stream.
pub async fn abort_with_reason(mut self, reason: &JsValue) -> Result<(), JsValue> {
match self.writer.take() {
Some(mut writer) => writer.abort_with_reason(reason).await,
None => Ok(()),
}
}
/// Returns the stored error, or a default "sink is closed" error.
fn get_error(&self) -> JsValue {
self.error
.clone()
.unwrap_or_else(|| JsValue::from_str("WritableStream sink is already closed"))
}
}
impl<'writer> Sink<JsValue> for IntoSink<'writer> {
type Error = JsValue;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let ready_fut = match self.ready_fut.as_mut() {
Some(fut) => fut,
None => match &self.writer {
Some(writer) => {
// No pending ready future yet, create one from ready promise
let fut = JsFuture::from(writer.as_raw().ready());
self.ready_fut.insert(fut)
}
None => {
// Writer was already dropped due to error or close
return Poll::Ready(Err(self.get_error()));
}
},
};
// Poll the ready future
let js_result = ready!(ready_fut.poll_unpin(cx));
self.ready_fut = None;
// Ready future completed
Poll::Ready(match js_result {
Ok(js_value) => {
debug_assert!(js_value.is_undefined());
Ok(())
}
Err(js_value) => {
// Error, store it and drop writer
self.error = Some(js_value.clone());
self.writer = None;
Err(js_value)
}
})
}
fn start_send(mut self: Pin<&mut Self>, item: JsValue) -> Result<(), Self::Error> {
match &self.writer {
Some(writer) => {
let fut = JsFuture::from(writer.as_raw().write_with_chunk(&item));
// Set or replace the pending write future
self.write_fut = Some(fut);
Ok(())
}
None => {
// Writer was already dropped due to error or close
Err(self.get_error())
}
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let write_fut = match self.write_fut.as_mut() {
Some(fut) => fut,
None => {
// If we're not writing, then there's nothing to flush
return Poll::Ready(Ok(()));
}
};
// Poll the write future
let js_result = ready!(write_fut.poll_unpin(cx));
self.write_fut = None;
// Write future completed
Poll::Ready(match js_result {
Ok(js_value) => {
debug_assert!(js_value.is_undefined());
Ok(())
}
Err(js_value) => {
// Error, store it and drop writer
self.error = Some(js_value.clone());
self.writer = None;
Err(js_value)
}
})
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let close_fut = match self.close_fut.as_mut() {
Some(fut) => fut,
None => match &self.writer {
Some(writer) => {
// No pending close future
// Start closing the stream and create future from close promise
let fut = JsFuture::from(writer.as_raw().close());
self.close_fut.insert(fut)
}
None => {
// Writer was already dropped due to error or close
return Poll::Ready(Err(self.get_error()));
}
},
};
// Poll the close future
let js_result = ready!(close_fut.poll_unpin(cx));
self.close_fut = None;
// Close future completed
self.writer = None;
Poll::Ready(match js_result {
Ok(js_value) => {
debug_assert!(js_value.is_undefined());
Ok(())
}
Err(js_value) => {
self.error = Some(js_value.clone());
Err(js_value)
}
})
}
}
@@ -0,0 +1,99 @@
use std::cell::RefCell;
use std::panic::AssertUnwindSafe;
use std::pin::Pin;
use std::rc::Rc;
use futures_util::{Sink, SinkExt};
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;
#[wasm_bindgen]
pub(crate) struct IntoUnderlyingSink {
inner: Rc<RefCell<Inner>>,
}
impl IntoUnderlyingSink {
pub fn new(sink: Box<dyn Sink<JsValue, Error = JsValue>>) -> Self {
IntoUnderlyingSink {
inner: Rc::new(RefCell::new(Inner::new(sink))),
}
}
}
#[allow(clippy::await_holding_refcell_ref)]
#[wasm_bindgen]
impl IntoUnderlyingSink {
pub fn write(&mut self, chunk: JsValue) -> Promise {
let inner = self.inner.clone();
// SAFETY: We use the take-and-replace pattern in Inner::write() to ensure
// that if a panic occurs, the sink is already taken out of the Option,
// leaving it in a clean None state. This prevents use of corrupted state
// after a panic is caught.
future_to_promise(AssertUnwindSafe(async move {
// This mutable borrow can never panic, since the WritableStream always queues
// each operation on the underlying sink.
let mut inner = inner.try_borrow_mut().unwrap_throw();
inner.write(chunk).await.map(|_| JsValue::undefined())
}))
}
pub fn close(self) -> Promise {
// SAFETY: Inner::close() takes the sink before the fallible operation.
future_to_promise(AssertUnwindSafe(async move {
let mut inner = self.inner.try_borrow_mut().unwrap_throw();
inner.close().await.map(|_| JsValue::undefined())
}))
}
pub fn abort(self, reason: JsValue) -> Promise {
// SAFETY: Inner::abort() just sets sink to None, no fallible operation.
future_to_promise(AssertUnwindSafe(async move {
let mut inner = self.inner.try_borrow_mut().unwrap_throw();
inner.abort(reason).await.map(|_| JsValue::undefined())
}))
}
}
struct Inner {
sink: Option<Pin<Box<dyn Sink<JsValue, Error = JsValue>>>>,
}
impl Inner {
fn new(sink: Box<dyn Sink<JsValue, Error = JsValue>>) -> Self {
Inner {
sink: Some(sink.into()),
}
}
async fn write(&mut self, chunk: JsValue) -> Result<(), JsValue> {
// Take the sink out before the fallible/panickable operation.
// This ensures that if a panic occurs, self.sink is already None,
// so any subsequent call will fail cleanly instead of using corrupted state.
let mut sink = self.sink.take().unwrap_throw();
match sink.send(chunk).await {
Ok(()) => {
// Success: put the sink back for reuse
self.sink = Some(sink);
Ok(())
}
Err(err) => {
// Error: the sink is dropped, self.sink remains None
Err(err)
}
}
// Panic: sink is dropped during unwind, self.sink remains None
}
async fn close(&mut self) -> Result<(), JsValue> {
// Take ownership and close - sink is dropped after close completes
self.sink.take().unwrap_throw().close().await
}
async fn abort(&mut self, _reason: JsValue) -> Result<(), JsValue> {
// Take and drop the sink immediately
self.sink = None;
Ok(())
}
}
+190
View File
@@ -0,0 +1,190 @@
//! Bindings and conversions for
//! [writable streams](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
use futures_util::Sink;
use wasm_bindgen::prelude::*;
pub use default_writer::WritableStreamDefaultWriter;
pub use into_async_write::IntoAsyncWrite;
pub use into_sink::IntoSink;
use into_underlying_sink::IntoUnderlyingSink;
use crate::util::promise_to_void_future;
mod default_writer;
mod into_async_write;
mod into_sink;
mod into_underlying_sink;
pub mod sys;
/// A [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
///
/// `WritableStream`s can be created from a [raw JavaScript stream](sys::WritableStream) with
/// [`from_raw`](Self::from_raw), or from a Rust [`Sink`] with [`from_sink`](Self::from_sink).
///
/// They can be converted into a [raw JavaScript stream](sys::WritableStream) with
/// [`into_raw`](Self::into_raw), or into a Rust [`Sink`] with [`into_sink`](Self::into_sink).
///
/// [`Sink`]: https://docs.rs/futures/0.3.30/futures/sink/trait.Sink.html
#[derive(Debug)]
pub struct WritableStream {
raw: sys::WritableStream,
}
impl WritableStream {
/// Creates a new `WritableStream` from a [JavaScript stream](sys::WritableStream).
#[inline]
pub fn from_raw(raw: sys::WritableStream) -> Self {
Self { raw }
}
/// Creates a new `WritableStream` from a [`Sink`].
///
/// Items and errors must be represented as raw [`JsValue`]s.
/// Use [`with`] and/or [`sink_map_err`] to convert a sink's items to a `JsValue`
/// before passing it to this function.
///
/// [`Sink`]: https://docs.rs/futures/0.3.30/futures/sink/trait.Sink.html
/// [`with`]: https://docs.rs/futures/0.3.30/futures/sink/trait.SinkExt.html#method.with
/// [`sink_map_err`]: https://docs.rs/futures/0.3.30/futures/sink/trait.SinkExt.html#method.sink_map_err
pub fn from_sink<Si>(sink: Si) -> Self
where
Si: Sink<JsValue, Error = JsValue> + 'static,
{
let sink = IntoUnderlyingSink::new(Box::new(sink));
// Use the default queuing strategy (with a HWM of 1 chunk).
// We shouldn't set HWM to 0, since that would break piping to the writable stream.
let raw = sys::WritableStreamExt::new_with_into_underlying_sink(sink).unchecked_into();
Self::from_raw(raw)
}
/// Acquires a reference to the underlying [JavaScript stream](sys::WritableStream).
#[inline]
pub fn as_raw(&self) -> &sys::WritableStream {
&self.raw
}
/// Consumes this `WritableStream`, returning the underlying [JavaScript stream](sys::WritableStream).
#[inline]
pub fn into_raw(self) -> sys::WritableStream {
self.raw
}
/// Returns `true` if the stream is [locked to a writer](https://streams.spec.whatwg.org/#lock).
#[inline]
pub fn is_locked(&self) -> bool {
self.as_raw().locked()
}
/// [Aborts](https://streams.spec.whatwg.org/#abort-a-writable-stream) the stream,
/// signaling that the producer can no longer successfully write to the stream
/// and it is to be immediately moved to an errored state, with any queued-up writes discarded.
///
/// If the stream is currently locked to a writer, then this returns an error.
pub async fn abort(&mut self) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().abort()).await
}
/// [Aborts](https://streams.spec.whatwg.org/#abort-a-writable-stream) the stream with the
/// given `reason`, signaling that the producer can no longer successfully write to the stream
/// and it is to be immediately moved to an errored state, with any queued-up writes discarded.
///
/// If the stream is currently locked to a writer, then this returns an error.
pub async fn abort_with_reason(&mut self, reason: &JsValue) -> Result<(), JsValue> {
promise_to_void_future(self.as_raw().abort_with_reason(reason)).await
}
/// Creates a [writer](WritableStreamDefaultWriter) and
/// [locks](https://streams.spec.whatwg.org/#lock) the stream to the new writer.
///
/// While the stream is locked, no other writer can be acquired until this one is released.
///
/// **Panics** if the stream is already locked to a writer. For a non-panicking variant,
/// use [`try_get_writer`](Self::try_get_writer).
#[inline]
pub fn get_writer(&mut self) -> WritableStreamDefaultWriter<'_> {
self.try_get_writer()
.expect_throw("already locked to a writer")
}
/// Try to create a [writer](WritableStreamDefaultWriter) and
/// [lock](https://streams.spec.whatwg.org/#lock) the stream to the new writer.
///
/// While the stream is locked, no other writer can be acquired until this one is released.
///
/// If the stream is already locked to a writer, then this returns an error.
pub fn try_get_writer(&mut self) -> Result<WritableStreamDefaultWriter<'_>, js_sys::Error> {
WritableStreamDefaultWriter::new(self)
}
/// Converts this `WritableStream` into a [`Sink`].
///
/// Items and errors are represented by their raw [`JsValue`].
/// Use [`with`] and/or [`sink_map_err`] on the returned stream to convert them to a more
/// appropriate type.
///
/// **Panics** if the stream is already locked to a writer. For a non-panicking variant,
/// use [`try_into_sink`](Self::try_into_sink).
///
/// [`Sink`]: https://docs.rs/futures/0.3.30/futures/sink/trait.Sink.html
/// [`with`]: https://docs.rs/futures/0.3.30/futures/sink/trait.SinkExt.html#method.with
/// [`sink_map_err`]: https://docs.rs/futures/0.3.30/futures/sink/trait.SinkExt.html#method.sink_map_err
#[inline]
pub fn into_sink(self) -> IntoSink<'static> {
self.try_into_sink()
.expect_throw("already locked to a writer")
}
/// Try to convert this `WritableStream` into a [`Sink`].
///
/// Items and errors are represented by their raw [`JsValue`].
/// Use [`with`] and/or [`sink_map_err`] on the returned stream to convert them to a more
/// appropriate type.
///
/// If the stream is already locked to a writer, then this returns an error
/// along with the original `WritableStream`.
///
/// [`Sink`]: https://docs.rs/futures/0.3.30/futures/sink/trait.Sink.html
/// [`with`]: https://docs.rs/futures/0.3.30/futures/sink/trait.SinkExt.html#method.with
/// [`sink_map_err`]: https://docs.rs/futures/0.3.30/futures/sink/trait.SinkExt.html#method.sink_map_err
pub fn try_into_sink(mut self) -> Result<IntoSink<'static>, (js_sys::Error, Self)> {
let writer = WritableStreamDefaultWriter::new(&mut self).map_err(|err| (err, self))?;
Ok(writer.into_sink())
}
/// Converts this `WritableStream` into an [`AsyncWrite`].
///
/// The writable stream must accept [`Uint8Array`](js_sys::Uint8Array) chunks.
///
/// **Panics** if the stream is already locked to a writer. For a non-panicking variant,
/// use [`try_into_async_write`](Self::try_into_async_write).
///
/// [`AsyncWrite`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncWrite.html
pub fn into_async_write(self) -> IntoAsyncWrite<'static> {
self.try_into_async_write()
.expect_throw("already locked to a writer")
}
/// Try to convert this `WritableStream` into an [`AsyncWrite`].
///
/// The writable stream must accept [`Uint8Array`](js_sys::Uint8Array) chunks.
///
/// If the stream is already locked to a writer, then this returns an error
/// along with the original `WritableStream`.
///
/// [`AsyncWrite`]: https://docs.rs/futures/0.3.30/futures/io/trait.AsyncWrite.html
pub fn try_into_async_write(self) -> Result<IntoAsyncWrite<'static>, (js_sys::Error, Self)> {
Ok(IntoAsyncWrite::new(self.try_into_sink()?))
}
}
impl<Si> From<Si> for WritableStream
where
Si: Sink<JsValue, Error = JsValue> + 'static,
{
/// Equivalent to [`from_sink`](Self::from_sink).
#[inline]
fn from(sink: Si) -> Self {
Self::from_sink(sink)
}
}
+19
View File
@@ -0,0 +1,19 @@
//! Raw bindings to JavaScript objects used
//! by a [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
//! These are re-exported from [web-sys](https://docs.rs/web-sys/0.3.70/web_sys/struct.WritableStream.html).
use wasm_bindgen::prelude::*;
pub use web_sys::WritableStream;
pub use web_sys::WritableStreamDefaultWriter;
use crate::writable::into_underlying_sink::IntoUnderlyingSink;
#[wasm_bindgen]
extern "C" {
/// A raw [`WritableStream`](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream).
#[wasm_bindgen(js_name = WritableStream, typescript_type = "WritableStream")]
#[derive(Clone, Debug)]
pub(crate) type WritableStreamExt;
#[wasm_bindgen(constructor, js_class = WritableStream)]
pub(crate) fn new_with_into_underlying_sink(sink: IntoUnderlyingSink) -> WritableStreamExt;
}
+7
View File
@@ -0,0 +1,7 @@
pub use readable_stream::*;
pub use transform_stream::*;
pub use writable_stream::*;
mod readable_stream;
mod transform_stream;
mod writable_stream;
@@ -0,0 +1,4 @@
{
"type": "module",
"private": true
}
@@ -0,0 +1,81 @@
export function new_noop_readable_stream() {
return new ReadableStream();
}
export function new_noop_readable_byte_stream() {
return new ReadableStream({
type: 'bytes',
start(controller) {
this.controller = controller;
},
cancel() {
const byobRequest = this.controller.byobRequest;
if (byobRequest) {
byobRequest.respond(0);
}
}
});
}
export function new_readable_stream_from_array(chunks) {
return new ReadableStream({
start(controller) {
for (let chunk of chunks) {
controller.enqueue(chunk);
}
controller.close();
}
});
}
export function new_readable_byte_stream_from_array(chunks) {
return new ReadableStream({
type: 'bytes',
start(controller) {
this.controller = controller;
for (let chunk of chunks) {
controller.enqueue(chunk);
}
controller.close();
},
cancel() {
const byobRequest = this.controller.byobRequest;
if (byobRequest) {
byobRequest.respond(0);
}
}
});
}
export function new_readable_stream_with_rejecting_cancel() {
return new ReadableStream({
cancel(reason) {
return Promise.reject('error from cancel');
}
});
}
export function new_readable_byte_stream_with_rejecting_cancel() {
return new ReadableStream({
type: 'bytes',
cancel(reason) {
return Promise.reject('error from cancel');
}
});
}
/**
* Tests whether `reader.releaseLock()` is allowed while there are pending read requests.
*
* See: https://github.com/whatwg/streams/commit/d5f92d9f17306d31ba6b27424d23d58e89bf64a5
*/
export function supports_release_lock_with_pending_read() {
try {
const reader = new ReadableStream().getReader();
reader.read().then(() => {}, () => {});
reader.releaseLock();
return true;
} catch {
return false;
}
}
@@ -0,0 +1,14 @@
use wasm_bindgen::prelude::*;
use wasm_streams::readable::*;
#[wasm_bindgen(module = "/tests/js/readable_stream.js")]
extern "C" {
pub fn new_noop_readable_stream() -> sys::ReadableStream;
pub fn new_noop_readable_byte_stream() -> sys::ReadableStream;
pub fn new_readable_stream_from_array(chunks: Box<[JsValue]>) -> sys::ReadableStream;
pub fn new_readable_byte_stream_from_array(chunks: Box<[JsValue]>) -> sys::ReadableStream;
pub fn new_readable_stream_with_rejecting_cancel() -> sys::ReadableStream;
pub fn new_readable_byte_stream_with_rejecting_cancel() -> sys::ReadableStream;
pub fn supports_release_lock_with_pending_read() -> bool;
}
@@ -0,0 +1,11 @@
export function new_noop_transform_stream() {
return new TransformStream();
}
export function new_uppercase_transform_stream() {
return new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
});
}
@@ -0,0 +1,9 @@
use wasm_bindgen::prelude::*;
use wasm_streams::transform::*;
#[wasm_bindgen(module = "/tests/js/transform_stream.js")]
extern "C" {
pub fn new_noop_transform_stream() -> sys::TransformStream;
pub fn new_uppercase_transform_stream() -> sys::TransformStream;
}
@@ -0,0 +1,23 @@
export function new_noop_writable_stream() {
return new WritableStream();
}
const TYPE_WRITE = 0;
const TYPE_CLOSE = 1;
const TYPE_ABORT = 2;
export function new_recording_writable_stream() {
const events = [];
const stream = new WritableStream({
write(chunk) {
events.push({type: TYPE_WRITE, chunk});
},
close() {
events.push({type: TYPE_CLOSE});
},
abort(reason) {
events.push({type: TYPE_ABORT, reason});
}
});
return {stream, events};
}
@@ -0,0 +1,119 @@
use std::fmt::{Debug, Formatter};
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_streams::writable::*;
#[wasm_bindgen(module = "/tests/js/writable_stream.js")]
extern "C" {
pub fn new_noop_writable_stream() -> sys::WritableStream;
fn new_recording_writable_stream() -> WritableStreamAndEvents;
#[derive(Clone, Debug)]
type WritableStreamAndEvents;
#[wasm_bindgen(method, getter)]
fn stream(this: &WritableStreamAndEvents) -> sys::WritableStream;
#[wasm_bindgen(method, getter)]
fn events(this: &WritableStreamAndEvents) -> Box<[JsValue]>;
#[derive(Clone, Debug)]
type JsRecordedEvent;
#[wasm_bindgen(method, getter, js_name = "type")]
fn type_(this: &JsRecordedEvent) -> u8;
#[wasm_bindgen(method, getter)]
fn chunk(this: &JsRecordedEvent) -> JsValue;
#[wasm_bindgen(method, getter)]
fn reason(this: &JsRecordedEvent) -> JsValue;
}
pub struct RecordingWritableStream {
raw: WritableStreamAndEvents,
}
impl RecordingWritableStream {
pub fn new() -> Self {
Self {
raw: new_recording_writable_stream(),
}
}
pub fn stream(&self) -> sys::WritableStream {
self.raw.stream()
}
pub fn events(&self) -> Vec<RecordedEvent> {
self.raw
.events()
.iter()
.map(|x| RecordedEvent::from(x.unchecked_ref::<JsRecordedEvent>()))
.collect::<Vec<_>>()
}
}
pub enum RecordedEvent {
Write(JsValue),
Close,
Abort(JsValue),
}
impl From<&JsRecordedEvent> for RecordedEvent {
fn from(js_event: &JsRecordedEvent) -> Self {
match js_event.type_() {
0 => RecordedEvent::Write(js_event.chunk()),
1 => RecordedEvent::Close,
2 => RecordedEvent::Abort(js_event.reason()),
event_type => panic!("unknown event type: {}", event_type),
}
}
}
impl PartialEq for RecordedEvent {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(RecordedEvent::Write(left_val), RecordedEvent::Write(right_val)) => {
if left_val.eq(right_val) {
true
} else {
equal_uint8_array(left_val, right_val)
}
}
(RecordedEvent::Close, RecordedEvent::Close) => true,
(RecordedEvent::Abort(left_val), RecordedEvent::Abort(right_val)) => {
left_val.eq(right_val)
}
_ => false,
}
}
}
fn equal_uint8_array(left: &JsValue, right: &JsValue) -> bool {
match (left.dyn_ref::<Uint8Array>(), right.dyn_ref::<Uint8Array>()) {
(Some(left_array), Some(right_array)) => left_array.to_vec().eq(&right_array.to_vec()),
_ => false,
}
}
impl Debug for RecordedEvent {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RecordedEvent::Write(value) => {
let mut tuple = f.debug_tuple("Write");
if let Some(array) = value.dyn_ref::<Uint8Array>() {
tuple.field(&array.to_vec())
} else {
tuple.field(value)
};
tuple.finish()
}
RecordedEvent::Close => f.debug_tuple("Close").finish(),
RecordedEvent::Abort(value) => f.debug_tuple("Abort").field(value).finish(),
}
}
}
@@ -0,0 +1,37 @@
use futures_util::{AsyncReadExt, TryStreamExt};
use js_sys::{global, Uint8Array};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use wasm_bindgen_test::*;
use wasm_streams::ReadableStream;
use web_sys::{Response, Window};
#[wasm_bindgen_test]
async fn test_fetch_as_stream() {
// Make a fetch request
let url = "https://drager.github.io/wasm-pack/public/img/wasm-ferris.png";
let window = global().unchecked_into::<Window>(); // hack to also support Node.js
let resp_value = JsFuture::from(window.fetch_with_str(url))
.await
.unwrap_throw();
let resp: Response = resp_value.dyn_into().unwrap_throw();
// Get the response's body as a JS ReadableStream
let raw_body = resp.body().unwrap_throw();
let body = ReadableStream::from_raw(raw_body);
// Convert the JS ReadableStream to a Rust stream
let stream = body.into_stream();
// Consume to an AsyncRead
let mut async_read = stream
.map_ok(|js_value| js_value.dyn_into::<Uint8Array>().unwrap_throw().to_vec())
.map_err(|_js_error| std::io::Error::new(std::io::ErrorKind::Other, "failed to read"))
.into_async_read();
// Read the first 4 bytes
let mut buf = [0u8; 4];
assert_eq!(async_read.read(&mut buf).await.unwrap_throw(), 4);
assert_eq!(&buf, b"\x89PNG");
}
+6
View File
@@ -0,0 +1,6 @@
mod fetch_as_stream;
mod pipe;
mod readable_byte_stream;
mod readable_stream;
mod transform_stream;
mod writable_stream;
+85
View File
@@ -0,0 +1,85 @@
use futures_util::stream::iter;
use futures_util::{SinkExt, StreamExt};
use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*;
use wasm_streams::readable::*;
use wasm_streams::writable::*;
use crate::js::*;
use crate::util::*;
#[wasm_bindgen_test]
async fn test_pipe_js_to_rust() {
let chunks = vec![JsValue::from("Hello"), JsValue::from("world!")];
let mut readable = ReadableStream::from_raw(new_readable_stream_from_array(
chunks.clone().into_boxed_slice(),
));
let (sink, stream) = SimpleChannel::<JsValue>::new().split();
let sink = sink.sink_map_err(|_| JsValue::from_str("cannot happen"));
let mut writable = WritableStream::from_sink(sink);
readable.pipe_to(&mut writable).await.unwrap();
// All chunks must be sent to sink
let output = stream.collect::<Vec<_>>().await;
assert_eq!(output, chunks);
// Both streams must be closed
readable.get_reader().closed().await.unwrap();
writable.get_writer().closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_pipe_rust_to_js() {
let stream = iter(vec!["Hello", "world!"]).map(|s| Ok(JsValue::from(s)));
let mut readable = ReadableStream::from_stream(stream);
let recording_stream = RecordingWritableStream::new();
let mut writable = WritableStream::from_raw(recording_stream.stream());
readable.pipe_to(&mut writable).await.unwrap();
// All chunks must be sent to sink
assert_eq!(
recording_stream.events(),
[
RecordedEvent::Write(JsValue::from("Hello")),
RecordedEvent::Write(JsValue::from("world!")),
RecordedEvent::Close
]
);
// Both streams must be closed
readable.get_reader().closed().await.unwrap();
writable.get_writer().closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_pipe_prevent_close() {
let chunks = vec![JsValue::from("Hello"), JsValue::from("world!")];
let mut readable = ReadableStream::from_raw(new_readable_stream_from_array(
chunks.clone().into_boxed_slice(),
));
let recording_stream = RecordingWritableStream::new();
let mut writable = WritableStream::from_raw(recording_stream.stream());
readable
.pipe_to_with_options(&mut writable, PipeOptions::new().prevent_close(true))
.await
.unwrap();
// All chunks must be sent to sink, without closing it
assert_eq!(
recording_stream.events(),
[
RecordedEvent::Write(JsValue::from("Hello")),
RecordedEvent::Write(JsValue::from("world!"))
]
);
// Readable stream must be closed
readable.get_reader().closed().await.unwrap();
}
@@ -0,0 +1,340 @@
use std::pin::Pin;
use std::task::Poll;
use std::time::Duration;
use futures_util::AsyncReadExt;
use futures_util::{poll, FutureExt};
use gloo_timers::future::sleep;
use js_sys::Uint8Array;
use wasm_bindgen_test::*;
use wasm_streams::readable::*;
use crate::js::*;
use crate::util::*;
#[wasm_bindgen_test]
async fn test_readable_byte_stream_new() {
let mut readable = ReadableStream::from_raw(new_readable_byte_stream_from_array(
vec![
Uint8Array::from(&[1, 2, 3][..]).into(),
Uint8Array::from(&[4, 5, 6][..]).into(),
]
.into_boxed_slice(),
));
assert!(!readable.is_locked());
let mut reader = readable.get_byob_reader();
let mut dst = [0u8; 3];
assert_eq!(reader.read(&mut dst).await.unwrap(), 3);
assert_eq!(&dst, &[1, 2, 3]);
assert_eq!(reader.read(&mut dst).await.unwrap(), 3);
assert_eq!(&dst, &[4, 5, 6]);
assert_eq!(reader.read(&mut dst).await.unwrap(), 0);
assert_eq!(&dst, &[4, 5, 6]);
reader.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_read_with_buffer() {
let mut readable = ReadableStream::from_raw(new_readable_byte_stream_from_array(
vec![
Uint8Array::from(&[1, 2, 3][..]).into(),
Uint8Array::from(&[4, 5, 6][..]).into(),
]
.into_boxed_slice(),
));
assert!(!readable.is_locked());
let mut reader = readable.get_byob_reader();
let mut dst = [0u8; 3];
let buf = Uint8Array::new_with_length(3);
let (bytes_read, buf) = reader.read_with_buffer(&mut dst, buf).await.unwrap();
assert_eq!(bytes_read, 3);
assert_eq!(&dst, &[1, 2, 3]);
let (bytes_read, buf) = reader
.read_with_buffer(&mut dst, buf.unwrap())
.await
.unwrap();
assert_eq!(bytes_read, 3);
assert_eq!(&dst, &[4, 5, 6]);
let (bytes_read, buf) = reader
.read_with_buffer(&mut dst, buf.unwrap())
.await
.unwrap();
assert_eq!(bytes_read, 0);
assert_eq!(&dst, &[4, 5, 6]);
drop(buf);
reader.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_into_async_read() {
let readable = ReadableStream::from_raw(new_readable_byte_stream_from_array(
vec![
Uint8Array::from(&[1, 2, 3][..]).into(),
Uint8Array::from(&[4, 5, 6][..]).into(),
]
.into_boxed_slice(),
));
assert!(!readable.is_locked());
let mut async_read = readable.into_async_read();
let mut buf = [0u8; 3];
assert_eq!(async_read.read(&mut buf).await.unwrap(), 3);
assert_eq!(&buf, &[1, 2, 3]);
assert_eq!(async_read.read(&mut buf[..1]).await.unwrap(), 1);
assert_eq!(&buf, &[4, 2, 3]);
assert_eq!(async_read.read(&mut buf[1..]).await.unwrap(), 2);
assert_eq!(&buf, &[4, 5, 6]);
assert_eq!(async_read.read(&mut buf).await.unwrap(), 0);
assert_eq!(&buf, &[4, 5, 6]);
}
#[wasm_bindgen_test]
fn test_readable_byte_stream_into_async_read_impl_unpin() {
let readable = ReadableStream::from_raw(new_noop_readable_byte_stream());
let async_read = readable.into_async_read();
let _ = Pin::new(&async_read); // must be Unpin for this to work
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_byob_reader_into_async_read() {
let mut readable = ReadableStream::from_raw(new_readable_byte_stream_from_array(
vec![
Uint8Array::from(&[1, 2, 3][..]).into(),
Uint8Array::from(&[4, 5, 6][..]).into(),
]
.into_boxed_slice(),
));
assert!(!readable.is_locked());
{
// Acquire a BYOB reader and wrap it in a Rust Stream
let reader = readable.get_byob_reader();
let mut async_read = reader.into_async_read();
let mut buf = [0u8; 3];
assert_eq!(async_read.read(&mut buf).await.unwrap(), 3);
assert_eq!(&buf, &[1, 2, 3]);
}
// Dropping the wrapped Stream should release the lock
assert!(!readable.is_locked());
{
// Can acquire a new reader after wrapped Stream is dropped
let mut reader = readable.get_byob_reader();
let mut buf = [0u8; 3];
assert_eq!(reader.read(&mut buf).await.unwrap(), 3);
assert_eq!(&buf, &[4, 5, 6]);
assert_eq!(reader.read(&mut buf).await.unwrap(), 0);
reader.closed().await.unwrap();
}
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_from_async_read() {
static ASYNC_READ: [u8; 6] = [1, 2, 3, 4, 5, 6];
let mut readable = ReadableStream::from_async_read(&ASYNC_READ[..], 2);
assert!(!readable.is_locked());
let mut reader = readable.get_byob_reader();
let mut dst = [0u8; 3];
let buf = Uint8Array::new_with_length(3);
let (bytes_read, buf) = reader.read_with_buffer(&mut dst, buf).await.unwrap();
assert_eq!(bytes_read, 3);
assert_eq!(&dst, &[1, 2, 3]);
let (bytes_read, buf) = reader
.read_with_buffer(&mut dst[0..2], buf.unwrap())
.await
.unwrap();
assert_eq!(bytes_read, 2);
assert_eq!(&dst, &[4, 5, 3]);
let (bytes_read, buf) = reader
.read_with_buffer(&mut dst[2..], buf.unwrap())
.await
.unwrap();
assert_eq!(bytes_read, 1);
assert_eq!(&dst, &[4, 5, 6]);
let (bytes_read, buf) = reader
.read_with_buffer(&mut dst, buf.unwrap())
.await
.unwrap();
assert_eq!(bytes_read, 0);
assert_eq!(&dst, &[4, 5, 6]);
drop(buf);
reader.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_from_async_read_cancel() {
static ASYNC_READ: [u8; 6] = [1, 2, 3, 4, 5, 6];
let mut readable = ReadableStream::from_async_read(&ASYNC_READ[..], 2);
let mut reader = readable.get_byob_reader();
let mut dst = [0u8; 3];
assert_eq!(reader.read(&mut dst).await.unwrap(), 3);
assert_eq!(&dst, &[1, 2, 3]);
assert_eq!(reader.cancel().await, Ok(()));
reader.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_multiple_byob_readers() {
let mut readable = ReadableStream::from_raw(new_noop_readable_byte_stream());
assert!(!readable.is_locked());
// Release explicitly
let reader = readable.get_byob_reader();
reader.release_lock();
assert!(!readable.is_locked());
// Release by drop
let reader = readable.get_byob_reader();
drop(reader);
assert!(!readable.is_locked());
let reader = readable.get_byob_reader();
reader.release_lock();
assert!(!readable.is_locked());
}
async fn test_readable_byte_stream_abort_read(readable: ReadableStream) {
if supports_release_lock_with_pending_read() {
test_readable_byte_stream_abort_read_new(readable).await;
} else {
test_readable_byte_stream_abort_read_old(readable).await;
}
}
async fn test_readable_byte_stream_abort_read_new(mut readable: ReadableStream) {
let mut reader = readable.get_byob_reader();
// Start reading
// Since the stream will never produce a chunk, this read will remain pending forever
let mut dst = [0u8; 3];
let mut fut = reader.read(&mut dst).boxed_local();
// We need to poll the future at least once to start the read
let poll_result = poll!(&mut fut);
assert!(matches!(poll_result, Poll::Pending));
// Drop the future, to regain control over the reader
drop(fut);
// Releasing the lock should work even while there are pending reads
reader
.try_release_lock()
.expect("releasing the reader should work even while there are pending reads");
}
async fn test_readable_byte_stream_abort_read_old(mut readable: ReadableStream) {
let mut reader = readable.get_byob_reader();
// Start reading
// Since the stream will never produce a chunk, this read will remain pending forever
let mut dst = [0u8; 3];
let mut fut = reader.read(&mut dst).boxed_local();
// We need to poll the future at least once to start the read
let poll_result = poll!(&mut fut);
assert!(matches!(poll_result, Poll::Pending));
// Drop the future, to regain control over the reader
drop(fut);
// Cannot release the lock while there are pending reads
let (_err, mut reader) = reader
.try_release_lock()
.expect_err("reader was released while there are pending reads");
// Cancel all pending reads
reader.cancel().await.unwrap();
// Can release lock after cancelling
reader.release_lock();
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_abort_read_from_raw() {
let readable = ReadableStream::from_raw(new_noop_readable_byte_stream());
test_readable_byte_stream_abort_read(readable).await
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_abort_read_from_async_read() {
static ASYNC_READ: [u8; 6] = [1, 2, 3, 4, 5, 6];
let readable = ReadableStream::from_async_read(&ASYNC_READ[..], 2);
test_readable_byte_stream_abort_read(readable).await
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_into_async_read_auto_cancel() {
let raw_readable = new_noop_readable_byte_stream();
let readable = ReadableStream::from_raw(raw_readable.clone());
let mut async_read = readable.into_async_read();
// Start reading
// Since the stream will never produce a chunk, this read will remain pending forever
let mut buf = [0u8; 1];
let mut fut = async_read.read(&mut buf).boxed_local();
// We need to poll the future at least once to start the read
let poll_result = poll!(&mut fut);
assert!(matches!(poll_result, Poll::Pending));
// Drop the future, to regain control over the AsyncRead
drop(fut);
// Drop the AsyncRead
drop(async_read);
// Stream must be unlocked and cancelled
let mut readable = ReadableStream::from_raw(raw_readable);
assert!(!readable.is_locked());
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), None);
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_into_async_read_auto_cancel_rejects() {
let _guard = UnhandledErrorGuard::new();
let raw_readable = new_readable_byte_stream_with_rejecting_cancel();
let readable = ReadableStream::from_raw(raw_readable.clone());
let async_read = readable.into_async_read();
// Drop the AsyncRead
drop(async_read);
// Stream must be unlocked and cancelled
let mut readable = ReadableStream::from_raw(raw_readable);
assert!(!readable.is_locked());
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), None);
// Wait a little bit for any unhandled rejections
sleep(Duration::from_millis(100)).await;
}
#[wasm_bindgen_test]
async fn test_readable_byte_stream_into_async_read_manual_cancel() {
let raw_readable = new_noop_readable_byte_stream();
let readable = ReadableStream::from_raw(raw_readable.clone());
let mut async_read = readable.into_async_read();
// Start reading
// Since the stream will never produce a chunk, this read will remain pending forever
let mut buf = [0u8; 1];
let mut fut = async_read.read(&mut buf).boxed_local();
// We need to poll the future at least once to start the read
let poll_result = poll!(&mut fut);
assert!(matches!(poll_result, Poll::Pending));
// Drop the future, to regain control over the AsyncRead
drop(fut);
// Cancel the AsyncRead
async_read.cancel().await.unwrap();
// Stream must be unlocked and cancelled
let mut readable = ReadableStream::from_raw(raw_readable);
assert!(!readable.is_locked());
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), None);
}
@@ -0,0 +1,348 @@
use std::pin::Pin;
use std::task::Poll;
use std::time::Duration;
use futures_util::stream::{iter, pending, StreamExt, TryStreamExt};
use futures_util::{poll, AsyncReadExt, FutureExt};
use gloo_timers::future::sleep;
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use wasm_streams::readable::*;
use crate::js::*;
use crate::util::*;
#[wasm_bindgen_test]
async fn test_readable_stream_new() {
let mut readable = ReadableStream::from_raw(new_readable_stream_from_array(
vec![JsValue::from("Hello"), JsValue::from("world!")].into_boxed_slice(),
));
assert!(!readable.is_locked());
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("Hello")));
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("world!")));
assert_eq!(reader.read().await.unwrap(), None);
reader.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_readable_stream_into_stream() {
let readable = ReadableStream::from_raw(new_readable_stream_from_array(
vec![JsValue::from("Hello"), JsValue::from("world!")].into_boxed_slice(),
));
assert!(!readable.is_locked());
let mut stream = readable.into_stream();
assert_eq!(stream.next().await, Some(Ok(JsValue::from("Hello"))));
assert_eq!(stream.next().await, Some(Ok(JsValue::from("world!"))));
assert_eq!(stream.next().await, None);
}
#[wasm_bindgen_test]
fn test_readable_stream_into_stream_impl_unpin() {
let readable = ReadableStream::from_raw(new_noop_readable_stream());
let stream: IntoStream = readable.into_stream();
let _ = Pin::new(&stream); // must be Unpin for this to work
}
#[wasm_bindgen_test]
async fn test_readable_stream_reader_into_stream() {
let mut readable = ReadableStream::from_raw(new_readable_stream_from_array(
vec![JsValue::from("Hello"), JsValue::from("world!")].into_boxed_slice(),
));
assert!(!readable.is_locked());
{
// Acquire a reader and wrap it in a Rust Stream
let reader = readable.get_reader();
let mut stream = reader.into_stream();
assert_eq!(stream.next().await, Some(Ok(JsValue::from("Hello"))));
}
// Dropping the wrapped Stream should release the lock
assert!(!readable.is_locked());
{
// Can acquire a new reader after wrapped stream is dropped
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("world!")));
assert_eq!(reader.read().await.unwrap(), None);
}
}
#[wasm_bindgen_test]
async fn test_readable_stream_from_stream() {
let stream = iter(vec!["Hello", "world!"]).map(|s| Ok(JsValue::from(s)));
let mut readable = ReadableStream::from_stream(stream);
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("Hello")));
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("world!")));
assert_eq!(reader.read().await.unwrap(), None);
reader.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_readable_stream_from_stream_cancel() {
let stream = iter(vec!["Hello", "world!"]).map(|s| Ok(JsValue::from(s)));
let (stream, observer) = observe_drop(stream);
let mut readable = ReadableStream::from_stream(stream);
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("Hello")));
assert!(!observer.is_dropped());
assert_eq!(reader.cancel().await, Ok(()));
assert!(observer.is_dropped());
reader.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_readable_stream_multiple_readers() {
let mut readable = ReadableStream::from_raw(new_noop_readable_stream());
assert!(!readable.is_locked());
// Release explicitly
let reader = readable.get_reader();
reader.release_lock();
assert!(!readable.is_locked());
// Release by drop
let reader = readable.get_reader();
drop(reader);
assert!(!readable.is_locked());
let reader = readable.get_reader();
reader.release_lock();
assert!(!readable.is_locked());
}
#[wasm_bindgen_test]
async fn test_readable_stream_abort_read() {
if supports_release_lock_with_pending_read() {
test_readable_stream_abort_read_new().await;
} else {
test_readable_stream_abort_read_old().await;
}
}
async fn test_readable_stream_abort_read_new() {
let stream = pending();
let mut readable = ReadableStream::from_stream(stream);
let mut reader = readable.get_reader();
// Start reading
// Since the stream will never produce a chunk, this read will remain pending forever
let mut fut = reader.read().boxed_local();
// We need to poll the future at least once to start the read
let poll_result = poll!(&mut fut);
assert!(matches!(poll_result, Poll::Pending));
// Drop the future, to regain control over the reader
drop(fut);
// Releasing the lock should work even while there are pending reads
reader
.try_release_lock()
.expect("releasing the reader should work even while there are pending reads");
}
async fn test_readable_stream_abort_read_old() {
let stream = pending();
let (stream, observer) = observe_drop(stream);
let mut readable = ReadableStream::from_stream(stream);
let mut reader = readable.get_reader();
// Start reading
// Since the stream will never produce a chunk, this read will remain pending forever
let mut fut = reader.read().boxed_local();
// We need to poll the future at least once to start the read
let poll_result = poll!(&mut fut);
assert!(matches!(poll_result, Poll::Pending));
// Drop the future, to regain control over the reader
drop(fut);
// Cannot release the lock while there are pending reads
let (_err, mut reader) = reader
.try_release_lock()
.expect_err("reader was released while there are pending reads");
// Cancel all pending reads
assert!(!observer.is_dropped());
reader.cancel().await.unwrap();
assert!(observer.is_dropped());
// Can release lock after cancelling
reader.release_lock();
}
#[wasm_bindgen_test]
async fn test_readable_stream_from_stream_then_into_stream() {
let stream = iter(vec!["Hello", "world!"]).map(|s| Ok(JsValue::from(s)));
let readable = ReadableStream::from_stream(stream);
let mut stream = readable.into_stream();
assert_eq!(stream.next().await, Some(Ok(JsValue::from("Hello"))));
assert_eq!(stream.next().await, Some(Ok(JsValue::from("world!"))));
assert_eq!(stream.next().await, None);
}
#[wasm_bindgen_test]
async fn test_readable_stream_into_stream_then_from_stream() {
let readable = ReadableStream::from_raw(new_readable_stream_from_array(
vec![JsValue::from("Hello"), JsValue::from("world!")].into_boxed_slice(),
));
let stream = readable.into_stream();
let mut readable = ReadableStream::from_stream(stream);
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("Hello")));
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("world!")));
assert_eq!(reader.read().await.unwrap(), None);
reader.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_readable_stream_tee() {
let chunks = vec![JsValue::from("Hello"), JsValue::from("world!")];
let readable = ReadableStream::from_raw(new_readable_stream_from_array(
chunks.clone().into_boxed_slice(),
));
let (left, right) = readable.tee();
let left_chunks = left.into_stream().try_collect::<Vec<_>>().await.unwrap();
let right_chunks = right.into_stream().try_collect::<Vec<_>>().await.unwrap();
assert_eq!(left_chunks, chunks);
assert_eq!(right_chunks, chunks);
}
#[wasm_bindgen_test]
async fn test_readable_stream_into_stream_auto_cancel() {
let raw_readable = new_noop_readable_stream();
let readable = ReadableStream::from_raw(raw_readable.clone());
let mut stream = readable.into_stream();
// Start reading
// Since the stream will never produce a chunk, this read will remain pending forever
let mut fut = stream.next().boxed_local();
// We need to poll the future at least once to start the read
let poll_result = poll!(&mut fut);
assert!(matches!(poll_result, Poll::Pending));
// Drop the future, to regain control over the stream
drop(fut);
// Drop the stream
drop(stream);
// Stream must be unlocked and cancelled
let mut readable = ReadableStream::from_raw(raw_readable);
assert!(!readable.is_locked());
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), None);
}
#[wasm_bindgen_test]
async fn test_readable_stream_into_stream_manual_cancel() {
let raw_readable = new_noop_readable_stream();
let readable = ReadableStream::from_raw(raw_readable.clone());
let mut stream = readable.into_stream();
// Start reading
// Since the stream will never produce a chunk, this read will remain pending forever
let mut fut = stream.next().boxed_local();
// We need to poll the future at least once to start the read
let poll_result = poll!(&mut fut);
assert!(matches!(poll_result, Poll::Pending));
// Drop the future, to regain control over the stream
drop(fut);
// Cancel the stream
stream.cancel().await.unwrap();
// Stream must be unlocked and cancelled
let mut readable = ReadableStream::from_raw(raw_readable);
assert!(!readable.is_locked());
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), None);
}
#[wasm_bindgen_test]
async fn test_readable_stream_into_stream_auto_cancel_rejects() {
let _guard = UnhandledErrorGuard::new();
let raw_readable = new_readable_stream_with_rejecting_cancel();
let readable = ReadableStream::from_raw(raw_readable.clone());
let stream = readable.into_stream();
// Drop the stream
drop(stream);
// Stream must be unlocked and cancelled
let mut readable = ReadableStream::from_raw(raw_readable);
assert!(!readable.is_locked());
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), None);
// Wait a little bit for any unhandled rejections
sleep(Duration::from_millis(100)).await;
}
#[wasm_bindgen_test]
async fn test_readable_stream_into_stream_then_into_async_read() {
let readable = ReadableStream::from_raw(new_readable_stream_from_array(
vec![
Uint8Array::from(&[1, 2, 3][..]).into(),
Uint8Array::from(&[4, 5, 6][..]).into(),
]
.into_boxed_slice(),
));
assert!(!readable.is_locked());
let mut async_read = readable
.into_stream()
.map_ok(|value| value.dyn_into::<Uint8Array>().unwrap().to_vec())
.map_err(|_err| std::io::Error::from(std::io::ErrorKind::Other))
.into_async_read();
let mut buf = [0u8; 3];
assert_eq!(async_read.read(&mut buf).await.unwrap(), 3);
assert_eq!(&buf, &[1, 2, 3]);
assert_eq!(async_read.read(&mut buf[..1]).await.unwrap(), 1);
assert_eq!(&buf, &[4, 2, 3]);
assert_eq!(async_read.read(&mut buf[1..]).await.unwrap(), 2);
assert_eq!(&buf, &[4, 5, 6]);
assert_eq!(async_read.read(&mut buf).await.unwrap(), 0);
assert_eq!(&buf, &[4, 5, 6]);
}
#[wasm_bindgen_test]
async fn test_readable_stream_from_js_array() {
let js_array =
js_sys::Array::from_iter([JsValue::from_str("Hello"), JsValue::from_str("world!")]);
let mut readable = match ReadableStream::try_from(js_array.unchecked_into()) {
Ok(readable) => readable,
Err(err) => {
// ReadableStream.from() is not yet supported in all browsers.
assert_eq!(err.name(), "TypeError");
assert_eq!(
err.message().as_string().unwrap(),
"ReadableStream.from is not a function"
);
return;
}
};
assert!(!readable.is_locked());
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("Hello")));
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("world!")));
assert_eq!(reader.read().await.unwrap(), None);
reader.closed().await.unwrap();
}
@@ -0,0 +1,51 @@
use futures_util::future::join;
use wasm_bindgen::prelude::*;
use wasm_bindgen_test::*;
use wasm_streams::transform::*;
use crate::js::*;
#[wasm_bindgen_test]
async fn test_transform_stream_new() {
let transform = TransformStream::from_raw(new_noop_transform_stream());
join(
async {
let mut writable = transform.writable();
let mut writer = writable.get_writer();
writer.write(JsValue::from("Hello")).await.unwrap();
writer.write(JsValue::from("world!")).await.unwrap();
writer.close().await.unwrap();
},
async {
let mut readable = transform.readable();
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("Hello")));
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("world!")));
assert_eq!(reader.read().await.unwrap(), None);
},
)
.await;
}
#[wasm_bindgen_test]
async fn test_transform_stream_new_uppercase() {
let transform = TransformStream::from_raw(new_uppercase_transform_stream());
join(
async {
let mut writable = transform.writable();
let mut writer = writable.get_writer();
writer.write(JsValue::from("Hello")).await.unwrap();
writer.write(JsValue::from("world!")).await.unwrap();
writer.close().await.unwrap();
},
async {
let mut readable = transform.readable();
let mut reader = readable.get_reader();
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("HELLO")));
assert_eq!(reader.read().await.unwrap(), Some(JsValue::from("WORLD!")));
assert_eq!(reader.read().await.unwrap(), None);
},
)
.await;
}
@@ -0,0 +1,304 @@
use std::pin::Pin;
use futures_util::stream::iter;
use futures_util::{AsyncReadExt, AsyncWriteExt, SinkExt, StreamExt};
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use wasm_streams::writable::*;
use wasm_streams::WritableStream;
use crate::js::*;
use crate::util::*;
#[wasm_bindgen_test]
async fn test_writable_stream_new() {
let mut writable = WritableStream::from_raw(new_noop_writable_stream());
assert!(!writable.is_locked());
let mut writer = writable.get_writer();
assert_eq!(writer.write(JsValue::from("Hello")).await, Ok(()));
assert_eq!(writer.write(JsValue::from("world!")).await, Ok(()));
assert_eq!(writer.close().await, Ok(()));
writer.closed().await.unwrap();
}
#[wasm_bindgen_test]
async fn test_writable_stream_into_sink() {
let recording_stream = RecordingWritableStream::new();
let writable = WritableStream::from_raw(recording_stream.stream());
assert!(!writable.is_locked());
let mut sink = writable.into_sink();
assert_eq!(sink.send(JsValue::from("Hello")).await, Ok(()));
assert_eq!(sink.send(JsValue::from("world!")).await, Ok(()));
assert_eq!(sink.close().await, Ok(()));
assert_eq!(
recording_stream.events(),
[
RecordedEvent::Write(JsValue::from("Hello")),
RecordedEvent::Write(JsValue::from("world!")),
RecordedEvent::Close
]
);
}
#[wasm_bindgen_test]
fn test_writable_stream_into_sink_impl_unpin() {
let writable = WritableStream::from_raw(new_noop_writable_stream());
let sink: IntoSink = writable.into_sink();
let _ = Pin::new(&sink); // must be Unpin for this to work
}
#[wasm_bindgen_test]
async fn test_writable_stream_writer_into_sink() {
let recording_stream = RecordingWritableStream::new();
let mut writable = WritableStream::from_raw(recording_stream.stream());
assert!(!writable.is_locked());
{
// Acquire a writer and wrap it in a Rust Sink
let writer = writable.get_writer();
let mut sink = writer.into_sink();
assert_eq!(sink.send(JsValue::from("Hello")).await, Ok(()));
}
assert_eq!(
recording_stream.events(),
[RecordedEvent::Write(JsValue::from("Hello")),]
);
// Dropping the wrapped Sink should release the lock
assert!(!writable.is_locked());
{
// Can acquire a new writer after wrapped Sink is dropped
let mut writer = writable.get_writer();
assert_eq!(writer.write(JsValue::from("world!")).await, Ok(()));
assert_eq!(writer.close().await, Ok(()));
}
assert_eq!(
recording_stream.events(),
[
RecordedEvent::Write(JsValue::from("Hello")),
RecordedEvent::Write(JsValue::from("world!")),
RecordedEvent::Close
]
);
}
#[wasm_bindgen_test]
async fn test_writable_stream_from_sink() {
let (sink, stream) = SimpleChannel::<JsValue>::new().split();
let sink = sink.sink_map_err(|_| JsValue::from_str("cannot happen"));
let mut writable = WritableStream::from_sink(sink);
let mut writer = writable.get_writer();
assert_eq!(writer.write(JsValue::from("Hello")).await, Ok(()));
assert_eq!(writer.write(JsValue::from("world!")).await, Ok(()));
assert_eq!(writer.close().await, Ok(()));
writer.closed().await.unwrap();
let output = stream.collect::<Vec<_>>().await;
assert_eq!(
output,
vec![JsValue::from("Hello"), JsValue::from("world!")]
);
}
#[wasm_bindgen_test]
async fn test_writable_stream_from_sink_then_into_sink() {
let (sink, stream) = SimpleChannel::<JsValue>::new().split();
let sink = sink.sink_map_err(|_| JsValue::from_str("cannot happen"));
let writable = WritableStream::from_sink(sink);
let mut sink = writable.into_sink();
let chunks = vec![JsValue::from("Hello"), JsValue::from("world!")];
let mut input = iter(chunks.clone()).map(Ok);
sink.send_all(&mut input).await.unwrap();
sink.close().await.unwrap();
let output = stream.collect::<Vec<_>>().await;
assert_eq!(output, chunks);
}
#[wasm_bindgen_test]
async fn test_writable_stream_multiple_writers() {
let recording_stream = RecordingWritableStream::new();
let mut writable = WritableStream::from_raw(recording_stream.stream());
let mut writer = writable.get_writer();
writer.write(JsValue::from_str("Hello")).await.unwrap();
drop(writer);
let mut writer = writable.get_writer();
writer.write(JsValue::from_str("world!")).await.unwrap();
writer.close().await.unwrap();
drop(writer);
assert_eq!(
recording_stream.events(),
[
RecordedEvent::Write(JsValue::from("Hello")),
RecordedEvent::Write(JsValue::from("world!")),
RecordedEvent::Close
]
);
}
#[wasm_bindgen_test]
async fn test_writable_stream_into_async_write() {
let recording_stream = RecordingWritableStream::new();
let writable = WritableStream::from_raw(recording_stream.stream());
assert!(!writable.is_locked());
let mut async_write = writable.into_async_write();
let mut buf = [1, 2, 3];
assert_eq!(async_write.write(&buf).await.unwrap(), 3);
buf = [4, 5, 6];
assert_eq!(async_write.write(&buf).await.unwrap(), 3);
buf = [7, 8, 9];
assert_eq!(async_write.write(&buf[0..2]).await.unwrap(), 2);
async_write.close().await.unwrap();
assert_eq!(
recording_stream.events(),
[
RecordedEvent::Write(Uint8Array::from(&[1, 2, 3][..]).into()),
RecordedEvent::Write(Uint8Array::from(&[4, 5, 6][..]).into()),
RecordedEvent::Write(Uint8Array::from(&[7, 8][..]).into()),
RecordedEvent::Close
]
);
}
#[wasm_bindgen_test]
fn test_writable_stream_into_async_write_impl_unpin() {
let writable = WritableStream::from_raw(new_noop_writable_stream());
let async_write: IntoAsyncWrite = writable.into_async_write();
let _ = Pin::new(&async_write); // must be Unpin for this to work
}
#[wasm_bindgen_test]
async fn test_writable_stream_writer_into_async_write() {
let recording_stream = RecordingWritableStream::new();
let mut writable = WritableStream::from_raw(recording_stream.stream());
assert!(!writable.is_locked());
{
// Acquire a writer and wrap it in a Rust AsyncWrite
let writer = writable.get_writer();
let mut async_write = writer.into_async_write();
async_write.write_all(&[1, 2, 3]).await.unwrap();
}
assert_eq!(
recording_stream.events(),
[RecordedEvent::Write(
Uint8Array::from(&[1, 2, 3][..]).into()
),]
);
// Dropping the wrapped AsyncWrite should release the lock
assert!(!writable.is_locked());
{
// Can acquire a new writer after wrapped sink is dropped
let mut writer = writable.get_writer();
assert_eq!(
writer.write(Uint8Array::from(&[4, 5, 6][..]).into()).await,
Ok(())
);
assert_eq!(writer.close().await, Ok(()));
}
assert_eq!(
recording_stream.events(),
[
RecordedEvent::Write(Uint8Array::from(&[1, 2, 3][..]).into()),
RecordedEvent::Write(Uint8Array::from(&[4, 5, 6][..]).into()),
RecordedEvent::Close
]
);
}
#[wasm_bindgen_test]
async fn test_writable_stream_into_async_write_then_into_sink() {
let recording_stream = RecordingWritableStream::new();
let writable = WritableStream::from_raw(recording_stream.stream());
assert!(!writable.is_locked());
let mut sink = writable.into_async_write().into_sink();
sink.send(vec![1, 2, 3]).await.unwrap();
sink.send(vec![4, 5, 6]).await.unwrap();
sink.close().await.unwrap();
assert_eq!(
recording_stream.events(),
[
RecordedEvent::Write(Uint8Array::from(&[1, 2, 3][..]).into()),
RecordedEvent::Write(Uint8Array::from(&[4, 5, 6][..]).into()),
RecordedEvent::Close
]
);
}
#[wasm_bindgen_test]
async fn test_writable_stream_from_async_write() {
let (mut async_read, async_write) = ByteChannel::new().split();
let sink = async_write
.into_sink()
.with(
|js_value: JsValue| -> std::future::Ready<std::io::Result<Vec<u8>>> {
std::future::ready(Ok(js_value.dyn_into::<Uint8Array>().unwrap().to_vec()))
},
)
.sink_map_err(|_| JsValue::undefined());
let mut writable = WritableStream::from_sink(sink);
assert!(!writable.is_locked());
let mut writer = writable.get_writer();
assert_eq!(
writer.write(Uint8Array::from(&[1, 2, 3][..]).into()).await,
Ok(())
);
assert_eq!(
writer.write(Uint8Array::from(&[4, 5, 6][..]).into()).await,
Ok(())
);
assert_eq!(writer.close().await, Ok(()));
writer.closed().await.unwrap();
let mut dest = vec![];
assert_eq!(async_read.read_to_end(&mut dest).await.unwrap(), 6);
assert_eq!(dest, [1, 2, 3, 4, 5, 6]);
}
#[wasm_bindgen_test]
async fn test_into_sink_errors_after_failure() {
let failing_sink = FailingSink::new();
let writable = WritableStream::from_sink(failing_sink);
let mut sink = writable.into_sink();
// First write should fail
let result1 = sink.send(JsValue::from(1)).await;
assert!(result1.is_err(), "First write should fail");
// After an error, the stream should be in an errored state and
// reject all subsequent operations.
let result2 = sink.send(JsValue::from(2)).await;
assert!(
result2.is_err(),
"Second write should fail because stream is errored, but got Ok(())"
);
}
+1
View File
@@ -0,0 +1 @@
pub mod util;
@@ -0,0 +1,153 @@
use std::cmp::min;
use std::collections::VecDeque;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use futures_util::{AsyncRead, AsyncWrite};
#[derive(Debug, Default)]
pub struct ByteChannel {
queue: VecDeque<u8>,
waker: Option<Waker>,
closed: bool,
}
impl ByteChannel {
pub fn new() -> Self {
Self::default()
}
}
impl AsyncRead for ByteChannel {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
if buf.is_empty() || (self.queue.is_empty() && self.closed) {
return Poll::Ready(Ok(0));
}
let num_read = min(self.queue.len(), buf.len());
if num_read == 0 {
self.waker = Some(cx.waker().clone());
return Poll::Pending;
}
buf.iter_mut()
.zip(self.queue.drain(0..num_read))
.for_each(|(dst, src)| *dst = src);
Poll::Ready(Ok(num_read))
}
}
impl AsyncWrite for ByteChannel {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
self.queue.extend(buf.iter());
if let Some(waker) = self.waker.take() {
waker.wake();
}
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_close(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
self.closed = true;
if let Some(waker) = self.waker.take() {
waker.wake();
}
Poll::Ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use futures_util::future::join;
use futures_util::{AsyncReadExt, AsyncWriteExt};
use super::*;
#[tokio::test]
async fn test_write_then_read() {
let channel = ByteChannel::new();
let (mut reader, mut writer) = channel.split();
let mut buf = [0u8; 3];
writer.write_all(&[1, 2, 3, 4]).await.unwrap();
assert_eq!(reader.read(&mut buf).await.unwrap(), 3);
assert_eq!(&buf, &[1, 2, 3]);
writer.write_all(&[5, 6]).await.unwrap();
assert_eq!(reader.read(&mut buf).await.unwrap(), 3);
assert_eq!(&buf, &[4, 5, 6]);
writer.close().await.unwrap();
assert_eq!(reader.read(&mut buf).await.unwrap(), 0);
}
#[tokio::test]
async fn test_read_then_write() {
let channel = ByteChannel::new();
let (mut reader, mut writer) = channel.split();
join(
async {
let mut buf = [0u8; 3];
assert_eq!(reader.read(&mut buf).await.unwrap(), 3);
assert_eq!(&buf, &[1, 2, 3]);
},
async {
writer.write_all(&[1, 2, 3, 4]).await.unwrap();
},
)
.await;
}
#[tokio::test]
async fn test_read_then_close() {
let channel = ByteChannel::new();
let (mut reader, mut writer) = channel.split();
join(
async {
let mut buf = [0u8; 3];
assert_eq!(reader.read(&mut buf).await.unwrap(), 0);
assert_eq!(&buf, &[0, 0, 0]);
},
async {
writer.close().await.unwrap();
},
)
.await;
}
#[tokio::test]
async fn test_close_then_read() {
let channel = ByteChannel::new();
let (mut reader, mut writer) = channel.split();
writer.write_all(&[1, 2, 3]).await.unwrap();
writer.close().await.unwrap();
// should still read bytes from queue
let mut buf = [0u8; 3];
assert_eq!(reader.read(&mut buf).await.unwrap(), 3);
assert_eq!(&buf, &[1, 2, 3]);
// should read EOF
assert_eq!(reader.read(&mut buf).await.unwrap(), 0);
}
#[tokio::test]
async fn test_read_into_empty_buffer() {
let channel = ByteChannel::new();
let (mut reader, _writer) = channel.split();
let mut buf = [0u8; 0];
assert_eq!(reader.read(&mut buf).await.unwrap(), 0);
}
}
@@ -0,0 +1,77 @@
use std::cell::RefCell;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use futures_util::{Sink, Stream};
use pin_project::{pin_project, pinned_drop};
#[pin_project(PinnedDrop)]
pub struct DropObservable<S> {
#[pin]
inner: S,
handle: Rc<RefCell<bool>>,
}
#[pinned_drop]
impl<T> PinnedDrop for DropObservable<T> {
fn drop(self: Pin<&mut Self>) {
*self.project().handle.borrow_mut() = true
}
}
pub struct DropObserver {
handle: Rc<RefCell<bool>>,
}
impl DropObserver {
pub fn is_dropped(&self) -> bool {
*self.handle.borrow()
}
}
pub fn observe_drop<T>(inner: T) -> (DropObservable<T>, DropObserver) {
let handle = Rc::new(RefCell::new(false));
(
DropObservable {
inner,
handle: handle.clone(),
},
DropObserver { handle },
)
}
impl<S: Stream> Stream for DropObservable<S> {
type Item = S::Item;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.project().inner.poll_next(cx)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S, Item> Sink<Item> for DropObservable<S>
where
S: Sink<Item>,
{
type Error = S::Error;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project().inner.poll_ready(cx)
}
fn start_send(self: Pin<&mut Self>, item: Item) -> Result<(), Self::Error> {
self.project().inner.start_send(item)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project().inner.poll_flush(cx)
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.project().inner.poll_close(cx)
}
}
@@ -0,0 +1,52 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use futures_util::sink::Sink;
use wasm_bindgen::prelude::*;
/// A Sink that always errors on the first write.
pub struct FailingSink {
failed: bool,
}
impl FailingSink {
pub fn new() -> Self {
Self { failed: false }
}
}
impl Default for FailingSink {
fn default() -> Self {
Self::new()
}
}
impl Sink<JsValue> for FailingSink {
type Error = JsValue;
fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn start_send(mut self: Pin<&mut Self>, _item: JsValue) -> Result<(), Self::Error> {
if !self.failed {
self.failed = true;
// Return an error on first write
Err(JsValue::from_str("intentional error"))
} else {
Ok(())
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
if self.failed {
Poll::Ready(Err(JsValue::from_str("sink has failed")))
} else {
Poll::Ready(Ok(()))
}
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
}
+11
View File
@@ -0,0 +1,11 @@
pub use byte_channel::ByteChannel;
pub use drop_observer::observe_drop;
pub use failing_sink::FailingSink;
pub use simple_channel::SimpleChannel;
pub use unhandled_error_guard::UnhandledErrorGuard;
pub mod byte_channel;
pub mod drop_observer;
pub mod failing_sink;
pub mod simple_channel;
pub mod unhandled_error_guard;
@@ -0,0 +1,163 @@
use std::collections::VecDeque;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use futures_util::{Sink, Stream};
use pin_project::pin_project;
#[pin_project]
#[derive(Debug)]
pub struct SimpleChannel<T> {
queue: VecDeque<T>,
waker: Option<Waker>,
closed: bool,
}
impl<T> SimpleChannel<T> {
pub fn new() -> Self {
SimpleChannel {
queue: VecDeque::new(),
waker: None,
closed: false,
}
}
}
impl<T> Default for SimpleChannel<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> Stream for SimpleChannel<T> {
type Item = T;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.project();
match this.queue.pop_front() {
Some(item) => Poll::Ready(Some(item)),
None if *this.closed => Poll::Ready(None),
None => {
*this.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.queue.len(), None)
}
}
impl<T> Sink<T> for SimpleChannel<T> {
type Error = ();
fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn start_send(self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
let this = self.project();
this.queue.push_back(item);
if let Some(waker) = this.waker.take() {
waker.wake();
}
Ok(())
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let this = self.project();
*this.closed = true;
if let Some(waker) = this.waker.take() {
waker.wake();
}
Poll::Ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use futures_util::future::join;
use futures_util::stream::iter;
use futures_util::{SinkExt, StreamExt};
use super::*;
#[tokio::test]
async fn test_write_then_read() {
let channel = SimpleChannel::<u32>::new();
let (mut sink, mut stream) = channel.split();
send_many(&mut sink, vec![1, 2, 3]).await.unwrap();
assert_eq!(stream.next().await.unwrap(), 1);
assert_eq!(stream.next().await.unwrap(), 2);
send_many(&mut sink, vec![4, 5]).await.unwrap();
assert_eq!(stream.next().await.unwrap(), 3);
assert_eq!(stream.next().await.unwrap(), 4);
assert_eq!(stream.next().await.unwrap(), 5);
sink.close().await.unwrap();
assert_eq!(stream.next().await, None);
}
#[tokio::test]
async fn test_read_then_write() {
let channel = SimpleChannel::<u32>::new();
let (mut sink, mut stream) = channel.split();
join(
async {
assert_eq!(stream.next().await.unwrap(), 1);
assert_eq!(stream.next().await.unwrap(), 2);
assert_eq!(stream.next().await.unwrap(), 3);
},
async {
send_many(&mut sink, vec![1, 2, 3, 4]).await.unwrap();
},
)
.await;
}
#[tokio::test]
async fn test_read_then_close() {
let channel = SimpleChannel::<u32>::new();
let (mut sink, mut stream) = channel.split();
join(
async {
assert_eq!(stream.next().await, None);
},
async {
sink.close().await.unwrap();
},
)
.await;
}
#[tokio::test]
async fn test_close_then_read() {
let channel = SimpleChannel::<u32>::new();
let (mut sink, stream) = channel.split();
send_many(&mut sink, vec![1, 2, 3]).await.unwrap();
sink.close().await.unwrap();
// should still read items from queue
assert_eq!(stream.collect::<Vec<_>>().await, vec![1, 2, 3]);
}
async fn send_many<T, Si>(
sink: &mut Si,
values: impl IntoIterator<Item = T>,
) -> Result<(), Si::Error>
where
Si: Sink<T> + Unpin,
{
sink.send_all(&mut iter(values).map(Ok)).await
}
}
@@ -0,0 +1,69 @@
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{window, ErrorEvent, PromiseRejectionEvent};
pub struct UnhandledErrorGuard {
errors: Rc<RefCell<Vec<JsValue>>>,
listener: JsValue,
}
const ERROR_TYPES: [&str; 2] = ["error", "unhandledrejection"];
impl UnhandledErrorGuard {
pub fn new() -> Self {
// Add a listener that collects any errors
let errors = Rc::new(RefCell::new(vec![]));
let listener = {
let errors = errors.clone();
Closure::<dyn FnMut(_)>::new(move |event: JsValue| {
if let Some(event) = event.dyn_ref::<ErrorEvent>() {
errors.borrow_mut().push(event.error());
} else if let Some(event) = event.dyn_ref::<PromiseRejectionEvent>() {
errors.borrow_mut().push(event.reason());
}
})
};
if let Some(window) = window() {
for event_type in ERROR_TYPES {
window
.add_event_listener_with_callback(event_type, listener.as_ref().unchecked_ref())
.unwrap();
}
}
Self {
errors,
listener: listener.into_js_value(),
}
}
}
impl Default for UnhandledErrorGuard {
fn default() -> Self {
Self::new()
}
}
impl Drop for UnhandledErrorGuard {
fn drop(&mut self) {
// Remove listeners
if let Some(window) = window() {
for event_type in ERROR_TYPES {
window
.add_event_listener_with_callback(
event_type,
self.listener.as_ref().unchecked_ref(),
)
.unwrap();
}
}
// Panic if there are any errors
let errors = self.errors.take();
assert!(
errors.is_empty(),
"There were {} unexpected errors",
errors.len()
);
}
}
+7
View File
@@ -0,0 +1,7 @@
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
mod js;
mod tests;
mod util;
+5
View File
@@ -0,0 +1,5 @@
{
"goog:chromeOptions": {
"args": []
}
}