forked from MattiasBuelens/wasm-streams
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinto_underlying_source.rs
More file actions
118 lines (103 loc) · 3.97 KB
/
Copy pathinto_underlying_source.rs
File metadata and controls
118 lines (103 loc) · 3.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
use std::cell::RefCell;
use std::panic::{AssertUnwindSafe, RefUnwindSafe, UnwindSafe};
use std::pin::Pin;
use std::rc::Rc;
use futures_util::future::{AbortHandle, TryFutureExt, abortable};
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>,
}
// SAFETY: `Inner` holds an `Option<Pin<Box<JsValueStream>>>` and uses the
// take-and-replace pattern around every fallible await in `Inner::pull`. On
// panic, the stream is already taken out of the `Option`, leaving the cell in
// a clean `None` state. Subsequent calls fail cleanly via `unwrap_throw`
// rather than observing a torn intermediate state, which upholds the logical
// unwind-safety contract `#[wasm_bindgen]` enforces for exported types.
impl UnwindSafe for IntoUnderlyingSource {}
impl RefUnwindSafe for IntoUnderlyingSource {}
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())
}
}