Skip to main content

web_sys_async_io/
reader.rs

1use std::{future::Future, pin::Pin, task::Poll};
2
3use wasm_bindgen_futures::JsFuture;
4
5#[derive(Debug, Default)]
6pub enum Op {
7    #[default]
8    Idle,
9    ReadPending(JsFuture),
10    ConsumingReadBuffer {
11        read_buffer: js_sys::Uint8Array,
12        already_read: usize,
13    },
14}
15
16/// The underlying stream reader.
17///
18/// A BYOB reader is used when the stream is a byte stream; a default reader
19/// is the fallback for the streams that do not support BYOB reads.
20#[derive(Debug)]
21pub enum Mode {
22    /// A BYOB reader; reads into the owned internal buffer.
23    Byob {
24        /// The reader itself.
25        reader: web_sys::ReadableStreamByobReader,
26
27        /// The owned buffer to pass to the reads.
28        ///
29        /// [`None`] when the buffer ownership is currently transferred to
30        /// a pending read, or when the buffer has not been allocated yet.
31        internal_buf: Option<js_sys::ArrayBuffer>,
32    },
33
34    /// A default reader; yields stream-allocated chunks.
35    Default {
36        /// The reader itself.
37        reader: web_sys::ReadableStreamDefaultReader,
38    },
39}
40
41impl Mode {
42    /// Release the stream lock held by the reader.
43    pub fn release_lock(&self) {
44        match self {
45            Self::Byob { reader, .. } => reader.release_lock(),
46            Self::Default { reader } => reader.release_lock(),
47        }
48    }
49
50    /// Cancel the stream with the given reason.
51    pub fn cancel_with_reason(&self, reason: &wasm_bindgen::JsValue) -> js_sys::Promise {
52        match self {
53            Self::Byob { reader, .. } => reader.cancel_with_reason(reason),
54            Self::Default { reader } => reader.cancel_with_reason(reason),
55        }
56    }
57
58    /// A promise that settles when the stream closes or errors.
59    pub fn closed(&self) -> js_sys::Promise {
60        match self {
61            Self::Byob { reader, .. } => reader.closed(),
62            Self::Default { reader } => reader.closed(),
63        }
64    }
65
66    /// Start a read from the stream, returning the future of a read result
67    /// (to be interpreted via [`parse_read_result`]).
68    ///
69    /// In the BYOB mode, passes the owned internal buffer to the read
70    /// (allocating a new one if needed), requesting at most `requested_size`
71    /// bytes; in the default mode, `requested_size` has no effect, as
72    /// the stream decides the chunk sizes on its own.
73    fn start_read(&mut self, requested_size: u32) -> JsFuture {
74        match self {
75            Self::Byob {
76                reader,
77                internal_buf,
78            } => {
79                let internal_buf = internal_buf
80                    .take()
81                    .filter(|internal_buf| {
82                        let actual_size = internal_buf.byte_length();
83                        debug_assert!(actual_size > 0);
84                        actual_size >= requested_size
85                    })
86                    .unwrap_or_else(|| js_sys::ArrayBuffer::new(requested_size));
87                let internal_buf_view = js_sys::Uint8Array::new_with_byte_offset_and_length(
88                    &internal_buf,
89                    0,
90                    requested_size,
91                );
92                // Despite this not being properly indicated at the type system,
93                // the `read_with_array_buffer_view` fn is actually supposed to
94                // be taking the buffer by value - as it takes the ownership of
95                // the buffer and the old JS reference to it is no longer valid.
96                JsFuture::from(reader.read_with_array_buffer_view(&internal_buf_view))
97            }
98            Self::Default { reader } => JsFuture::from(reader.read()),
99        }
100    }
101
102    /// Take the ownership of the buffer view returned from a read.
103    ///
104    /// In the BYOB mode, the buffer returned from a read is actually the same
105    /// buffer we passed when we started the read - despite it being an
106    /// entirely new JS object; we assume the ownership of the buffer and keep
107    /// it for the next read.
108    /// In the default mode this is a no-op, as the chunk buffers are
109    /// stream-allocated.
110    fn reclaim_read_buffer(&mut self, read_buffer: &js_sys::Uint8Array) {
111        if let Self::Byob { internal_buf, .. } = self {
112            *internal_buf = Some(read_buffer.buffer());
113        }
114    }
115}
116
117/// Interpret the settled result of a read started via
118/// [`Mode::start_read`], extracting the read chunk.
119///
120/// Returns [`None`] when the stream is closed and has no more data.
121fn parse_read_result(
122    mode: &Mode,
123    result: Result<wasm_bindgen::JsValue, wasm_bindgen::JsValue>,
124) -> Result<Option<js_sys::Uint8Array>, ReadError> {
125    let result = result.map_err(ReadError::Read)?;
126    let result: crate::sys::ReadableStreamReaderValue = result.into();
127    match result.value() {
128        Some(read_buffer) => Ok(Some(read_buffer)),
129        // In the BYOB mode, the clean end of stream is a zero-length
130        // buffer, and no buffer at all is an error condition; in
131        // the default mode, no chunk is the clean end of stream.
132        None => match mode {
133            Mode::Byob { .. } => Err(ReadError::ByobReadConsumedBuffer),
134            Mode::Default { .. } => Ok(None),
135        },
136    }
137}
138
139/// Copy the data from the read chunk into `dest`, returning the number of
140/// bytes copied.
141///
142/// Keeps the chunk remainder that did not fit into `dest` (if any) in `op`
143/// for the next read, and reclaims the chunk buffer into `mode` once
144/// the chunk is fully consumed.
145fn consume_read_buffer(
146    mode: &mut Mode,
147    op: &mut Op,
148    read_buffer: js_sys::Uint8Array,
149    already_read: usize,
150    dest: &mut [u8],
151) -> usize {
152    let read_buffer_size = read_buffer.byte_length() as usize;
153    let remaining_size = read_buffer_size - already_read;
154    let copy_size = remaining_size.min(dest.len());
155
156    // One JS-to-wasm copy per read is the minimum possible: the stream
157    // cannot fill the wasm linear memory directly, as a BYOB read
158    // transfers (detaches) the buffer backing the view it is given, and
159    // the `WebAssembly.Memory` buffer is not detachable per spec.
160    if already_read == 0 && copy_size == read_buffer_size {
161        // The whole chunk is copied - no need for a subarray view.
162        read_buffer.copy_to(&mut dest[..copy_size]);
163    } else {
164        let source_view =
165            read_buffer.subarray(already_read as u32, (already_read + copy_size) as u32);
166        source_view.copy_to(&mut dest[..copy_size]);
167    }
168
169    if already_read + copy_size < read_buffer_size {
170        // Keep the chunk remainder for the next read.
171        *op = Op::ConsumingReadBuffer {
172            read_buffer,
173            already_read: already_read + copy_size,
174        };
175    } else {
176        mode.reclaim_read_buffer(&read_buffer);
177    }
178
179    copy_size
180}
181
182impl From<web_sys::ReadableStreamByobReader> for Mode {
183    fn from(reader: web_sys::ReadableStreamByobReader) -> Self {
184        Self::Byob {
185            reader,
186            internal_buf: None,
187        }
188    }
189}
190
191impl From<web_sys::ReadableStreamDefaultReader> for Mode {
192    fn from(reader: web_sys::ReadableStreamDefaultReader) -> Self {
193        Self::Default { reader }
194    }
195}
196
197/// An error that can occur when reading via [`Reader::read_into`].
198#[derive(Debug)]
199pub enum ReadError {
200    /// The underlying read operation threw an error.
201    Read(wasm_bindgen::JsValue),
202
203    /// A BYOB read consumed the buffer and did not provide a new one;
204    /// this indicates an error condition.
205    ByobReadConsumedBuffer,
206}
207
208impl From<ReadError> for std::io::Error {
209    fn from(err: ReadError) -> Self {
210        match err {
211            ReadError::Read(err) => super::js_value_to_io_error(err),
212            ReadError::ByobReadConsumedBuffer => {
213                std::io::Error::other("BYOB read consumed the buffer and did not provide a new one")
214            }
215        }
216    }
217}
218
219#[derive(Debug)]
220pub struct Reader {
221    pub inner: Mode,
222    pub op: Op,
223}
224
225impl Reader {
226    pub fn new(inner: impl Into<Mode>) -> Self {
227        Self {
228            inner: inner.into(),
229            op: Op::default(),
230        }
231    }
232
233    pub fn with_buf(
234        inner: web_sys::ReadableStreamByobReader,
235        internal_buf: js_sys::ArrayBuffer,
236    ) -> Self {
237        Self {
238            inner: Mode::Byob {
239                reader: inner,
240                internal_buf: Some(internal_buf),
241            },
242            op: Op::default(),
243        }
244    }
245
246    /// Read from the stream into the given buffer, returning the number of
247    /// bytes read.
248    ///
249    /// Returns `Ok(0)` when the stream is closed and has no more data.
250    ///
251    /// Consumes the leftovers of the chunks that did not fit into the buffers
252    /// of the previous reads, and resolves the reads left pending by
253    /// a dropped [`tokio::io::AsyncRead::poll_read`].
254    pub async fn read_into(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
255        // Take the leftover of a previously read chunk, or read a new chunk
256        // from the stream.
257        let (read_buffer, already_read) = match std::mem::take(&mut self.op) {
258            Op::ConsumingReadBuffer {
259                read_buffer,
260                already_read,
261            } => (read_buffer, already_read),
262            op => {
263                let fut = match op {
264                    // Resolve the read left pending by a dropped `poll_read`.
265                    Op::ReadPending(fut) => fut,
266                    // Start a new read.
267                    _ => {
268                        let requested_size = buf.len().try_into().unwrap();
269                        self.inner.start_read(requested_size)
270                    }
271                };
272                match parse_read_result(&self.inner, fut.await)? {
273                    Some(read_buffer) => (read_buffer, 0),
274                    None => return Ok(0),
275                }
276            }
277        };
278
279        Ok(consume_read_buffer(
280            &mut self.inner,
281            &mut self.op,
282            read_buffer,
283            already_read,
284            buf,
285        ))
286    }
287}
288
289impl tokio::io::AsyncRead for Reader {
290    fn poll_read(
291        self: Pin<&mut Self>,
292        cx: &mut std::task::Context<'_>,
293        buf: &mut tokio::io::ReadBuf<'_>,
294    ) -> Poll<std::io::Result<()>> {
295        let this = self.get_mut();
296
297        // A read into a buffer with no remaining capacity must complete
298        // immediately without requesting more data from the stream.
299        if buf.remaining() == 0 {
300            return Poll::Ready(Ok(()));
301        }
302
303        match std::mem::take(&mut this.op) {
304            Op::ReadPending(mut fut) => {
305                let result = match Pin::new(&mut fut).poll(cx) {
306                    Poll::Pending => {
307                        this.op = Op::ReadPending(fut);
308                        return Poll::Pending;
309                    }
310                    Poll::Ready(result) => result,
311                };
312
313                // No chunk indicates the end of stream.
314                let Some(read_buffer) = parse_read_result(&this.inner, result)? else {
315                    return Poll::Ready(Ok(()));
316                };
317
318                this.op = Op::ConsumingReadBuffer {
319                    read_buffer,
320                    already_read: 0,
321                };
322
323                Pin::new(this).poll_read(cx, buf)
324            }
325            Op::ConsumingReadBuffer {
326                read_buffer,
327                already_read,
328            } => {
329                let remaining_size = read_buffer.byte_length() as usize - already_read;
330                let copy_size = remaining_size.min(buf.remaining());
331
332                let write_slice = buf.initialize_unfilled_to(copy_size);
333                let copied = consume_read_buffer(
334                    &mut this.inner,
335                    &mut this.op,
336                    read_buffer,
337                    already_read,
338                    write_slice,
339                );
340                buf.advance(copied);
341
342                Poll::Ready(Ok(()))
343            }
344            Op::Idle => {
345                let requested_size = buf.remaining().try_into().unwrap();
346                let fut = this.inner.start_read(requested_size);
347                this.op = Op::ReadPending(fut);
348                Pin::new(this).poll_read(cx, buf)
349            }
350        }
351    }
352}