diff --git a/SPEC.md b/SPEC.md index 92c01dba5..1df1fe66d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -79,7 +79,9 @@ record frame { } ``` -It is assumed that streams using this framing protocol can communicate "closing" to peers using some out-of-band mechanism. +An empty frame, i.e. a frame with empty `data`, closes its `path`: the sender MUST NOT send any more frames on that `path`, including another empty frame, and a receiver MUST treat a frame received on a closed `path` as an error. Closing a `path` does not close the paths nested in it. A sender MAY leave a `path` open, unless stated otherwise (see [Futures](#futures)). + +Frames do not communicate the end of transmit, i.e. that no more frames follow. Each transport MUST communicate it using its own mechanism, for example, TCP shutdown or finishing a QUIC stream. The end of transmit closes every `path`. ## Transport specifications @@ -106,6 +108,8 @@ For example: If `v` is pending, encoded `bool` value is sent on index `0` (corresponding to first parameter) +A `future` without a payload type has an empty value encoding, so once such a pending future resolves, the sender MUST close its index, either with an empty frame or by ending transmit (see [Framed stream specification](#framed-stream-specification)), and the receiver MUST consider it resolved once the index is closed. + ### Streams `stream` values are encoded as `variant stream { pending, ready(list) }`. diff --git a/crates/transport/src/frame/conn/client.rs b/crates/transport/src/frame/conn/client.rs index 35afff0bc..b3b86b924 100644 --- a/crates/transport/src/frame/conn/client.rs +++ b/crates/transport/src/frame/conn/client.rs @@ -11,7 +11,7 @@ use crate::frame::conn::{Incoming, Outgoing}; use crate::frame::{ConnHandler, PROTOCOL}; /// Encodes the header of an invocation, which is followed by `params_len` bytes of -/// parameters, to a `BytesMut` +/// parameters, to a `BytesMut`. If `params_len` is 0, only the header is encoded. /// /// This is low-level API, most users should use [`invoke`]. #[instrument(level = "trace", skip_all)] @@ -31,8 +31,10 @@ pub fn encode_invocation( buf.put_u8(PROTOCOL); CoreNameEncoder.encode(instance, buf)?; CoreNameEncoder.encode(func, buf)?; - buf.put_u8(0); - Leb128Encoder.encode(params_len, buf)?; + if params_len > 0 { + buf.put_u8(0); + Leb128Encoder.encode(params_len, buf)?; + } Ok(()) } diff --git a/crates/transport/src/frame/conn/mod.rs b/crates/transport/src/frame/conn/mod.rs index 379760b1b..ab3c458c2 100644 --- a/crates/transport/src/frame/conn/mod.rs +++ b/crates/transport/src/frame/conn/mod.rs @@ -94,23 +94,30 @@ impl Header { } } +/// Sender of a subscribed path +#[derive(Debug)] +enum TxSlot { + Open(mpsc::Sender>), + Closed, +} + /// Index trie containing async stream subscriptions #[derive(Debug, Default)] enum IndexTrie { #[default] Empty, Leaf { - tx: Option>>, + tx: Option, rx: Option>>, }, IndexNode { - tx: Option>>, + tx: Option, rx: Option>>, nested: Vec>, }, // TODO: Add partially-indexed `WildcardIndexNode` WildcardNode { - tx: Option>>, + tx: Option, rx: Option>>, nested: Option>, }, @@ -131,7 +138,10 @@ impl<'a> ), ) -> Self { match path { - [] => Self::Leaf { tx: Some(tx), rx }, + [] => Self::Leaf { + tx: Some(TxSlot::Open(tx)), + rx, + }, [None, path @ ..] => Self::WildcardNode { tx: None, rx: None, @@ -190,13 +200,23 @@ impl]>> FromIterator

for IndexTrie { } impl IndexTrie { + /// Builds a trie of `paths`, which sends frames received on the root path to `tx` + fn new]>>( + paths: impl IntoIterator, + tx: mpsc::Sender>, + ) -> Self { + let mut index: Self = paths + .into_iter() + .filter(|path| !path.as_ref().is_empty()) + .collect(); + index.insert(&[], tx, None); + index + } + /// Calls `f` with the sender and receiver of each node in the trie fn for_each( &mut self, - f: &mut impl FnMut( - &mut Option>>, - &mut Option>>, - ), + f: &mut impl FnMut(&mut Option, &mut Option>>), ) { match self { Self::Empty => {} @@ -261,14 +281,15 @@ impl IndexTrie { } } - /// Gets a sender - #[instrument(level = "trace", skip(self), ret(level = "trace"))] - fn get_tx(&mut self, path: &[usize]) -> Option>> { + /// Gets the sender slot of a subscription + #[instrument(level = "trace", skip(self))] + fn get_tx_mut(&mut self, path: &[usize]) -> Option<&mut TxSlot> { let Some((i, path)) = path.split_first() else { return match self { Self::Empty => None, - Self::Leaf { tx, .. } => tx.clone(), - Self::IndexNode { tx, .. } | Self::WildcardNode { tx, .. } => tx.clone(), + Self::Leaf { tx, .. } + | Self::IndexNode { tx, .. } + | Self::WildcardNode { tx, .. } => tx.as_mut(), }; }; match self { @@ -276,7 +297,7 @@ impl IndexTrie { Self::IndexNode { nested, .. } => { let nested = nested.get_mut(*i)?; let nested = nested.as_mut()?; - nested.get_tx(path) + nested.get_tx_mut(path) } // TODO: Demux the subscription //Self::WildcardNode { ref mut nested, .. } => { // nested.as_mut().and_then(|nested| nested.take(path)) @@ -284,11 +305,13 @@ impl IndexTrie { } } - /// Closes all senders in the trie + /// Closes all open senders in the trie #[instrument(level = "trace", skip(self), ret(level = "trace"))] fn close_tx(&mut self) { self.for_each(&mut |tx, _| { - tx.take(); + if let Some(TxSlot::Open(..)) = tx { + tx.take(); + } }); } @@ -330,7 +353,7 @@ impl IndexTrie { } Self::IndexNode { tx, rx, nested } => match (&tx, &rx, path) { (None, None, []) => { - *tx = Some(sender); + *tx = Some(TxSlot::Open(sender)); *rx = receiver; true } @@ -351,7 +374,7 @@ impl IndexTrie { }, Self::WildcardNode { tx, rx, nested } => match (&tx, &rx, path) { (None, None, []) => { - *tx = Some(sender); + *tx = Some(TxSlot::Open(sender)); *rx = receiver; true } @@ -413,7 +436,7 @@ impl Incoming { { let (rx_tx, rx_rx) = mpsc::channel(128); let state = Arc::new(IngressState { - index: std::sync::Mutex::new(paths.into_iter().collect()), + index: std::sync::Mutex::new(IndexTrie::new(paths, rx_tx)), err: std::sync::OnceLock::default(), unprocessed: AtomicBool::default(), }); @@ -424,11 +447,10 @@ impl Incoming { async move { let res = async { let mut io = pin!(async { - let res = ingress(&mut rx, &state.index, &rx_tx).await; + let res = ingress(&mut rx, &state.index).await; if let Err(err) = &res { _ = state.err.set(copy_io_error(err)); } - drop(rx_tx); trace!("shutting down index trie"); state .index @@ -534,7 +556,9 @@ impl AsyncRead for Incoming { trace!(buf = ?buf.filled(), "read buffer"); if buf.filled().is_empty() { self.rx.take(); - if let Some(err) = self.state.err.get() { + if let Some(err) = self.state.err.get() + && !self.state.is_closed(&self.path) + { return Poll::Ready(Err(copy_io_error(err))); } } @@ -548,6 +572,14 @@ struct IngressState { unprocessed: AtomicBool, } +impl IngressState { + /// Returns whether an empty frame closed the `path` + fn is_closed(&self, path: &[usize]) -> bool { + let mut index = self.index.lock().unwrap_or_else(PoisonError::into_inner); + matches!(index.get_tx_mut(path), Some(TxSlot::Closed)) + } +} + enum IngressGuard { Abort { _task: AbortOnDropHandle<()> }, Drain { _dropped: oneshot::Sender<()> }, @@ -637,6 +669,7 @@ impl Outgoing { } /// Writes `buf` to the stream as a single frame, without copying it. + /// Empty `buf` is not written, since an empty frame closes the stream, see [`Self::close`]. pub async fn write_bytes(&mut self, buf: Bytes) -> std::io::Result<()> { if buf.is_empty() { return Ok(()); @@ -644,6 +677,12 @@ impl Outgoing { self.reserve().await?; self.send_reserved(buf) } + + /// Closes the stream by writing an empty frame. No more data may be written to it afterwards. + pub async fn close(&mut self) -> std::io::Result<()> { + self.reserve().await?; + self.send_reserved(Bytes::default()) + } } impl AsyncWrite for Outgoing { @@ -677,7 +716,6 @@ impl AsyncWrite for Outgoing { async fn ingress( mut rx: impl AsyncRead + Unpin, index: &std::sync::Mutex, - param_tx: &mpsc::Sender>, ) -> std::io::Result<()> { loop { trace!("reading path length"); @@ -692,8 +730,8 @@ async fn ingress( let n = usize::try_from(n) .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?; trace!(n, "read path length"); - let tx = if n == 0 { - param_tx + let path = if n == 0 { + Vec::default() } else { let mut path = Vec::with_capacity(n.min(MAX_INITIAL_PATH_CAPACITY)); for i in 0..n { @@ -704,21 +742,34 @@ async fn ingress( path.push(p); } trace!(?path, "read path"); - + path + }; + trace!("reading data length"); + let n = rx.read_u32_leb128().await?; + trace!(n, "read data length"); + let tx = { trace!("locking index trie"); - let mut index = index + let mut trie = index .lock() .map_err(|err| std::io::Error::other(err.to_string()))?; - &index.get_tx(&path).ok_or_else(|| { + let tx = trie.get_tx_mut(&path).ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::NotFound, format!("`{path:?}` subscription not found"), ) - })? + })?; + if n == 0 { + trace!(?path, "closing path"); + if let TxSlot::Closed = mem::replace(tx, TxSlot::Closed) { + return Err(error_closed(&path)); + } + continue; + } + let TxSlot::Open(tx) = tx else { + return Err(error_closed(&path)); + }; + tx.clone() }; - trace!("reading data length"); - let n = rx.read_u32_leb128().await?; - trace!(n, "read data length"); let mut data = (&mut rx).take(n.into()); while data.limit() > 0 { let k = usize::try_from(data.limit()) @@ -750,6 +801,14 @@ fn close_rx(state: &IngressState) -> std::io::Result<()> { Ok(()) } +/// Returns the error for a frame received on a closed `path` +fn error_closed(path: &[usize]) -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("received a frame on closed path `{path:?}`"), + ) +} + /// Returns the error for received data, which was not processed fn error_unprocessed() -> std::io::Error { std::io::Error::new( @@ -892,6 +951,21 @@ mod tests { done(done_rx).await.unwrap(); } + #[test_log::test(tokio::test)] + async fn incoming_empty_frame_closes_path() { + let (mut tx, mut rx, done_rx) = incoming(true); + let mut sub = rx.index(&[0]).unwrap(); + tx.write_all(&[0x00, 0x00]).await.unwrap(); + tx.write_all(&FRAME).await.unwrap(); + tx.write_all(&[0x01, 0x00, 0x00]).await.unwrap(); + tx.write_all(&[0x01, 0x01, 0x01, 0x2a]).await.unwrap(); + assert_eq!(failed(done_rx).await, ErrorKind::NotFound); + let mut buf = vec![]; + sub.read_to_end(&mut buf).await.unwrap(); + assert_eq!(buf, [0x2a]); + assert_eq!(rx.read_to_end(&mut Vec::default()).await.unwrap(), 0); + } + #[test_log::test(tokio::test)] async fn incoming_drop_rejects_data() { let (mut tx, rx, done_rx) = incoming(true); @@ -961,42 +1035,49 @@ mod tests { } #[test_log::test(tokio::test)] - async fn ingress_truncated_path() { - let index = std::sync::Mutex::new(IndexTrie::Empty); - let (tx, _rx) = mpsc::channel(1); - let err = ingress([0x02, 0x00].as_slice(), &index, &tx) - .await - .unwrap_err(); - assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof); + async fn ingress_rejects_invalid_frames() { + for (frames, kind) in [ + ([0x02, 0x00].as_slice(), ErrorKind::UnexpectedEof), + ( + &[0x00, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x42], + ErrorKind::UnexpectedEof, + ), + (&[0x00, 0x00, 0x00, 0x01, 0x2a], ErrorKind::InvalidData), + (&[0x00, 0x00, 0x00, 0x00], ErrorKind::InvalidData), + ( + &[0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x2a], + ErrorKind::InvalidData, + ), + ( + &[0x01, 0x00, 0x00, 0x01, 0x00, 0x00], + ErrorKind::InvalidData, + ), + (&[0x01, 0x01, 0x00], ErrorKind::NotFound), + (&[0x01, 0x01, 0x01, 0x2a], ErrorKind::NotFound), + ] { + let (tx, _rx) = mpsc::channel(1); + let index = std::sync::Mutex::new(IndexTrie::new( + [[Some(0)].as_slice(), &[Some(1), Some(0)]], + tx, + )); + let err = ingress(frames, &index).await.unwrap_err(); + assert_eq!(err.kind(), kind, "{frames:02x?}"); + } } #[test_log::test(tokio::test)] async fn ingress_chunked_data() { - let index = std::sync::Mutex::new(IndexTrie::Empty); let (tx, mut rx) = mpsc::channel(4); + let index = std::sync::Mutex::new(IndexTrie::new::<[_; 0]>([], tx)); let n = DEFAULT_MAX_INITIAL_CAPACITY + 1; let mut frame = vec![0x00, 0x81, 0x80, 0x40]; frame.resize(frame.len() + n, 0x42); - ingress(frame.as_slice(), &index, &tx).await.unwrap(); - drop(tx); + ingress(frame.as_slice(), &index).await.unwrap(); + drop(index); let mut chunks = vec![]; while let Some(chunk) = rx.recv().await { chunks.push(chunk.unwrap().len()); } assert_eq!(chunks, [DEFAULT_MAX_INITIAL_CAPACITY, 1]); } - - #[test_log::test(tokio::test)] - async fn ingress_truncated_data() { - let index = std::sync::Mutex::new(IndexTrie::Empty); - let (tx, _rx) = mpsc::channel(1); - let err = ingress( - [0x00, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x42].as_slice(), - &index, - &tx, - ) - .await - .unwrap_err(); - assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof); - } } diff --git a/crates/transport/src/value.rs b/crates/transport/src/value.rs index 12b2d94e4..8d983e8c9 100644 --- a/crates/transport/src/value.rs +++ b/crates/transport/src/value.rs @@ -443,7 +443,11 @@ impl Sink for Outgoing { let mut enc = T::Encoder::default(); let mut buf = BytesMut::default(); enc.encode(item, &mut buf)?; - self.write_bytes(buf.freeze()).await?; + if buf.is_empty() { + self.close().await?; + } else { + self.write_bytes(buf.freeze()).await?; + } match enc.take_deferred() { Some(f) => f(self, Vec::default()).await, _ => Ok(()), @@ -1922,17 +1926,19 @@ where return Err(std::io::ErrorKind::UnexpectedEof.into()); }; let item = item?; + let rx = dec.decoder_mut().take_deferred(); + if rx.is_none() { + ensure_processed(&dec)?; + } if tx.send(item).is_err() { return Err(error_receiver_closed()); } - if let Some(rx) = dec.decoder_mut().take_deferred() { + if let Some(rx) = rx { let mut buf = mem::take(dec.read_buffer_mut()); let mut r = dec.into_inner(); buf.unsplit(mem::take(&mut r.buffer)); r.buffer = buf; rx(r, Vec::default()).await?; - } else { - ensure_processed(&dec)?; } Ok(()) } @@ -2636,6 +2642,11 @@ mod tests { let (_fut, f) = pending::(); let err = f(buffered(b"\x03foo\x2a"), Vec::default()).await; assert_eq!(err.unwrap_err().kind(), ErrorKind::InvalidData); + + let (fut, f) = pending:: + Send>>>(); + let err = f(buffered(b"\x2a"), Vec::default()).await; + assert_eq!(err.unwrap_err().kind(), ErrorKind::InvalidData); + assert!(fut.now_or_never().is_none()); } #[test_log::test(tokio::test)] @@ -2649,6 +2660,13 @@ mod tests { assert_eq!(err.unwrap_err().kind(), ErrorKind::InvalidData); } + #[test_log::test(tokio::test)] + async fn future_unit_resolves_on_end() { + let (fut, f) = pending:: + Send>>>(); + f(buffered(b""), Vec::default()).await.unwrap(); + fut.await; + } + #[test_log::test(tokio::test)] async fn future_rejects_dropped_receiver() { let (fut, f) = pending::(); diff --git a/crates/wit-bindgen-go/src/interface.rs b/crates/wit-bindgen-go/src/interface.rs index c797c40b0..1782e76b4 100644 --- a/crates/wit-bindgen-go/src/interface.rs +++ b/crates/wit-bindgen-go/src/interface.rs @@ -10,7 +10,7 @@ use wit_bindgen_core::wit_parser::{ Variant, WorldKey, }; use wit_bindgen_core::{Source, TypeInfo, uwrite, uwriteln}; -use wrpc_introspect::{async_paths_ty, is_list_of, is_tuple, is_ty, rpc_func_name}; +use wrpc_introspect::{async_paths_ty, is_tuple, is_ty, rpc_func_name}; use crate::{ Deps, GoWrpc, Identifier, InterfaceName, to_go_ident, to_package_ident, to_upper_camel_case, @@ -578,8 +578,11 @@ impl InterfaceGenerator<'_> { let errors = self.deps.errors(); let slog = self.deps.slog(); let wrpc = self.deps.wrpc(); + let deferred = self.is_async(ty); + let d = if deferred { "nil, " } else { "" }; uwrite!(self.src, "func(r {wrpc}.IndexReadCloser, path ...uint32) ("); self.print_list(ty); + self.print_deferred_ret(deferred); uwrite!( self.src, r#", error) {{ @@ -592,41 +595,62 @@ impl InterfaceGenerator<'_> { if i > 0 && err == {io}.EOF {{ err = {io}.ErrUnexpectedEOF }} - return nil, {fmt}.Errorf("failed to read list length byte: %w", err) + return nil, {d}{fmt}.Errorf("failed to read list length byte: %w", err) }} if s == 28 && b > 0x0f {{ - return nil, {errors}.New("list length overflows a 32-bit integer") + return nil, {d}{errors}.New("list length overflows a 32-bit integer") }} if b < 0x80 {{ x = x | uint32(b)< { let fmt = self.deps.fmt(); let slog = self.deps.slog(); let wrpc = self.deps.wrpc(); + let deferred = self.is_async(ty); + let (d, dv) = if deferred { ("nil, ", "d, ") } else { ("", "") }; uwrite!(self.src, "func(r {wrpc}.IndexReadCloser, path ...uint32) ("); self.print_option(ty, true); + self.print_deferred_ret(deferred); uwrite!( self.src, r#", error) {{ {slog}.Debug("reading option status byte") status, err := r.ReadByte() if err != nil {{ - return nil, {fmt}.Errorf("failed to read option status byte: %w", err) + return nil, {d}{fmt}.Errorf("failed to read option status byte: %w", err) }} switch status {{ case 0: - return nil, nil + return nil, {d}nil case 1: {slog}.Debug("reading `option::some` payload") - v, err := "#, + v, {dv}err := "#, ); self.print_read_ty(ty, "r", "path"); self.push_str("\n"); uwrite!( self.src, r#"if err != nil {{ - return nil, {fmt}.Errorf("failed to read `option::some` value: %w", err) + return nil, {d}{fmt}.Errorf("failed to read `option::some` value: %w", err) }} return "#, ); self.print_nillable_ptr(ty, false, false); uwrite!( self.src, - r#"v, nil + r#"v, {dv}nil default: - return nil, {fmt}.Errorf("invalid option status byte %d", status) + return nil, {d}{fmt}.Errorf("invalid option status byte %d", status) }} }}({reader}"#, ); @@ -688,71 +715,65 @@ impl InterfaceGenerator<'_> { let fmt = self.deps.fmt(); let slog = self.deps.slog(); let wrpc = self.deps.wrpc(); + let deferred = ty.ok.iter().chain(&ty.err).any(|ty| self.is_async(ty)); + let d = if deferred { "nil, " } else { "" }; uwrite!( self.src, "func(r {wrpc}.IndexReadCloser, path ...uint32) (*" ); self.print_result(ty); + self.print_deferred_ret(deferred); uwriteln!( self.src, r#", error) {{ {slog}.Debug("reading result status byte") status, err := r.ReadByte() if err != nil {{ - return nil, {fmt}.Errorf("failed to read result status byte: %w", err) + return nil, {d}{fmt}.Errorf("failed to read result status byte: %w", err) }}"#, ); self.push_str("switch status {\n"); - self.push_str("case 0:\n"); - if let Some(ref ok) = ty.ok { - uwriteln!(self.src, r#"{slog}.Debug("reading `result::ok` payload")"#); - self.push_str("v, err := "); - self.print_read_ty(ok, "r", "path"); - self.push_str("\n"); - uwriteln!( - self.src, - r#"if err != nil {{ - return nil, fmt.Errorf("failed to read `result::ok` value: %w", err) - }}"#, - ); - self.push_str("return &"); - self.print_result(ty); - self.push_str("{ Ok: "); - self.print_nillable_ptr(ok, true, false); - } else { - self.push_str("var v struct{}\n"); - self.push_str("return &"); - self.print_result(ty); - self.push_str("{ Ok: &"); - } - self.push_str("v }, nil\n"); - self.push_str("case 1:\n"); - if let Some(ref err) = ty.err { - uwriteln!(self.src, r#"{slog}.Debug("reading `result::err` payload")"#); - self.push_str("v, err := "); - self.print_read_ty(err, "r", "path"); - self.push_str("\n"); - uwriteln!( - self.src, - r#"if err != nil {{ - return nil, {fmt}.Errorf("failed to read `result::err` value: %w", err) + for (i, (case, field, payload)) in [("ok", "Ok", &ty.ok), ("err", "Err", &ty.err)] + .into_iter() + .enumerate() + { + uwriteln!(self.src, "case {i}:"); + let mut ret = d; + if let Some(payload) = payload { + uwriteln!( + self.src, + r#"{slog}.Debug("reading `result::{case}` payload")"# + ); + if self.is_async(payload) { + self.push_str("v, d, err := "); + ret = "d, "; + } else { + self.push_str("v, err := "); + } + self.print_read_ty(payload, "r", "path"); + self.push_str("\n"); + uwriteln!( + self.src, + r#"if err != nil {{ + return nil, {d}{fmt}.Errorf("failed to read `result::{case}` value: %w", err) }}"#, - ); - self.push_str("return &"); - self.print_result(ty); - self.push_str("{ Err: "); - self.print_nillable_ptr(err, true, false); - } else { - self.push_str("var v struct{}\n"); - self.push_str("return &"); - self.print_result(ty); - self.push_str("{ Err: &"); + ); + self.push_str("return &"); + self.print_result(ty); + uwrite!(self.src, "{{ {field}: "); + self.print_nillable_ptr(payload, true, false); + } else { + self.push_str("var v struct{}\n"); + self.push_str("return &"); + self.print_result(ty); + uwrite!(self.src, "{{ {field}: &"); + } + uwriteln!(self.src, "v }}, {ret}nil"); } uwrite!( self.src, - r#"v }}, nil - default: - return nil, {fmt}.Errorf("invalid result status byte %d", status) + r#"default: + return nil, {d}{fmt}.Errorf("invalid result status byte %d", status) }} }}({reader}"# ); @@ -772,32 +793,57 @@ impl InterfaceGenerator<'_> { name: &str, ) { let wrpc = self.deps.wrpc(); - + let deferred = fields.iter().any(|Field { ty, .. }| self.is_async(ty)); + let d = if deferred { "nil, " } else { "" }; + uwrite!( + self.src, + "func(r {wrpc}.IndexReadCloser, path ...uint32) (*{name}" + ); + self.print_deferred_ret(deferred); uwriteln!( self.src, - r#"func(r {wrpc}.IndexReadCloser, path ...uint32) (*{name}, error) {{ + r#", error) {{ v := &{name}{{}} var err error"# ); + if deferred { + let context = self.deps.context(); + uwrite!( + self.src, + "var d func({context}.Context) error\nvar ds []func({context}.Context) error\n" + ); + } for (i, Field { name, ty, .. }) in fields.iter().enumerate() { let fmt = self.deps.fmt(); let slog = self.deps.slog(); let ident = name.to_upper_camel_case(); + let field_deferred = self.is_async(ty); uwrite!( self.src, r#"{slog}.Debug("reading field", "name", "{name}") - v.{ident}, err = "# + v.{ident}, "# ); + if field_deferred { + self.push_str("d, "); + } + self.push_str("err = "); self.print_read_ty(ty, "r", &format!("append(path, {i})")); self.push_str("\n"); uwriteln!( self.src, r#"if err != nil {{ - return nil, {fmt}.Errorf("failed to read `{name}` field: %w", err) + return nil, {d}{fmt}.Errorf("failed to read `{name}` field: %w", err) }}"# ); + if field_deferred { + self.push_str("ds = append(ds, d)\n"); + } + } + if deferred { + uwriteln!(self.src, "return v, {wrpc}.JoinDeferred(ds...), nil"); + } else { + self.push_str("return v, nil\n"); } - self.push_str("return v, nil\n"); uwrite!(self.src, "}}({reader}"); if !path.is_empty() { self.src.push_str(", "); @@ -864,19 +910,30 @@ impl InterfaceGenerator<'_> { fn print_read_variant(&mut self, ty: &Variant, reader: &str, path: &str, name: &str) { let fmt = self.deps.fmt(); let wrpc = self.deps.wrpc(); - + let deferred = ty + .cases + .iter() + .any(|Case { ty, .. }| ty.as_ref().is_some_and(|ty| self.is_async(ty))); + let d = if deferred { "nil, " } else { "" }; uwrite!( self.src, - r#"func(r {wrpc}.IndexReadCloser, path ...uint32) (*{name}, error) {{ + "func(r {wrpc}.IndexReadCloser, path ...uint32) (*{name}" + ); + self.print_deferred_ret(deferred); + uwrite!( + self.src, + r#", error) {{ v := &{name}{{}} n, err := "# ); self.print_read_discriminant(ty.tag(), "r"); self.push_str("\n"); - self.push_str("if err != nil {\n"); - self.push_str("return nil, "); - self.push_str(fmt); - self.push_str(".Errorf(\"failed to read discriminant: %w\", err)\n}\n"); + uwriteln!( + self.src, + r#"if err != nil {{ + return nil, {d}{fmt}.Errorf("failed to read discriminant: %w", err) + }}"# + ); uwriteln!(self.src, "switch {name}Discriminant(n) {{"); for Case { name: case_name, @@ -885,28 +942,29 @@ impl InterfaceGenerator<'_> { } in &ty.cases { let camel = case_name.to_upper_camel_case(); - self.push_str("case "); - self.push_str(name); - self.push_str(&camel); - self.push_str(":\n"); + uwriteln!(self.src, "case {name}{camel}:"); if let Some(ty) = ty { - self.push_str("payload, err := "); + let ret = if self.is_async(ty) { + self.push_str("payload, d, err := "); + "d, " + } else { + self.push_str("payload, err := "); + d + }; self.print_read_ty(ty, "r", "path"); self.push_str("\n"); - self.push_str("if err != nil { return nil, "); - self.push_str(fmt); uwriteln!( self.src, - r#".Errorf("failed to read `{case_name}` payload: %w", err) }}"# + r#"if err != nil {{ return nil, {d}{fmt}.Errorf("failed to read `{case_name}` payload: %w", err) }}"# ); - uwriteln!(self.src, "return v.Set{camel}(payload), nil"); + uwriteln!(self.src, "return v.Set{camel}(payload), {ret}nil"); } else { - uwriteln!(self.src, "return v.Set{camel}(), nil"); + uwriteln!(self.src, "return v.Set{camel}(), {d}nil"); } } uwriteln!( self.src, - r#"default: return nil, {fmt}.Errorf("unknown discriminant value %d", n) }}"# + r#"default: return nil, {d}{fmt}.Errorf("unknown discriminant value %d", n) }}"# ); uwrite!(self.src, "}}({reader}"); if !path.is_empty() { @@ -929,35 +987,56 @@ impl InterfaceGenerator<'_> { } _ => { let wrpc = self.deps.wrpc(); - + let deferred = ty.types.iter().any(|ty| self.is_async(ty)); + let d = if deferred { "nil, " } else { "" }; uwrite!( self.src, "func(r {wrpc}.IndexReadCloser, path ...uint32) (*" ); self.print_tuple(ty, true); + self.print_deferred_ret(deferred); self.push_str(", error) {\n"); self.push_str("v := &"); self.print_tuple(ty, false); self.push_str("{}\n"); self.push_str("var err error\n"); + if deferred { + let context = self.deps.context(); + uwrite!( + self.src, + "var d func({context}.Context) error\nvar ds []func({context}.Context) error\n" + ); + } for (i, ty) in ty.types.iter().enumerate() { let fmt = self.deps.fmt(); let slog = self.deps.slog(); + let elem_deferred = self.is_async(ty); uwrite!( self.src, r#"{slog}.Debug("reading tuple element {i}") - v.V{i}, err = "# + v.V{i}, "# ); + if elem_deferred { + self.push_str("d, "); + } + self.push_str("err = "); self.print_read_ty(ty, "r", &format!("append(path, {i})")); self.push_str("\n"); uwriteln!( self.src, r#"if err != nil {{ - return nil, {fmt}.Errorf("failed to read tuple element {i}: %w", err) + return nil, {d}{fmt}.Errorf("failed to read tuple element {i}: %w", err) }}"# ); + if elem_deferred { + self.push_str("ds = append(ds, d)\n"); + } + } + if deferred { + uwriteln!(self.src, "return v, {wrpc}.JoinDeferred(ds...), nil"); + } else { + self.push_str("return v, nil\n"); } - self.push_str("return v, nil\n"); uwrite!(self.src, "}}({reader}"); if !path.is_empty() { self.src.push_str(", "); @@ -970,286 +1049,56 @@ impl InterfaceGenerator<'_> { } fn print_read_future(&mut self, ty: &Option, reader: &str, path: &str) { - match ty { - Some(ty) if is_list_of(self.resolve, Type::U8, ty) => { - let bytes = self.deps.bytes(); - let io = self.deps.io(); - let fmt = self.deps.fmt(); - let slog = self.deps.slog(); - let wrpc = self.deps.wrpc(); - - uwriteln!( - self.src, - r#"func(r {wrpc}.IndexReadCloser, path ...uint32) ({io}.ReadCloser, error) {{ - {slog}.Debug("reading byte list future status byte") - status, err := r.ReadByte() - if err != nil {{ - return nil, {fmt}.Errorf("failed to read byte list future status byte: %w", err) - }} - switch status {{ - case 0: - {slog}.Debug("indexing pending byte list future reader") - if len(path) > 0 {{ - var err error - r, err = r.Index(path...) - if err != nil {{ - return nil, {fmt}.Errorf("failed to index nested byte list future reader: %w", err) - }} - }} - return {wrpc}.NewByteStreamReader(r), nil - case 1: - {slog}.Debug("reading ready byte list future contents") - buf, err := "# - ); - self.print_read_byte_list("r"); - uwrite!( - self.src, - r#" - if err != nil {{ - return nil, {fmt}.Errorf("failed to read ready byte list future contents: %w", err) - }} - {slog}.Debug("read ready byte list future contents", "len", len(buf)) - return {io}.NopCloser({bytes}.NewReader(buf)), nil - default: - return nil, {fmt}.Errorf("invalid byte list future status byte %d", status) - }} -}}({reader}"# - ); - if !path.is_empty() { - self.src.push_str(", "); - self.src.push_str(path); - self.src.push_str("..."); - } - self.src.push_str(")\n"); - } - Some(ty) => { - let fmt = self.deps.fmt(); - let slog = self.deps.slog(); - let wrpc = self.deps.wrpc(); - - uwrite!( - self.src, - r#"func(r {wrpc}.IndexReadCloser, path ...uint32) ({wrpc}.Receiver["# - ); - self.print_opt_ty(ty, true); - uwrite!( - self.src, - r#"], error) {{ - {slog}.Debug("reading future status byte") - status, err := r.ReadByte() - if err != nil {{ - return nil, {fmt}.Errorf("failed to read future status byte: %w", err) - }} - switch status {{ - case 0: - {slog}.Debug("indexing pending future reader") - if len(path) > 0 {{ - var err error - r, err = r.Index(path...) - if err != nil {{ - return nil, {fmt}.Errorf("failed to index nested future reader: %w", err) - }} - }} - return {wrpc}.NewDecodeReceiver(r, func(r {wrpc}.IndexReadCloser) ("# - ); - self.print_opt_ty(ty, true); - uwrite!( - self.src, - r#", error) {{ - {slog}.Debug("reading pending future element") - v, err := "# - ); - self.print_read_ty(ty, "r", ""); - uwriteln!( - self.src, - r#" - if err != nil {{ - return v, {fmt}.Errorf("failed to read pending future element: %w", err) - }} - return v, nil - }}), nil - case 1: - {slog}.Debug("reading ready future contents") - v, err := "# - ); - self.print_read_ty(ty, "r", "path"); - uwrite!( - self.src, - r#" - if err != nil {{ - return nil, {fmt}.Errorf("failed to read ready future contents: %w", err) - }} - return {wrpc}.NewCompleteReceiver(v), nil - default: - return nil, {fmt}.Errorf("invalid future status byte %d", status) - }} -}}({reader}"# - ); - if !path.is_empty() { - self.src.push_str(", "); - self.src.push_str(path); - self.src.push_str("..."); - } - self.src.push_str(")"); - } - None => panic!("futures with no element types are not supported"), + let Some(ty) = ty else { + panic!("futures with no element types are not supported") + }; + let wrpc = self.deps.wrpc(); + uwrite!(self.src, "{wrpc}.ReadFuture({reader}, "); + self.print_read_element(ty); + if !path.is_empty() { + self.src.push_str(", "); + self.src.push_str(path); + self.src.push_str("..."); } + self.src.push_str(")"); } - fn print_read_stream(&mut self, element: &Option, reader: &str, path: &str) { - match element { - Some(ty) if is_ty(self.resolve, Type::U8, ty) => { - let bytes = self.deps.bytes(); - let io = self.deps.io(); - let fmt = self.deps.fmt(); - let slog = self.deps.slog(); - let wrpc = self.deps.wrpc(); - - uwriteln!( - self.src, - r#"func(r {wrpc}.IndexReadCloser, path ...uint32) ({io}.ReadCloser, error) {{ - {slog}.Debug("reading byte stream status byte") - status, err := r.ReadByte() - if err != nil {{ - return nil, {fmt}.Errorf("failed to read byte stream status byte: %w", err) - }} - switch status {{ - case 0: - if len(path) > 0 {{ - var err error - r, err = r.Index(path...) - if err != nil {{ - return nil, {fmt}.Errorf("failed to index nested byte stream reader: %w", err) - }} - }} - return {wrpc}.NewByteStreamReader(r), nil - case 1: - {slog}.Debug("reading ready byte stream contents") - buf, err := "# - ); - self.print_read_byte_list("r"); - uwrite!( - self.src, - r#" - if err != nil {{ - return nil, {fmt}.Errorf("failed to read ready byte stream contents: %w", err) - }} - {slog}.Debug("read ready byte stream contents", "len", len(buf)) - return {io}.NopCloser({bytes}.NewReader(buf)), nil - default: - return nil, {fmt}.Errorf("invalid stream status byte %d", status) - }} -}}({reader}"# - ); - if !path.is_empty() { - self.src.push_str(", "); - self.src.push_str(path); - self.src.push_str("..."); - } - self.src.push_str(")"); - } - Some(ty) => { - let errors = self.deps.errors(); - let fmt = self.deps.fmt(); - let io = self.deps.io(); - let math = self.deps.math(); - let slog = self.deps.slog(); - let wrpc = self.deps.wrpc(); - - uwrite!( - self.src, - r#"func(r {wrpc}.IndexReadCloser, path ...uint32) ({wrpc}.Receiver["# - ); - self.print_list(ty); - uwrite!( - self.src, - r#"], error) {{ - {slog}.Debug("reading stream status byte") - status, err := r.ReadByte() - if err != nil {{ - return nil, {fmt}.Errorf("failed to read stream status byte: %w", err) - }} - switch status {{ - case 0: - if len(path) > 0 {{ - var err error - r, err = r.Index(path...) - if err != nil {{ - return nil, {fmt}.Errorf("failed to index nested stream reader: %w", err) - }} - }} - var total uint32 - return {wrpc}.NewDecodeReceiver(r, func(r {wrpc}.IndexReadCloser) ("# - ); - self.print_list(ty); - uwrite!( - self.src, - r#", error) {{ - {slog}.Debug("reading pending stream chunk length") - n, err := "# - ); - self.print_read_u32("r"); - uwrite!( - self.src, - r#" - if err != nil {{ - return nil, {fmt}.Errorf("failed to read pending stream chunk length: %w", err) - }} - if n == 0 {{ - return nil, {io}.EOF - }} - if {math}.MaxUint32 - n < total {{ - return nil, {errors}.New("total incoming pending stream element count would overflow a 32-bit unsigned integer") - }} - vs := {wrpc}.NewSlice["# - ); - self.print_list(ty); - uwrite!( - self.src, - r#"](n) - for i := range n {{ - {slog}.Debug("reading pending stream element", "i", total) - v, err := "# - ); - self.print_read_ty(ty, "r", "[]uint32{total}"); - uwriteln!( - self.src, - r#" - if err != nil {{ - return nil, {fmt}.Errorf("failed to read pending stream chunk element %d: %w", i, err) - }} - vs = append(vs, v) - total++ - }} - return vs, nil - }}), nil - case 1: - {slog}.Debug("reading ready stream contents") - vs, err := "# - ); - self.print_read_list(ty, "r", "path"); - uwrite!( - self.src, - r#" - if err != nil {{ - return nil, {fmt}.Errorf("failed to read ready stream contents: %w", err) - }} - {slog}.Debug("read ready stream contents", "len", len(vs)) - return {wrpc}.NewCompleteReceiver(vs), nil - default: - return nil, {fmt}.Errorf("invalid stream status byte %d", status) - }} -}}({reader}"# - ); - if !path.is_empty() { - self.src.push_str(", "); - self.src.push_str(path); - self.src.push_str("..."); - } - self.src.push_str(")"); + fn print_read_element(&mut self, ty: &Type) { + let wrpc = self.deps.wrpc(); + uwrite!(self.src, "func(r {wrpc}.IndexReadCloser, path ...uint32) ("); + self.print_opt_ty(ty, true); + let context = self.deps.context(); + uwrite!(self.src, ", func({context}.Context) error, error) {{\n"); + if self.is_async(ty) { + self.push_str("return "); + self.print_read_ty(ty, "r", "path"); + self.push_str("\n"); + } else { + self.push_str("v, err := "); + self.print_read_ty(ty, "r", "path"); + self.push_str("\nreturn v, nil, err\n"); + } + self.push_str("}"); + } + + fn print_read_stream(&mut self, element: &Option, reader: &str, path: &str) { + let wrpc = self.deps.wrpc(); + match element { + Some(ty) if is_ty(self.resolve, Type::U8, ty) => { + uwrite!(self.src, "{wrpc}.ReadByteStream({reader}"); + } + Some(ty) => { + uwrite!(self.src, "{wrpc}.ReadStream({reader}, "); + self.print_read_element(ty); } None => panic!("streams with no element types are not supported"), } + if !path.is_empty() { + self.src.push_str(", "); + self.src.push_str(path); + self.src.push_str("..."); + } + self.src.push_str(")"); } fn print_read_own(&mut self, reader: &str, id: TypeId) { @@ -1404,17 +1253,27 @@ impl InterfaceGenerator<'_> { TypeDefKind::Stream(ty) => self.print_read_stream(ty, reader, path), TypeDefKind::Type(ty) => { if let Some(name) = name { + let deferred = self.is_async(ty); self.push_str("func() ("); self.print_opt_ptr(ty, true); self.push_str(&name); + self.print_deferred_ret(deferred); self.push_str(", error) {\n"); - self.push_str("v, err :="); + if deferred { + self.push_str("v, d, err :="); + } else { + self.push_str("v, err :="); + } self.print_read_ty(ty, reader, path); self.push_str("\n"); self.push_str("return ("); self.print_opt_ptr(ty, true); self.push_str(&name); - self.push_str(")(v), err\n"); + if deferred { + self.push_str(")(v), d, err\n"); + } else { + self.push_str(")(v), err\n"); + } self.push_str("}()\n"); } else { self.print_read_ty(ty, reader, path); @@ -1734,7 +1593,7 @@ impl InterfaceGenerator<'_> { if len(writes) > 0 {{ return func(w {wrpc}.IndexWriter) error {{ var wg {sync}.WaitGroup - var wgErr {atomic}.Value + var wgErr {atomic}.Pointer[error] for index, write := range writes {{ wg.Add(1) w, err := w.Index(index) @@ -1745,16 +1604,15 @@ impl InterfaceGenerator<'_> { go func() {{ defer wg.Done() if err := write(w); err != nil {{ - wgErr.Store(err) + wgErr.Store(&err) }} }}() }} wg.Wait() - err := wgErr.Load() - if err == nil {{ - return nil + if err := wgErr.Load(); err != nil {{ + return *err }} - return err.(error) + return nil }}, nil }} return nil, nil @@ -1961,7 +1819,7 @@ impl InterfaceGenerator<'_> { r#"if len(writes) > 0 {{ return func(w {wrpc}.IndexWriter) error {{ var wg {sync}.WaitGroup - var wgErr {atomic}.Value + var wgErr {atomic}.Pointer[error] for index, write := range writes {{ wg.Add(1) w, err := w.Index(index) @@ -1972,16 +1830,15 @@ impl InterfaceGenerator<'_> { go func() {{ defer wg.Done() if err := write(w); err != nil {{ - wgErr.Store(err) + wgErr.Store(&err) }} }}() }} wg.Wait() - err := wgErr.Load() - if err == nil {{ - return nil + if err := wgErr.Load(); err != nil {{ + return *err }} - return err.(error) + return nil }}, nil }} return nil, nil @@ -1992,267 +1849,37 @@ impl InterfaceGenerator<'_> { } fn print_write_future(&mut self, ty: &Option, name: &str, writer: &str) { - match ty { - Some(ty) if is_list_of(self.resolve, Type::U8, ty) => { - let fmt = self.deps.fmt(); - let io = self.deps.io(); - let math = self.deps.math(); - let slog = self.deps.slog(); - let wrpc = self.deps.wrpc(); - uwrite!( - self.src, - r#"func(v {io}.Reader, w interface {{ {io}.ByteWriter; {io}.Writer }}) (write func({wrpc}.IndexWriter) error, err error) {{ - {slog}.Debug("writing byte list future `future::pending` status byte") - if err = w.WriteByte(0); err != nil {{ - return nil, fmt.Errorf("failed to write `future::pending` byte: %w", err) - }} - return func(w {wrpc}.IndexWriter) (err error) {{ - defer func() {{ - body, ok := v.({io}.Closer) - if ok {{ - {slog}.Debug("closing byte list future reader") - if cErr := body.Close(); cErr != nil {{ - if err == nil {{ - err = {fmt}.Errorf("failed to close pending byte list future: %w", cErr) - }} else {{ - {slog}.Warn("failed to close pending byte list future", "err", cErr) - }} - }} - }} - }}() - {slog}.Debug("reading pending byte list future contents") - chunk, err := {io}.ReadAll(chunk) - if err != nil {{ - return {fmt}.Errorf("failed to read pending byte list future: %w", err) - }} - if n > {math}.MaxUint32 {{ - return {fmt}.Errorf("pending byte list future length of %d overflows a 32-bit integer", n) - }} - {slog}.Debug("writing pending byte list future length", "len", n) - _, err = {wrpc}.WriteUint32(uint32(n), w) - if err != nil {{ - return {fmt}.Errorf("failed to write pending byte list future length of %d: %w", n, err) - }} - {slog}.Debug("writing pending byte list future contents", "buf", chunk[:n]) - _, err = w.Write(chunk[:n]) - if err != nil {{ - return {fmt}.Errorf("failed to write pending byte list future contents: %w", err) - }} - }}, nil - }}({name}, {writer})"#, - ); - } - Some(ty) => { - let fmt = self.deps.fmt(); - let io = self.deps.io(); - let slog = self.deps.slog(); - let wrpc = self.deps.wrpc(); - uwrite!(self.src, "func(v {wrpc}.Receiver[",); - self.print_opt_ty(ty, true); - uwrite!( - self.src, - r#"], w interface {{ {io}.ByteWriter; {io}.Writer }}) (write func({wrpc}.IndexWriter) error, err error) {{ - {slog}.Debug("writing future `future::pending` status byte") - if err := w.WriteByte(0); err != nil {{ - return nil, fmt.Errorf("failed to write `future::pending` byte: %w", err) - }} - return func(w {wrpc}.IndexWriter) (err error) {{ - defer func() {{ - {slog}.Debug("closing future writer") - if cErr := v.Close(); cErr != nil {{ - if err == nil {{ - err = {fmt}.Errorf("failed to close pending future: %w", cErr) - }} else {{ - {slog}.Warn("failed to close pending future", "err", cErr) - }} - }} - }}() - {slog}.Debug("receiving outgoing pending future contents") - rx, err := v.Receive() - if err != nil {{ - return {fmt}.Errorf("failed to receive outgoing pending future: %w", err) - }} - {slog}.Debug("writing pending future element") - write, err :="#, - ); - self.print_write_ty(ty, "rx", "w"); - uwrite!( - self.src, - r#" - if err != nil {{ - return {fmt}.Errorf("failed to write pending future element: %w", err) - }} - if write != nil {{ - return write(w) - }} - return nil - }}, nil - }}({name}, {writer})"#, - ); - } - None => panic!("streams with no element types are not supported"), - } + let Some(ty) = ty else { + panic!("futures with no element types are not supported") + }; + let wrpc = self.deps.wrpc(); + uwrite!(self.src, "{wrpc}.WriteFuture({name}, {writer}, "); + self.print_write_element(ty); + self.push_str(")"); + } + + fn print_write_element(&mut self, ty: &Type) { + let wrpc = self.deps.wrpc(); + self.push_str("func(v "); + self.print_opt_ty(ty, true); + uwrite!( + self.src, + ", w {wrpc}.ByteWriter) (func({wrpc}.IndexWriter) error, error) {{\nreturn " + ); + self.print_write_ty(ty, "v", "w"); + self.push_str("\n}"); } fn print_write_stream(&mut self, element: &Option, name: &str, writer: &str) { + let wrpc = self.deps.wrpc(); match element { Some(ty) if is_ty(self.resolve, Type::U8, ty) => { - let fmt = self.deps.fmt(); - let io = self.deps.io(); - let math = self.deps.math(); - let slog = self.deps.slog(); - let wrpc = self.deps.wrpc(); - uwrite!( - self.src, - r#"func(v {io}.ReadCloser, w interface {{ {io}.ByteWriter; {io}.Writer }}) (write func({wrpc}.IndexWriter) error, err error) {{ - {slog}.Debug("writing byte stream `stream::pending` status byte") - if err = w.WriteByte(0); err != nil {{ - return nil, fmt.Errorf("failed to write `stream::pending` byte: %w", err) - }} - return func(w {wrpc}.IndexWriter) (err error) {{ - defer func() {{ - {slog}.Debug("closing byte list stream writer") - if cErr := v.Close(); cErr != nil {{ - if err == nil {{ - err = {fmt}.Errorf("failed to close pending byte stream: %w", cErr) - }} else {{ - {slog}.Warn("failed to close pending byte stream", "err", cErr) - }} - }} - }}() - chunk := make([]byte, {wrpc}.ReadChunkSize) - for {{ - var end bool - {slog}.Debug("reading pending byte stream contents") - n, err := v.Read(chunk) - if err == {io}.EOF {{ - end = true - {slog}.Debug("pending byte stream reached EOF") - }} else if err != nil {{ - return {fmt}.Errorf("failed to read pending byte stream chunk: %w", err) - }} - if n > {math}.MaxUint32 {{ - return {fmt}.Errorf("pending byte stream chunk length of %d overflows a 32-bit integer", n) - }} - if n > 0 {{ - {slog}.Debug("writing pending byte stream chunk length", "len", n) - _, err = {wrpc}.WriteUint32(uint32(n), w) - if err != nil {{ - return {fmt}.Errorf("failed to write pending byte stream chunk length of %d: %w", n, err) - }} - _, err = w.Write(chunk[:n]) - if err != nil {{ - return {fmt}.Errorf("failed to write pending byte stream chunk contents: %w", err) - }} - }} - if end {{ - if err := w.WriteByte(0); err != nil {{ - return {fmt}.Errorf("failed to write pending byte stream end byte: %w", err) - }} - return nil - }} - }} - }}, nil - }}({name}, {writer})"#, - ); + uwrite!(self.src, "{wrpc}.WriteByteStream({name}, {writer})"); } Some(ty) => { - let atomic = self.deps.atomic(); - let errors = self.deps.errors(); - let fmt = self.deps.fmt(); - let io = self.deps.io(); - let math = self.deps.math(); - let slog = self.deps.slog(); - let sync = self.deps.sync(); - let wrpc = self.deps.wrpc(); - uwrite!(self.src, "func(v {wrpc}.Receiver[",); - self.print_list(ty); - uwrite!( - self.src, - r#"], w interface {{ {io}.ByteWriter; {io}.Writer }}) (write func({wrpc}.IndexWriter) error, err error) {{ - {slog}.Debug("writing stream `stream::pending` status byte") - if err := w.WriteByte(0); err != nil {{ - return nil, fmt.Errorf("failed to write `stream::pending` byte: %w", err) - }} - return func(w {wrpc}.IndexWriter) (err error) {{ - defer func() {{ - {slog}.Debug("closing outgoing pending stream") - if cErr := v.Close(); cErr != nil {{ - if err == nil {{ - err = {fmt}.Errorf("failed to close outgoing pending stream: %w", cErr) - }} else {{ - {slog}.Warn("failed to close outgoing pending stream", "err", cErr) - }} - }} - }}() - var wg {sync}.WaitGroup - var wgErr {atomic}.Value - var total uint32 - for {{ - var end bool - {slog}.Debug("receiving outgoing pending stream contents") - chunk, err := v.Receive() - n := len(chunk) - if err == {io}.EOF {{ - end = true - {slog}.Debug("outgoing pending stream reached EOF") - }} else if err != nil {{ - return {fmt}.Errorf("failed to receive outgoing pending stream chunk: %w", err) - }} - if n > {math}.MaxUint32 {{ - return {fmt}.Errorf("outgoing pending stream chunk length of %d overflows a 32-bit integer", n) - }} - if {math}.MaxUint32 - uint32(n) < total {{ - return {errors}.New("total outgoing pending stream element count would overflow a 32-bit unsigned integer") - }} - if n > 0 {{ - {slog}.Debug("writing pending stream chunk length", "len", n) - _, err = {wrpc}.WriteUint32(uint32(n), w) - if err != nil {{ - return {fmt}.Errorf("failed to write pending stream chunk length of %d: %w", n, err) - }} - for _, v := range chunk {{ - {slog}.Debug("writing pending stream element", "i", total) - write, err :="#, - ); - self.print_write_ty(ty, "v", "w"); - uwrite!( - self.src, - r#" - if err != nil {{ - return {fmt}.Errorf("failed to write pending stream chunk element %d: %w", total, err) - }} - if write != nil {{ - wg.Add(1) - w, err := w.Index(total) - if err != nil {{ - return {fmt}.Errorf("failed to index nested stream writer: %w", err) - }} - go func() {{ - defer wg.Done() - if err := write(w); err != nil {{ - wgErr.Store(err) - }} - }}() - }} - total++ - }} - }} - if end {{ - if err := w.WriteByte(0); err != nil {{ - return {fmt}.Errorf("failed to write pending stream end byte: %w", err) - }} - wg.Wait() - err := wgErr.Load() - if err == nil {{ - return nil - }} - return err.(error) - }} - }} - }}, nil - }}({name}, {writer})"#, - ); + uwrite!(self.src, "{wrpc}.WriteStream({name}, {writer}, "); + self.print_write_element(ty); + self.push_str(")"); } None => panic!("streams with no element types are not supported"), } @@ -2566,12 +2193,33 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { }} }}()"#, ); + let async_params = func.params.iter().any(|Param { ty, .. }| self.is_async(ty)); + if async_params { + let sync = self.deps.sync(); + uwrite!( + self.src, + r#" + ctx, cancel := {context}.WithCancel(ctx) + var wg {sync}.WaitGroup + defer func() {{ + go func() {{ + wg.Wait() + cancel() + }}() + }}()"#, + ); + } for (i, Param { ty, .. }) in func.params.iter().enumerate() { + let d = if self.is_async(ty) { + format!("d{i}, ") + } else { + String::new() + }; uwrite!( self.src, r#" {slog}.DebugContext(ctx, "reading parameter", "i", {i}) - p{i}, err := "# + p{i}, {d}err := "# ); self.print_read_ty(ty, "r", &format!("[]uint32{{ {i} }}")); self.push_str("\n"); @@ -2587,6 +2235,21 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { }}"#, ); } + for (i, Param { ty, .. }) in func.params.iter().enumerate() { + if self.is_async(ty) { + uwrite!( + self.src, + r#" + if d{i} != nil {{ + go func() {{ + if err := d{i}(ctx); err != nil {{ + {slog}.WarnContext(ctx, "failed to read deferred parameter", "i", {i}, "instance", "{instance}", "name", "{name}", "err", err) + }} + }}() + }}"#, + ); + } + } uwriteln!( self.src, r#" @@ -2614,7 +2277,7 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { self.src, r#") if cErr := r.Close(); cErr != nil {{ - {slog}.ErrorContext(ctx, "failed to close reader", "instance", "{instance}", "name", "{name}", "err", err) + {slog}.ErrorContext(ctx, "failed to close reader", "instance", "{instance}", "name", "{name}", "err", cErr) }} if err != nil {{ {slog}.WarnContext(ctx, "failed to handle invocation", "instance", "{instance}", "name", "{name}", "err", err) @@ -2678,8 +2341,27 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { {slog}.ErrorContext(ctx, "failed to index result writer", "instance", "{instance}", "name", "{name}", "err", err) return }} - write := write - go func() {{ + write := write"#, + ); + if async_params { + self.push_str( + r#" + wg.Add(1)"#, + ); + } + self.push_str( + r#" + go func() {"#, + ); + if async_params { + self.push_str( + r#" + defer wg.Done()"#, + ); + } + uwrite!( + self.src, + r#" if err := write(w); err != nil {{ {slog}.WarnContext(ctx, "failed to write nested result value", "instance", "{instance}", "name", "{name}", "err", err) }} @@ -2758,12 +2440,10 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { self.src.push_str(", "); } - let async_params = func.params.iter().any(|param| { - let (paths, fut) = async_paths_ty(self.resolve, ¶m.ty); - fut || !paths.is_empty() - }); - if async_params { - self.push_str("writeErrs__ <-chan error, "); + let async_params = func.params.iter().any(|param| self.is_async(¶m.ty)); + let async_results = results.iter().filter(|ty| self.is_async(ty)).count(); + if async_params || async_results > 0 { + self.push_str("errs__ <-chan error, "); } self.push_str("err__ error) {"); if !func.params.is_empty() { @@ -2888,17 +2568,33 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { }}()"#, name = func.name, ); - if async_params { + if async_params || async_results > 0 { let sync = self.deps.sync(); + let count = if async_params { + format!("uint(writeCount__) + {async_results}") + } else { + async_results.to_string() + }; + uwrite!( + self.src, + r#" + errCh__ := make(chan error, {count}) + errs__ = errCh__ + var wg__ {sync}.WaitGroup + defer func() {{ + go func() {{ + wg__.Wait() + close(errCh__) + }}() + }}()"# + ); + } + if async_params { uwrite!( self.src, r#" if writeCount__ > 0 {{ - writeErrCh__ := make(chan error, uint(writeCount__)) - writeErrs__ = writeErrCh__ - var wg__ {sync}.WaitGroup for index, write := range writes__ {{ - wg__.Add(1) w, err := w__.Index(index) if err != nil {{ if cErr := w__.Close(); cErr != nil {{ @@ -2907,18 +2603,15 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { err__ = {fmt}.Errorf("failed to index param writer at index `%v`: %w", index, err) return }} + wg__.Add(1) write := write go func() {{ defer wg__.Done() if err := write(w); err != nil {{ - writeErrCh__ <- err + errCh__ <- err }} }}() }} - go func() {{ - wg__.Wait() - close(writeErrCh__) - }}() }}"#, func.name, ); @@ -2933,13 +2626,25 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { ); let mut idx = 0usize; + let mut deferreds = Vec::default(); for (i, rty) in func.result.iter().enumerate() { for (j, ty) in flatten_ty(self.resolve, rty).enumerate() { - uwrite!( - self.src, - " + let deferred = self.is_async(&ty); + if deferred { + let context = self.deps.context(); + uwrite!( + self.src, + " + var d{idx}__ func({context}.Context) error + r{idx}__, d{idx}__, err__ = " + ); + } else { + uwrite!( + self.src, + " r{idx}__, err__ = " - ); + ); + } let path = if is_tuple(self.resolve, rty) { format!("[]uint32{{ {i}, {j} }}") } else { @@ -2954,9 +2659,27 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { return }}"#, ); + if deferred { + deferreds.push(idx); + } idx = idx.saturating_add(1); } } + for idx in deferreds { + uwrite!( + self.src, + r#" + if d{idx}__ != nil {{ + wg__.Add(1) + go func() {{ + defer wg__.Done() + if err := d{idx}__(ctx__); err != nil {{ + errCh__ <- err + }} + }}() + }}"#, + ); + } if func.result.is_none() { // A function with no results still completes with an (empty) // result transmission; await it so the call is synchronous and @@ -3137,6 +2860,18 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { name } + fn print_deferred_ret(&mut self, deferred: bool) { + if deferred { + let context = self.deps.context(); + uwrite!(self.src, ", func({context}.Context) error"); + } + } + + fn is_async(&self, ty: &Type) -> bool { + let (paths, fut) = async_paths_ty(self.resolve, ty); + fut || !paths.is_empty() + } + fn nillable_ptr(&self, ty: &Type, result: bool, decl: bool) -> &'static str { if let Type::Id(id) = ty { match &self.resolve.types[*id].kind { @@ -3248,7 +2983,7 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { && let TypeDefKind::List(..) | TypeDefKind::Future(..) | TypeDefKind::Stream(..) = self.resolve.types[*id].kind { - // Go slices, channels and interfaces are nillable + // Go slices and channels are nillable self.print_ty(ty, true); return; } @@ -3294,43 +3029,19 @@ func ServeInterface(s {wrpc}.Server, h Handler) (stop func() error, err error) { } fn print_future(&mut self, ty: &Option) { - match ty { - Some(ty) if is_ty(self.resolve, Type::U8, ty) => { - let io = self.deps.io(); - self.push_str(io); - self.push_str(".ReadCloser"); - } - Some(ty) => { - let wrpc = self.deps.wrpc(); - self.push_str(wrpc); - self.push_str(".Receiver["); - self.print_opt_ty(ty, true); - self.push_str("]"); - } - None => { - panic!("futures with no element types are not supported") - } - } + let Some(ty) = ty else { + panic!("futures with no element types are not supported") + }; + self.push_str("<-chan "); + self.print_opt_ty(ty, true); } fn print_stream(&mut self, element: &Option) { - match element { - Some(ty) if is_ty(self.resolve, Type::U8, ty) => { - let io = self.deps.io(); - self.push_str(io); - self.push_str(".ReadCloser"); - } - Some(ty) => { - let wrpc = self.deps.wrpc(); - self.push_str(wrpc); - self.push_str(".Receiver["); - self.print_list(ty); - self.push_str("]"); - } - None => { - panic!("streams with no element types are not supported") - } - } + let Some(ty) = element else { + panic!("streams with no element types are not supported") + }; + self.push_str("<-chan "); + self.print_list(ty); } fn print_own(&mut self, id: TypeId) { @@ -3485,7 +3196,7 @@ func (v *{name}) WriteToIndex(w {wrpc}.ByteWriter) (func({wrpc}.IndexWriter) err if len(writes) > 0 {{ return func(w {wrpc}.IndexWriter) error {{ var wg {sync}.WaitGroup - var wgErr {atomic}.Value + var wgErr {atomic}.Pointer[error] for index, write := range writes {{ wg.Add(1) w, err := w.Index(index) @@ -3496,16 +3207,15 @@ func (v *{name}) WriteToIndex(w {wrpc}.ByteWriter) (func({wrpc}.IndexWriter) err go func() {{ defer wg.Done() if err := write(w); err != nil {{ - wgErr.Store(err) + wgErr.Store(&err) }} }}() }} wg.Wait() - err := wgErr.Load() - if err == nil {{ - return nil + if err := wgErr.Load(); err != nil {{ + return *err }} - return err.(error) + return nil }}, nil }} return nil, nil diff --git a/crates/wit-bindgen-test/src/go.rs b/crates/wit-bindgen-test/src/go.rs index 16a87677b..f85a20c76 100644 --- a/crates/wit-bindgen-test/src/go.rs +++ b/crates/wit-bindgen-test/src/go.rs @@ -35,14 +35,12 @@ impl LanguageMethods for Go { _args: &[String], ) -> bool { // The wRPC Go generator does not yet support bare `stream`/`future` - // types, `error-context`, or futures held by resources. + // types, `error-context`, named fixed-length lists, or `map`. matches!( name, "streams.wit" | "futures.wit" | "error-context.wit" - | "resources-with-futures.wit" - | "future-same-type-different-names.wit" | "named-fixed-length-list.wit" | "map.wit" ) diff --git a/examples/go/hello-server/bindings/exports/wrpc_examples/hello/handler/bindings.wrpc.go b/examples/go/hello-server/bindings/exports/wrpc_examples/hello/handler/bindings.wrpc.go index 91daab8cb..925871ddb 100644 --- a/examples/go/hello-server/bindings/exports/wrpc_examples/hello/handler/bindings.wrpc.go +++ b/examples/go/hello-server/bindings/exports/wrpc_examples/hello/handler/bindings.wrpc.go @@ -36,7 +36,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wrpc-examples:hello/handler.hello` handler") r0, err := h.Hello(ctx) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:hello/handler", "name", "hello", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:hello/handler", "name", "hello", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wrpc-examples:hello/handler", "name", "hello", "err", err) diff --git a/examples/go/resources-server/bindings/exports/wrpc_examples/resources/resources/bindings.wrpc.go b/examples/go/resources-server/bindings/exports/wrpc_examples/resources/resources/bindings.wrpc.go index 9bc0d36a9..0d7441e36 100644 --- a/examples/go/resources-server/bindings/exports/wrpc_examples/resources/resources/bindings.wrpc.go +++ b/examples/go/resources-server/bindings/exports/wrpc_examples/resources/resources/bindings.wrpc.go @@ -41,7 +41,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wrpc-examples:resources/resources.foo` handler") r0, err := h.Foo(ctx) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:resources/resources", "name", "foo", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:resources/resources", "name", "foo", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wrpc-examples:resources/resources", "name", "foo", "err", err) @@ -160,7 +160,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wrpc-examples:resources/resources.foo.baz` handler") r0, err := h.Foo_Baz(ctx, p0) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:resources/resources", "name", "foo.baz", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:resources/resources", "name", "foo.baz", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wrpc-examples:resources/resources", "name", "foo.baz", "err", err) @@ -279,7 +279,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wrpc-examples:resources/resources.foo.bar` handler") r0, err := h.Foo_Bar(ctx, p0) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:resources/resources", "name", "foo.bar", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:resources/resources", "name", "foo.bar", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wrpc-examples:resources/resources", "name", "foo.bar", "err", err) @@ -398,7 +398,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wrpc-examples:resources/resources.bar` handler") r0, err := h.Bar(ctx, p0) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:resources/resources", "name", "bar", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wrpc-examples:resources/resources", "name", "bar", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wrpc-examples:resources/resources", "name", "bar", "err", err) diff --git a/examples/go/streams-client/app.go b/examples/go/streams-client/app.go index dfa3c0d90..dc92731dd 100644 --- a/examples/go/streams-client/app.go +++ b/examples/go/streams-client/app.go @@ -5,7 +5,6 @@ package app import ( "context" "fmt" - "io" "log/slog" "os" "sync" @@ -26,57 +25,24 @@ func init() { }))) } -type ThrottleStream[T any] struct { - tick *time.Ticker - values []T -} - -func (s *ThrottleStream[T]) Receive() ([]T, error) { - if len(s.values) == 0 { - return nil, io.EOF - } - <-s.tick.C - var v T - v, s.values = s.values[0], s.values[1:] - return []T{v}, nil -} - -func (s *ThrottleStream[T]) Close() error { - s.tick.Stop() - return nil -} - -type ThrottleReader struct { - tick *time.Ticker - buf []byte -} - -func (s *ThrottleReader) Read(p []byte) (int, error) { - if len(s.buf) == 0 { - return 0, io.EOF - } - <-s.tick.C - var b byte - b, s.buf = s.buf[0], s.buf[1:] - p[0] = b - return 1, nil -} - -func (s *ThrottleReader) Close() error { - s.tick.Stop() - return nil +func throttle[T any](values []T, d time.Duration) <-chan []T { + ch := make(chan []T) + go func() { + defer close(ch) + tick := time.NewTicker(d) + defer tick.Stop() + for _, v := range values { + <-tick.C + ch <- []T{v} + } + }() + return ch } func Run(prefix string, client wrpc.Invoker) error { numbers, bytes, errCh, err := handler.Echo(context.Background(), client, &handler.Req{ - Numbers: &ThrottleStream[uint64]{ - tick: time.NewTicker(time.Second), - values: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, - }, - Bytes: &ThrottleReader{ - tick: time.NewTicker(time.Second), - buf: []byte("foo bar baz"), - }, + Numbers: throttle([]uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, time.Second), + Bytes: throttle([]byte("foo bar baz"), time.Second), }) if err != nil { return fmt.Errorf("failed to call `wrpc-examples:streams/handler.echo`: %w", err) @@ -87,38 +53,19 @@ func Run(prefix string, client wrpc.Invoker) error { wg.Add(1) go func() { defer wg.Done() - defer numbers.Close() - for { - chunk, err := numbers.Receive() - if err == io.EOF { - return - } - if err != nil { - slog.Error("failed to receive number chunk", "err", err) - return - } + for chunk := range numbers { fmt.Printf("%s numbers: %v\n", prefix, chunk) } }() wg.Add(1) go func() { defer wg.Done() - defer bytes.Close() - var chunk [128]byte - for { - n, err := bytes.Read(chunk[:]) - if err == io.EOF { - return - } - if err != nil { - slog.Error("failed to receive byte chunk", "err", err) - return - } - fmt.Printf("%s bytes: %s\n", prefix, chunk[:n]) + for chunk := range bytes { + fmt.Printf("%s bytes: %s\n", prefix, chunk) } }() for err := range errCh { - slog.Error("failed to transmit async parameters", "err", err) + slog.Error("failed to exchange async values", "err", err) } return nil } diff --git a/examples/go/streams-client/bindings/wrpc_examples/streams/handler/bindings.wrpc.go b/examples/go/streams-client/bindings/wrpc_examples/streams/handler/bindings.wrpc.go index 881eb6496..fb859f1c8 100644 --- a/examples/go/streams-client/bindings/wrpc_examples/streams/handler/bindings.wrpc.go +++ b/examples/go/streams-client/bindings/wrpc_examples/streams/handler/bindings.wrpc.go @@ -9,15 +9,14 @@ import ( fmt "fmt" io "io" slog "log/slog" - math "math" sync "sync" atomic "sync/atomic" wrpc "wrpc.io/go" ) type Req struct { - Numbers wrpc.Receiver[[]uint64] - Bytes io.ReadCloser + Numbers <-chan []uint64 + Bytes <-chan []uint8 } func (v *Req) String() string { return "Req" } @@ -25,93 +24,15 @@ func (v *Req) String() string { return "Req" } func (v *Req) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, error) { writes := make(map[uint32]func(wrpc.IndexWriter) error, 2) slog.Debug("writing field", "name", "numbers") - write0, err := func(v wrpc.Receiver[[]uint64], w interface { - io.ByteWriter - io.Writer - }) (write func(wrpc.IndexWriter) error, err error) { - slog.Debug("writing stream `stream::pending` status byte") - if err := w.WriteByte(0); err != nil { - return nil, fmt.Errorf("failed to write `stream::pending` byte: %w", err) - } - return func(w wrpc.IndexWriter) (err error) { - defer func() { - slog.Debug("closing outgoing pending stream") - if cErr := v.Close(); cErr != nil { - if err == nil { - err = fmt.Errorf("failed to close outgoing pending stream: %w", cErr) - } else { - slog.Warn("failed to close outgoing pending stream", "err", cErr) - } - } - }() - var wg sync.WaitGroup - var wgErr atomic.Value - var total uint32 - for { - var end bool - slog.Debug("receiving outgoing pending stream contents") - chunk, err := v.Receive() - n := len(chunk) - if err == io.EOF { - end = true - slog.Debug("outgoing pending stream reached EOF") - } else if err != nil { - return fmt.Errorf("failed to receive outgoing pending stream chunk: %w", err) - } - if n > math.MaxUint32 { - return fmt.Errorf("outgoing pending stream chunk length of %d overflows a 32-bit integer", n) - } - if math.MaxUint32-uint32(n) < total { - return errors.New("total outgoing pending stream element count would overflow a 32-bit unsigned integer") - } - if n > 0 { - slog.Debug("writing pending stream chunk length", "len", n) - _, err = wrpc.WriteUint32(uint32(n), w) - if err != nil { - return fmt.Errorf("failed to write pending stream chunk length of %d: %w", n, err) - } - for _, v := range chunk { - slog.Debug("writing pending stream element", "i", total) - write, err := (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) { - b := make([]byte, binary.MaxVarintLen64) - i := binary.PutUvarint(b, uint64(v)) - slog.Debug("writing u64") - _, err = w.Write(b[:i]) - return err - }(v, w) - if err != nil { - return fmt.Errorf("failed to write pending stream chunk element %d: %w", total, err) - } - if write != nil { - wg.Add(1) - w, err := w.Index(total) - if err != nil { - return fmt.Errorf("failed to index nested stream writer: %w", err) - } - go func() { - defer wg.Done() - if err := write(w); err != nil { - wgErr.Store(err) - } - }() - } - total++ - } - } - if end { - if err := w.WriteByte(0); err != nil { - return fmt.Errorf("failed to write pending stream end byte: %w", err) - } - wg.Wait() - err := wgErr.Load() - if err == nil { - return nil - } - return err.(error) - } - } - }, nil - }(v.Numbers, w) + write0, err := wrpc.WriteStream(v.Numbers, w, func(v uint64, w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, error) { + return (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) { + b := make([]byte, binary.MaxVarintLen64) + i := binary.PutUvarint(b, uint64(v)) + slog.Debug("writing u64") + _, err = w.Write(b[:i]) + return err + }(v, w) + }) if err != nil { return nil, fmt.Errorf("failed to write `numbers` field: %w", err) } @@ -119,59 +40,7 @@ func (v *Req) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, err writes[0] = write0 } slog.Debug("writing field", "name", "bytes") - write1, err := func(v io.ReadCloser, w interface { - io.ByteWriter - io.Writer - }) (write func(wrpc.IndexWriter) error, err error) { - slog.Debug("writing byte stream `stream::pending` status byte") - if err = w.WriteByte(0); err != nil { - return nil, fmt.Errorf("failed to write `stream::pending` byte: %w", err) - } - return func(w wrpc.IndexWriter) (err error) { - defer func() { - slog.Debug("closing byte list stream writer") - if cErr := v.Close(); cErr != nil { - if err == nil { - err = fmt.Errorf("failed to close pending byte stream: %w", cErr) - } else { - slog.Warn("failed to close pending byte stream", "err", cErr) - } - } - }() - chunk := make([]byte, wrpc.ReadChunkSize) - for { - var end bool - slog.Debug("reading pending byte stream contents") - n, err := v.Read(chunk) - if err == io.EOF { - end = true - slog.Debug("pending byte stream reached EOF") - } else if err != nil { - return fmt.Errorf("failed to read pending byte stream chunk: %w", err) - } - if n > math.MaxUint32 { - return fmt.Errorf("pending byte stream chunk length of %d overflows a 32-bit integer", n) - } - if n > 0 { - slog.Debug("writing pending byte stream chunk length", "len", n) - _, err = wrpc.WriteUint32(uint32(n), w) - if err != nil { - return fmt.Errorf("failed to write pending byte stream chunk length of %d: %w", n, err) - } - _, err = w.Write(chunk[:n]) - if err != nil { - return fmt.Errorf("failed to write pending byte stream chunk contents: %w", err) - } - } - if end { - if err := w.WriteByte(0); err != nil { - return fmt.Errorf("failed to write pending byte stream end byte: %w", err) - } - return nil - } - } - }, nil - }(v.Bytes, w) + write1, err := wrpc.WriteByteStream(v.Bytes, w) if err != nil { return nil, fmt.Errorf("failed to write `bytes` field: %w", err) } @@ -182,7 +51,7 @@ func (v *Req) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, err if len(writes) > 0 { return func(w wrpc.IndexWriter) error { var wg sync.WaitGroup - var wgErr atomic.Value + var wgErr atomic.Pointer[error] for index, write := range writes { wg.Add(1) w, err := w.Index(index) @@ -193,21 +62,20 @@ func (v *Req) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, err go func() { defer wg.Done() if err := write(w); err != nil { - wgErr.Store(err) + wgErr.Store(&err) } }() } wg.Wait() - err := wgErr.Load() - if err == nil { - return nil + if err := wgErr.Load(); err != nil { + return *err } - return err.(error) + return nil }, nil } return nil, nil } -func Echo(ctx__ context.Context, wrpc__ wrpc.Invoker, r *Req) (r0__ wrpc.Receiver[[]uint64], r1__ io.ReadCloser, writeErrs__ <-chan error, err__ error) { +func Echo(ctx__ context.Context, wrpc__ wrpc.Invoker, r *Req) (r0__ <-chan []uint64, r1__ <-chan []uint8, errs__ <-chan error, err__ error) { var buf__ bytes.Buffer var writeCount__ uint32 write0__, err__ := (r).WriteToIndex(&buf__) @@ -236,12 +104,17 @@ func Echo(ctx__ context.Context, wrpc__ wrpc.Invoker, r *Req) (r0__ wrpc.Receive slog.ErrorContext(ctx__, "failed to close reader", "instance", "wrpc-examples:streams/handler", "name", "echo", "err", err) } }() + errCh__ := make(chan error, uint(writeCount__)+2) + errs__ = errCh__ + var wg__ sync.WaitGroup + defer func() { + go func() { + wg__.Wait() + close(errCh__) + }() + }() if writeCount__ > 0 { - writeErrCh__ := make(chan error, uint(writeCount__)) - writeErrs__ = writeErrCh__ - var wg__ sync.WaitGroup for index, write := range writes__ { - wg__.Add(1) w, err := w__.Index(index) if err != nil { if cErr := w__.Close(); cErr != nil { @@ -250,246 +123,73 @@ func Echo(ctx__ context.Context, wrpc__ wrpc.Invoker, r *Req) (r0__ wrpc.Receive err__ = fmt.Errorf("failed to index param writer at index `%v`: %w", index, err) return } + wg__.Add(1) write := write go func() { defer wg__.Done() if err := write(w); err != nil { - writeErrCh__ <- err + errCh__ <- err } }() } - go func() { - wg__.Wait() - close(writeErrCh__) - }() } if cErr__ := w__.Close(); cErr__ != nil { slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wrpc-examples:streams/handler", "name", "echo", "err", cErr__) } - r0__, err__ = func(r wrpc.IndexReadCloser, path ...uint32) (wrpc.Receiver[[]uint64], error) { - slog.Debug("reading stream status byte") - status, err := r.ReadByte() - if err != nil { - return nil, fmt.Errorf("failed to read stream status byte: %w", err) - } - switch status { - case 0: - if len(path) > 0 { - var err error - r, err = r.Index(path...) + var d0__ func(context.Context) error + r0__, d0__, err__ = wrpc.ReadStream(r__, func(r wrpc.IndexReadCloser, path ...uint32) (uint64, func(context.Context) error, error) { + v, err := func(r io.ByteReader) (uint64, error) { + var x uint64 + var s uint8 + for i := 0; i < 10; i++ { + slog.Debug("reading u64 byte", "i", i) + b, err := r.ReadByte() if err != nil { - return nil, fmt.Errorf("failed to index nested stream reader: %w", err) - } - } - var total uint32 - return wrpc.NewDecodeReceiver(r, func(r wrpc.IndexReadCloser) ([]uint64, error) { - slog.Debug("reading pending stream chunk length") - n, err := func(r io.ByteReader) (uint32, error) { - var x uint32 - var s uint8 - for i := 0; i < 5; i++ { - slog.Debug("reading u32 byte", "i", i) - b, err := r.ReadByte() - if err != nil { - if i > 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return x, fmt.Errorf("failed to read u32 byte: %w", err) - } - if s == 28 && b > 0x0f { - return x, errors.New("varint overflows a 32-bit integer") - } - if b < 0x80 { - return x | uint32(b)< 0 && err == io.EOF { + err = io.ErrUnexpectedEOF } - return x, errors.New("varint overflows a 32-bit integer") - }(r) - if err != nil { - return nil, fmt.Errorf("failed to read pending stream chunk length: %w", err) - } - if n == 0 { - return nil, io.EOF + return x, fmt.Errorf("failed to read u64 byte: %w", err) } - if math.MaxUint32-n < total { - return nil, errors.New("total incoming pending stream element count would overflow a 32-bit unsigned integer") + if s == 63 && b > 0x01 { + return x, errors.New("varint overflows a 64-bit integer") } - vs := wrpc.NewSlice[[]uint64](n) - for i := range n { - slog.Debug("reading pending stream element", "i", total) - v, err := func(r io.ByteReader) (uint64, error) { - var x uint64 - var s uint8 - for i := 0; i < 10; i++ { - slog.Debug("reading u64 byte", "i", i) - b, err := r.ReadByte() - if err != nil { - if i > 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return x, fmt.Errorf("failed to read u64 byte: %w", err) - } - if s == 63 && b > 0x01 { - return x, errors.New("varint overflows a 64-bit integer") - } - if b < 0x80 { - return x | uint64(b)< 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return nil, fmt.Errorf("failed to read list length byte: %w", err) - } - if s == 28 && b > 0x0f { - return nil, errors.New("list length overflows a 32-bit integer") - } - if b < 0x80 { - x = x | uint32(b)< 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return x, fmt.Errorf("failed to read u64 byte: %w", err) - } - if s == 63 && b > 0x01 { - return x, errors.New("varint overflows a 64-bit integer") - } - if b < 0x80 { - return x | uint64(b)< 0 { - var err error - r, err = r.Index(path...) - if err != nil { - return nil, fmt.Errorf("failed to index nested byte stream reader: %w", err) - } - } - return wrpc.NewByteStreamReader(r), nil - case 1: - slog.Debug("reading ready byte stream contents") - buf, err := - func(r interface { - io.ByteReader - io.Reader - }) ([]byte, error) { - var x uint32 - var s uint - for i := 0; i < 5; i++ { - slog.Debug("reading byte list length", "i", i) - b, err := r.ReadByte() - if err != nil { - if i > 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return nil, fmt.Errorf("failed to read byte list length byte: %w", err) - } - if s == 28 && b > 0x0f { - return nil, errors.New("byte list length overflows a 32-bit integer") - } - if b < 0x80 { - x = x | uint32(b)< math.MaxUint32 { - return fmt.Errorf("outgoing pending stream chunk length of %d overflows a 32-bit integer", n) - } - if math.MaxUint32-uint32(n) < total { - return errors.New("total outgoing pending stream element count would overflow a 32-bit unsigned integer") - } - if n > 0 { - slog.Debug("writing pending stream chunk length", "len", n) - _, err = wrpc.WriteUint32(uint32(n), w) - if err != nil { - return fmt.Errorf("failed to write pending stream chunk length of %d: %w", n, err) - } - for _, v := range chunk { - slog.Debug("writing pending stream element", "i", total) - write, err := (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) { - b := make([]byte, binary.MaxVarintLen64) - i := binary.PutUvarint(b, uint64(v)) - slog.Debug("writing u64") - _, err = w.Write(b[:i]) - return err - }(v, w) - if err != nil { - return fmt.Errorf("failed to write pending stream chunk element %d: %w", total, err) - } - if write != nil { - wg.Add(1) - w, err := w.Index(total) - if err != nil { - return fmt.Errorf("failed to index nested stream writer: %w", err) - } - go func() { - defer wg.Done() - if err := write(w); err != nil { - wgErr.Store(err) - } - }() - } - total++ - } - } - if end { - if err := w.WriteByte(0); err != nil { - return fmt.Errorf("failed to write pending stream end byte: %w", err) - } - wg.Wait() - err := wgErr.Load() - if err == nil { - return nil - } - return err.(error) - } - } - }, nil - }(v.Numbers, w) + write0, err := wrpc.WriteStream(v.Numbers, w, func(v uint64, w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, error) { + return (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) { + b := make([]byte, binary.MaxVarintLen64) + i := binary.PutUvarint(b, uint64(v)) + slog.Debug("writing u64") + _, err = w.Write(b[:i]) + return err + }(v, w) + }) if err != nil { return nil, fmt.Errorf("failed to write `numbers` field: %w", err) } @@ -119,59 +40,7 @@ func (v *Req) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, err writes[0] = write0 } slog.Debug("writing field", "name", "bytes") - write1, err := func(v io.ReadCloser, w interface { - io.ByteWriter - io.Writer - }) (write func(wrpc.IndexWriter) error, err error) { - slog.Debug("writing byte stream `stream::pending` status byte") - if err = w.WriteByte(0); err != nil { - return nil, fmt.Errorf("failed to write `stream::pending` byte: %w", err) - } - return func(w wrpc.IndexWriter) (err error) { - defer func() { - slog.Debug("closing byte list stream writer") - if cErr := v.Close(); cErr != nil { - if err == nil { - err = fmt.Errorf("failed to close pending byte stream: %w", cErr) - } else { - slog.Warn("failed to close pending byte stream", "err", cErr) - } - } - }() - chunk := make([]byte, wrpc.ReadChunkSize) - for { - var end bool - slog.Debug("reading pending byte stream contents") - n, err := v.Read(chunk) - if err == io.EOF { - end = true - slog.Debug("pending byte stream reached EOF") - } else if err != nil { - return fmt.Errorf("failed to read pending byte stream chunk: %w", err) - } - if n > math.MaxUint32 { - return fmt.Errorf("pending byte stream chunk length of %d overflows a 32-bit integer", n) - } - if n > 0 { - slog.Debug("writing pending byte stream chunk length", "len", n) - _, err = wrpc.WriteUint32(uint32(n), w) - if err != nil { - return fmt.Errorf("failed to write pending byte stream chunk length of %d: %w", n, err) - } - _, err = w.Write(chunk[:n]) - if err != nil { - return fmt.Errorf("failed to write pending byte stream chunk contents: %w", err) - } - } - if end { - if err := w.WriteByte(0); err != nil { - return fmt.Errorf("failed to write pending byte stream end byte: %w", err) - } - return nil - } - } - }, nil - }(v.Bytes, w) + write1, err := wrpc.WriteByteStream(v.Bytes, w) if err != nil { return nil, fmt.Errorf("failed to write `bytes` field: %w", err) } @@ -182,7 +51,7 @@ func (v *Req) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, err if len(writes) > 0 { return func(w wrpc.IndexWriter) error { var wg sync.WaitGroup - var wgErr atomic.Value + var wgErr atomic.Pointer[error] for index, write := range writes { wg.Add(1) w, err := w.Index(index) @@ -193,23 +62,22 @@ func (v *Req) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, err go func() { defer wg.Done() if err := write(w); err != nil { - wgErr.Store(err) + wgErr.Store(&err) } }() } wg.Wait() - err := wgErr.Load() - if err == nil { - return nil + if err := wgErr.Load(); err != nil { + return *err } - return err.(error) + return nil }, nil } return nil, nil } type Handler interface { - Echo(ctx__ context.Context, r *Req) (wrpc.Receiver[[]uint64], io.ReadCloser, error) + Echo(ctx__ context.Context, r *Req) (<-chan []uint64, <-chan []uint8, error) } func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { @@ -229,236 +97,58 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "failed to close writer", "instance", "wrpc-examples:streams/handler", "name", "echo", "err", err) } }() + ctx, cancel := context.WithCancel(ctx) + var wg sync.WaitGroup + defer func() { + go func() { + wg.Wait() + cancel() + }() + }() slog.DebugContext(ctx, "reading parameter", "i", 0) - p0, err := func(r wrpc.IndexReadCloser, path ...uint32) (*Req, error) { + p0, d0, err := func(r wrpc.IndexReadCloser, path ...uint32) (*Req, func(context.Context) error, error) { v := &Req{} var err error + var d func(context.Context) error + var ds []func(context.Context) error slog.Debug("reading field", "name", "numbers") - v.Numbers, err = func(r wrpc.IndexReadCloser, path ...uint32) (wrpc.Receiver[[]uint64], error) { - slog.Debug("reading stream status byte") - status, err := r.ReadByte() - if err != nil { - return nil, fmt.Errorf("failed to read stream status byte: %w", err) - } - switch status { - case 0: - if len(path) > 0 { - var err error - r, err = r.Index(path...) + v.Numbers, d, err = wrpc.ReadStream(r, func(r wrpc.IndexReadCloser, path ...uint32) (uint64, func(context.Context) error, error) { + v, err := func(r io.ByteReader) (uint64, error) { + var x uint64 + var s uint8 + for i := 0; i < 10; i++ { + slog.Debug("reading u64 byte", "i", i) + b, err := r.ReadByte() if err != nil { - return nil, fmt.Errorf("failed to index nested stream reader: %w", err) - } - } - var total uint32 - return wrpc.NewDecodeReceiver(r, func(r wrpc.IndexReadCloser) ([]uint64, error) { - slog.Debug("reading pending stream chunk length") - n, err := func(r io.ByteReader) (uint32, error) { - var x uint32 - var s uint8 - for i := 0; i < 5; i++ { - slog.Debug("reading u32 byte", "i", i) - b, err := r.ReadByte() - if err != nil { - if i > 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return x, fmt.Errorf("failed to read u32 byte: %w", err) - } - if s == 28 && b > 0x0f { - return x, errors.New("varint overflows a 32-bit integer") - } - if b < 0x80 { - return x | uint32(b)< 0 && err == io.EOF { + err = io.ErrUnexpectedEOF } - return x, errors.New("varint overflows a 32-bit integer") - }(r) - if err != nil { - return nil, fmt.Errorf("failed to read pending stream chunk length: %w", err) - } - if n == 0 { - return nil, io.EOF + return x, fmt.Errorf("failed to read u64 byte: %w", err) } - if math.MaxUint32-n < total { - return nil, errors.New("total incoming pending stream element count would overflow a 32-bit unsigned integer") + if s == 63 && b > 0x01 { + return x, errors.New("varint overflows a 64-bit integer") } - vs := wrpc.NewSlice[[]uint64](n) - for i := range n { - slog.Debug("reading pending stream element", "i", total) - v, err := func(r io.ByteReader) (uint64, error) { - var x uint64 - var s uint8 - for i := 0; i < 10; i++ { - slog.Debug("reading u64 byte", "i", i) - b, err := r.ReadByte() - if err != nil { - if i > 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return x, fmt.Errorf("failed to read u64 byte: %w", err) - } - if s == 63 && b > 0x01 { - return x, errors.New("varint overflows a 64-bit integer") - } - if b < 0x80 { - return x | uint64(b)< 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return nil, fmt.Errorf("failed to read list length byte: %w", err) - } - if s == 28 && b > 0x0f { - return nil, errors.New("list length overflows a 32-bit integer") - } - if b < 0x80 { - x = x | uint32(b)< 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return x, fmt.Errorf("failed to read u64 byte: %w", err) - } - if s == 63 && b > 0x01 { - return x, errors.New("varint overflows a 64-bit integer") - } - if b < 0x80 { - return x | uint64(b)< 0 { - var err error - r, err = r.Index(path...) - if err != nil { - return nil, fmt.Errorf("failed to index nested byte stream reader: %w", err) - } - } - return wrpc.NewByteStreamReader(r), nil - case 1: - slog.Debug("reading ready byte stream contents") - buf, err := - func(r interface { - io.ByteReader - io.Reader - }) ([]byte, error) { - var x uint32 - var s uint - for i := 0; i < 5; i++ { - slog.Debug("reading byte list length", "i", i) - b, err := r.ReadByte() - if err != nil { - if i > 0 && err == io.EOF { - err = io.ErrUnexpectedEOF - } - return nil, fmt.Errorf("failed to read byte list length byte: %w", err) - } - if s == 28 && b > 0x0f { - return nil, errors.New("byte list length overflows a 32-bit integer") - } - if b < 0x80 { - x = x | uint32(b)< math.MaxUint32 { - return fmt.Errorf("outgoing pending stream chunk length of %d overflows a 32-bit integer", n) - } - if math.MaxUint32-uint32(n) < total { - return errors.New("total outgoing pending stream element count would overflow a 32-bit unsigned integer") - } - if n > 0 { - slog.Debug("writing pending stream chunk length", "len", n) - _, err = wrpc.WriteUint32(uint32(n), w) - if err != nil { - return fmt.Errorf("failed to write pending stream chunk length of %d: %w", n, err) - } - for _, v := range chunk { - slog.Debug("writing pending stream element", "i", total) - write, err := (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) { - b := make([]byte, binary.MaxVarintLen64) - i := binary.PutUvarint(b, uint64(v)) - slog.Debug("writing u64") - _, err = w.Write(b[:i]) - return err - }(v, w) - if err != nil { - return fmt.Errorf("failed to write pending stream chunk element %d: %w", total, err) - } - if write != nil { - wg.Add(1) - w, err := w.Index(total) - if err != nil { - return fmt.Errorf("failed to index nested stream writer: %w", err) - } - go func() { - defer wg.Done() - if err := write(w); err != nil { - wgErr.Store(err) - } - }() - } - total++ - } - } - if end { - if err := w.WriteByte(0); err != nil { - return fmt.Errorf("failed to write pending stream end byte: %w", err) - } - wg.Wait() - err := wgErr.Load() - if err == nil { - return nil - } - return err.(error) - } - } - }, nil - }(r0, &buf) + write0, err := wrpc.WriteStream(r0, &buf, func(v uint64, w wrpc.ByteWriter) (func(wrpc.IndexWriter) error, error) { + return (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) { + b := make([]byte, binary.MaxVarintLen64) + i := binary.PutUvarint(b, uint64(v)) + slog.Debug("writing u64") + _, err = w.Write(b[:i]) + return err + }(v, w) + }) if err != nil { slog.WarnContext(ctx, "failed to write result value", "i", 0, "instance", "wrpc-examples:streams/handler", "name", "echo", "err", err) return @@ -575,59 +194,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { if write0 != nil { writes[0] = write0 } - write1, err := func(v io.ReadCloser, w interface { - io.ByteWriter - io.Writer - }) (write func(wrpc.IndexWriter) error, err error) { - slog.Debug("writing byte stream `stream::pending` status byte") - if err = w.WriteByte(0); err != nil { - return nil, fmt.Errorf("failed to write `stream::pending` byte: %w", err) - } - return func(w wrpc.IndexWriter) (err error) { - defer func() { - slog.Debug("closing byte list stream writer") - if cErr := v.Close(); cErr != nil { - if err == nil { - err = fmt.Errorf("failed to close pending byte stream: %w", cErr) - } else { - slog.Warn("failed to close pending byte stream", "err", cErr) - } - } - }() - chunk := make([]byte, wrpc.ReadChunkSize) - for { - var end bool - slog.Debug("reading pending byte stream contents") - n, err := v.Read(chunk) - if err == io.EOF { - end = true - slog.Debug("pending byte stream reached EOF") - } else if err != nil { - return fmt.Errorf("failed to read pending byte stream chunk: %w", err) - } - if n > math.MaxUint32 { - return fmt.Errorf("pending byte stream chunk length of %d overflows a 32-bit integer", n) - } - if n > 0 { - slog.Debug("writing pending byte stream chunk length", "len", n) - _, err = wrpc.WriteUint32(uint32(n), w) - if err != nil { - return fmt.Errorf("failed to write pending byte stream chunk length of %d: %w", n, err) - } - _, err = w.Write(chunk[:n]) - if err != nil { - return fmt.Errorf("failed to write pending byte stream chunk contents: %w", err) - } - } - if end { - if err := w.WriteByte(0); err != nil { - return fmt.Errorf("failed to write pending byte stream end byte: %w", err) - } - return nil - } - } - }, nil - }(r1, &buf) + write1, err := wrpc.WriteByteStream(r1, &buf) if err != nil { slog.WarnContext(ctx, "failed to write result value", "i", 1, "instance", "wrpc-examples:streams/handler", "name", "echo", "err", err) return @@ -652,7 +219,9 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { return } write := write + wg.Add(1) go func() { + defer wg.Done() if err := write(w); err != nil { slog.WarnContext(ctx, "failed to write nested result value", "instance", "wrpc-examples:streams/handler", "name", "echo", "err", err) } @@ -664,7 +233,9 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { return } write := write + wg.Add(1) go func() { + defer wg.Done() if err := write(w); err != nil { slog.WarnContext(ctx, "failed to write nested result value", "instance", "wrpc-examples:streams/handler", "name", "echo", "err", err) } diff --git a/examples/go/wasi-keyvalue-client/bindings/wasi/keyvalue/store/bindings.wrpc.go b/examples/go/wasi-keyvalue-client/bindings/wasi/keyvalue/store/bindings.wrpc.go index c69933f19..3cd91ae32 100644 --- a/examples/go/wasi-keyvalue-client/bindings/wasi/keyvalue/store/bindings.wrpc.go +++ b/examples/go/wasi-keyvalue-client/bindings/wasi/keyvalue/store/bindings.wrpc.go @@ -228,7 +228,7 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er if len(writes) > 0 { return func(w wrpc.IndexWriter) error { var wg sync.WaitGroup - var wgErr atomic.Value + var wgErr atomic.Pointer[error] for index, write := range writes { wg.Add(1) w, err := w.Index(index) @@ -239,16 +239,15 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er go func() { defer wg.Done() if err := write(w); err != nil { - wgErr.Store(err) + wgErr.Store(&err) } }() } wg.Wait() - err := wgErr.Load() - if err == nil { - return nil + if err := wgErr.Load(); err != nil { + return *err } - return err.(error) + return nil }, nil } return nil, nil @@ -312,7 +311,7 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er if len(writes) > 0 { return func(w wrpc.IndexWriter) error { var wg sync.WaitGroup - var wgErr atomic.Value + var wgErr atomic.Pointer[error] for index, write := range writes { wg.Add(1) w, err := w.Index(index) @@ -323,16 +322,15 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er go func() { defer wg.Done() if err := write(w); err != nil { - wgErr.Store(err) + wgErr.Store(&err) } }() } wg.Wait() - err := wgErr.Load() - if err == nil { - return nil + if err := wgErr.Load(); err != nil { + return *err } - return err.(error) + return nil }, nil } return nil, nil @@ -886,7 +884,7 @@ func Bucket_Set(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[Buc if len(writes) > 0 { return func(w wrpc.IndexWriter) error { var wg sync.WaitGroup - var wgErr atomic.Value + var wgErr atomic.Pointer[error] for index, write := range writes { wg.Add(1) w, err := w.Index(index) @@ -897,16 +895,15 @@ func Bucket_Set(ctx__ context.Context, wrpc__ wrpc.Invoker, self wrpc.Borrow[Buc go func() { defer wg.Done() if err := write(w); err != nil { - wgErr.Store(err) + wgErr.Store(&err) } }() } wg.Wait() - err := wgErr.Load() - if err == nil { - return nil + if err := wgErr.Load(); err != nil { + return *err } - return err.(error) + return nil }, nil } return nil, nil diff --git a/examples/go/wasi-keyvalue-server/bindings/exports/wasi/keyvalue/store/bindings.wrpc.go b/examples/go/wasi-keyvalue-server/bindings/exports/wasi/keyvalue/store/bindings.wrpc.go index 75b45c930..e67f97691 100644 --- a/examples/go/wasi-keyvalue-server/bindings/exports/wasi/keyvalue/store/bindings.wrpc.go +++ b/examples/go/wasi-keyvalue-server/bindings/exports/wasi/keyvalue/store/bindings.wrpc.go @@ -228,7 +228,7 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er if len(writes) > 0 { return func(w wrpc.IndexWriter) error { var wg sync.WaitGroup - var wgErr atomic.Value + var wgErr atomic.Pointer[error] for index, write := range writes { wg.Add(1) w, err := w.Index(index) @@ -239,16 +239,15 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er go func() { defer wg.Done() if err := write(w); err != nil { - wgErr.Store(err) + wgErr.Store(&err) } }() } wg.Wait() - err := wgErr.Load() - if err == nil { - return nil + if err := wgErr.Load(); err != nil { + return *err } - return err.(error) + return nil }, nil } return nil, nil @@ -312,7 +311,7 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er if len(writes) > 0 { return func(w wrpc.IndexWriter) error { var wg sync.WaitGroup - var wgErr atomic.Value + var wgErr atomic.Pointer[error] for index, write := range writes { wg.Add(1) w, err := w.Index(index) @@ -323,16 +322,15 @@ func (v *KeyResponse) WriteToIndex(w wrpc.ByteWriter) (func(wrpc.IndexWriter) er go func() { defer wg.Done() if err := write(w); err != nil { - wgErr.Store(err) + wgErr.Store(&err) } }() } wg.Wait() - err := wgErr.Load() - if err == nil { - return nil + if err := wgErr.Load(); err != nil { + return *err } - return err.(error) + return nil }, nil } return nil, nil @@ -475,7 +473,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wasi:keyvalue/store@0.2.0-draft2.open` handler") r0, err := h.Open(ctx, p0) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "open", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "open", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "open", "err", err) @@ -680,7 +678,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wasi:keyvalue/store@0.2.0-draft2.bucket.get` handler") r0, err := h.Bucket_Get(ctx, p0, p1) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.get", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.get", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.get", "err", err) @@ -756,7 +754,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { if len(writes) > 0 { return func(w wrpc.IndexWriter) error { var wg sync.WaitGroup - var wgErr atomic.Value + var wgErr atomic.Pointer[error] for index, write := range writes { wg.Add(1) w, err := w.Index(index) @@ -767,16 +765,15 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { go func() { defer wg.Done() if err := write(w); err != nil { - wgErr.Store(err) + wgErr.Store(&err) } }() } wg.Wait() - err := wgErr.Load() - if err == nil { - return nil + if err := wgErr.Load(); err != nil { + return *err } - return err.(error) + return nil }, nil } return nil, nil @@ -988,7 +985,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wasi:keyvalue/store@0.2.0-draft2.bucket.set` handler") r0, err := h.Bucket_Set(ctx, p0, p1, p2) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.set", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.set", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.set", "err", err) @@ -1165,7 +1162,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wasi:keyvalue/store@0.2.0-draft2.bucket.delete` handler") r0, err := h.Bucket_Delete(ctx, p0, p1) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.delete", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.delete", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.delete", "err", err) @@ -1342,7 +1339,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wasi:keyvalue/store@0.2.0-draft2.bucket.exists` handler") r0, err := h.Bucket_Exists(ctx, p0, p1) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.exists", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.exists", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.exists", "err", err) @@ -1553,7 +1550,7 @@ func ServeInterface(s wrpc.Server, h Handler) (stop func() error, err error) { slog.DebugContext(ctx, "calling `wasi:keyvalue/store@0.2.0-draft2.bucket.list-keys` handler") r0, err := h.Bucket_ListKeys(ctx, p0, p1) if cErr := r.Close(); cErr != nil { - slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.list-keys", "err", err) + slog.ErrorContext(ctx, "failed to close reader", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.list-keys", "err", cErr) } if err != nil { slog.WarnContext(ctx, "failed to handle invocation", "instance", "wasi:keyvalue/store@0.2.0-draft2", "name", "bucket.list-keys", "err", err) diff --git a/go/frame_writer.go b/go/frame_writer.go index 674599946..f77b799af 100644 --- a/go/frame_writer.go +++ b/go/frame_writer.go @@ -111,6 +111,9 @@ func sendFrame(ctx context.Context, ch chan<- Frame, frame Frame) error { } func (w *FrameStreamWriter) Write(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } data := make([]byte, len(p)) copy(data, p) if err := sendFrame(w.ctx, w.ch, Frame{ diff --git a/go/frame_writer_test.go b/go/frame_writer_test.go index accf14284..94637cf49 100644 --- a/go/frame_writer_test.go +++ b/go/frame_writer_test.go @@ -71,6 +71,7 @@ func TestFrameStreamWriter(t *testing.T) { var dst sink w := wrpc.NewFrameStreamWriter(ctx, &dst) + assertWriteString(t, w, "") assertWriteString(t, w, "hello") assertClose(t, w) diff --git a/go/future.go b/go/future.go index 40d35f37c..3657092f2 100644 --- a/go/future.go +++ b/go/future.go @@ -1,6 +1,9 @@ package wrpc import ( + "bytes" + "context" + "errors" "fmt" "log/slog" ) @@ -23,27 +26,79 @@ func ReadFutureStatus(r ByteReader) (bool, error) { } } -// ReadFuture reads a future from `r` -func ReadFuture[T any](r IndexReadCloser, f func(IndexReadCloser) (T, error), path ...uint32) (Receiver[T], error) { +// ReadFuture reads a `future` from `r` using `f` to read its value. +// The returned channel receives the value once the future resolves and is closed afterwards. +// If the future is pending, the returned function reads it and must be called. If reading fails, +// the channel never receives a value and is never closed. +func ReadFuture[T any](r IndexReadCloser, f func(IndexReadCloser, ...uint32) (T, func(context.Context) error, error), path ...uint32) (<-chan T, func(context.Context) error, error) { slog.Debug("reading future status byte") ok, err := ReadFutureStatus(r) if err != nil { - return nil, err + return nil, nil, err } - if !ok { + ch := make(chan T, 1) + if ok { + slog.Debug("reading ready future") + v, deferred, err := f(r, path...) + if err != nil { + return nil, nil, fmt.Errorf("failed to read ready future: %w", err) + } + ch <- v + close(ch) + return ch, deferred, nil + } + if len(path) > 0 { + slog.Debug("indexing pending future reader") + r, err = r.Index(path...) + if err != nil { + return nil, nil, fmt.Errorf("failed to index nested future reader: %w", err) + } + } + return ch, func(ctx context.Context) error { if len(path) > 0 { - slog.Debug("indexing pending future reader") - r, err = r.Index(path...) - if err != nil { - return nil, fmt.Errorf("failed to index nested future reader: %w", err) - } + defer closeNested(r) } - return NewDecodeReceiver(r, f), nil + slog.Debug("reading pending future") + v, deferred, err := f(r) + if err != nil { + return fmt.Errorf("failed to read pending future: %w", err) + } + ch <- v + close(ch) + if deferred != nil { + return deferred(ctx) + } + return nil + }, nil +} + +// WriteFuture writes a `future` received from `v` to `w` using `f` to write its value +func WriteFuture[T any](v <-chan T, w ByteWriter, f func(T, ByteWriter) (func(IndexWriter) error, error)) (func(IndexWriter) error, error) { + if v == nil { + return nil, errors.New("outgoing future channel is nil") } - slog.Debug("reading ready future") - v, err := f(r) - if err != nil { - return nil, err + slog.Debug("writing `future::pending` status byte") + if err := w.WriteByte(0); err != nil { + return nil, fmt.Errorf("failed to write `future::pending` byte: %w", err) } - return NewCompleteReceiver(v), nil + return func(w IndexWriter) error { + slog.Debug("receiving outgoing pending future") + x, ok := <-v + if !ok { + return errors.New("outgoing pending future closed without a value") + } + slog.Debug("writing pending future value") + var buf bytes.Buffer + write, err := f(x, &buf) + if err != nil { + return fmt.Errorf("failed to encode pending future value: %w", err) + } + if _, err := w.Write(buf.Bytes()); err != nil { + return fmt.Errorf("failed to write pending future value: %w", err) + } + if write != nil { + return write(w) + } + return nil + }, nil } diff --git a/go/stream.go b/go/stream.go index a880cb950..c466376d2 100644 --- a/go/stream.go +++ b/go/stream.go @@ -1,113 +1,15 @@ package wrpc import ( - "bufio" "bytes" + "context" "errors" "fmt" - "io" "log/slog" "math" + "sync" ) -// ReadChunkSize is the size of chunks read from byte streams -const ReadChunkSize = 8192 - -type ByteStreamWriter struct { - r io.Reader - chunk []byte -} - -func (v *ByteStreamWriter) WriteTo(w ByteWriter) (err error) { - if len(v.chunk) == 0 { - v.chunk = make([]byte, ReadChunkSize) - } - buf := bufio.NewWriter(w) - defer func() { - if fErr := buf.Flush(); fErr != nil { - if err == nil { - err = fmt.Errorf("failed to flush pending byte stream writer: %w", fErr) - } else { - slog.Warn("failed to flush pending byte stream writer", "err", fErr) - } - } - }() - for { - var end bool - slog.Debug("reading pending byte stream contents") - n, err := v.r.Read(v.chunk) - if err == io.EOF { - end = true - slog.Debug("pending byte stream reached EOF") - } else if err != nil { - return fmt.Errorf("failed to read pending byte stream chunk: %w", err) - } - if n > math.MaxUint32 { - return fmt.Errorf("pending byte stream chunk length of %d overflows a 32-bit integer", n) - } - if n > 0 { - slog.Debug("writing pending byte stream chunk length", "len", n) - _, err = WriteUint32(uint32(n), buf) - if err != nil { - return fmt.Errorf("failed to write pending byte stream chunk length of %d: %w", n, err) - } - _, err = buf.Write(v.chunk[:n]) - if err != nil { - return fmt.Errorf("failed to write pending byte stream chunk contents: %w", err) - } - } - if end { - if err := buf.WriteByte(0); err != nil { - return fmt.Errorf("failed to write pending byte stream end byte: %w", err) - } - return nil - } - } -} - -type ByteStreamReader struct { - r ByteReadCloser - buf uint32 -} - -func (r *ByteStreamReader) Read(p []byte) (int, error) { - n := r.buf - if n == 0 { - slog.Debug("reading pending byte stream chunk length") - var err error - n, err = ReadUint32(r.r) - if err != nil { - return 0, fmt.Errorf("failed to read pending byte stream chunk length: %w", err) - } - if n == 0 { - return 0, io.EOF - } - } - if len(p) > int(n) { - p = p[:n] - } - slog.Debug("reading pending byte stream chunk contents", "len", n) - rn, err := r.r.Read(p) - if err != nil { - return rn, fmt.Errorf("failed to read pending stream chunk bytes: %w", err) - } - if rn > int(n) { - return rn, errors.New("read more bytes than requested") - } - r.buf = n - uint32(rn) - return rn, nil -} - -func (r *ByteStreamReader) Close() error { - return r.r.Close() -} - -func NewByteStreamReader(r ByteReadCloser) *ByteStreamReader { - return &ByteStreamReader{ - r: r, - } -} - // ReadStreamStatus reads a single byte from `r` and returns: // - `true` if stream is "ready" // - `false` if stream is "pending" @@ -126,92 +28,272 @@ func ReadStreamStatus(r ByteReader) (bool, error) { } } -// ReadByteStream reads a stream of bytes from `r` -func ReadByteStream(r IndexReader, path ...uint32) (io.ReadCloser, error) { +// ReadByteStream reads a `stream` from `r`. +// The returned channel receives the chunks of the stream and is closed once the stream ends. +// If the stream is pending, the returned function reads it and must be called. It closes the channel +// if reading fails or `ctx` is done. +func ReadByteStream(r IndexReadCloser, path ...uint32) (<-chan []byte, func(context.Context) error, error) { slog.Debug("reading byte stream status byte") ok, err := ReadStreamStatus(r) if err != nil { - return nil, err + return nil, nil, err } - if !ok { - r, err := r.Index(path...) + if ok { + slog.Debug("reading ready byte stream") + buf, err := ReadByteList(r) if err != nil { - return nil, fmt.Errorf("failed to index reader: %w", err) + return nil, nil, fmt.Errorf("failed to read ready byte stream: %w", err) + } + ch := make(chan []byte, 1) + if len(buf) > 0 { + ch <- buf } - return NewByteStreamReader(r), nil + close(ch) + return ch, nil, nil } - slog.Debug("reading ready byte stream") - buf, err := ReadByteList(r) - if err != nil { - return nil, fmt.Errorf("failed to read bytes: %w", err) + if len(path) > 0 { + slog.Debug("indexing pending byte stream reader") + r, err = r.Index(path...) + if err != nil { + return nil, nil, fmt.Errorf("failed to index nested byte stream reader: %w", err) + } } - slog.Debug("read ready byte stream", "len", len(buf)) - return io.NopCloser(bytes.NewReader(buf)), nil + ch := make(chan []byte) + return ch, func(ctx context.Context) error { + if len(path) > 0 { + defer closeNested(r) + } + defer close(ch) + for { + slog.Debug("reading pending byte stream chunk") + buf, err := ReadByteList(r) + if err != nil { + return fmt.Errorf("failed to read pending byte stream chunk: %w", err) + } + if len(buf) == 0 { + slog.Debug("pending byte stream ended") + return nil + } + select { + case ch <- buf: + case <-ctx.Done(): + return fmt.Errorf("failed to send pending byte stream chunk: %w", context.Cause(ctx)) + } + } + }, nil } -// ReadStream reads a stream from `r` -func ReadStream[T any](r IndexReader, f func(IndexReader) (T, error), path ...uint32) (Receiver[[]T], error) { +// ReadStream reads a `stream` from `r` using `f` to read its elements. +// The returned channel receives the chunks of the stream and is closed once the stream ends. +// If the stream is pending, the returned function reads it and must be called. It closes the channel +// if reading fails or `ctx` is done. +func ReadStream[T any](r IndexReadCloser, f func(IndexReadCloser, ...uint32) (T, func(context.Context) error, error), path ...uint32) (<-chan []T, func(context.Context) error, error) { slog.Debug("reading stream status byte") ok, err := ReadStreamStatus(r) if err != nil { - return nil, err + return nil, nil, err } - if !ok { - r, err := r.Index(path...) + if ok { + slog.Debug("reading ready stream length") + n, err := ReadUint32(r) if err != nil { - return nil, fmt.Errorf("failed to index reader: %w", err) + return nil, nil, fmt.Errorf("failed to read ready stream length: %w", err) } - return NewDecodeReceiver(r, func(r IndexReadCloser) ([]T, error) { - slog.Debug("reading pending stream chunk length") - n, err := ReadUint32(r) + vs := NewSlice[[]T](n) + var ds []func(context.Context) error + for i := range n { + v, deferred, err := f(r, append(path, i)...) if err != nil { - return nil, fmt.Errorf("failed to read pending stream chunk length: %w", err) - } - if n == 0 { - slog.Debug("pending stream EOF chunk received") - return nil, io.EOF + return nil, nil, fmt.Errorf("failed to read ready stream element %d: %w", i, err) } - vs := NewSlice[[]T](n) - for i := range n { - v, err := f(r) + vs = append(vs, v) + ds = append(ds, deferred) + } + ch := make(chan []T, 1) + if len(vs) > 0 { + ch <- vs + } + close(ch) + return ch, JoinDeferred(ds...), nil + } + if len(path) > 0 { + slog.Debug("indexing pending stream reader") + r, err = r.Index(path...) + if err != nil { + return nil, nil, fmt.Errorf("failed to index nested stream reader: %w", err) + } + } + ch := make(chan []T) + return ch, func(ctx context.Context) error { + if len(path) > 0 { + defer closeNested(r) + } + var wg sync.WaitGroup + var errMu sync.Mutex + var errs []error + err := func() error { + defer close(ch) + var total uint32 + for { + slog.Debug("reading pending stream chunk length") + n, err := ReadUint32(r) if err != nil { - return nil, fmt.Errorf("failed to read pending stream chunk element %d: %w", i, err) + return fmt.Errorf("failed to read pending stream chunk length: %w", err) + } + if n == 0 { + slog.Debug("pending stream ended") + return nil + } + if math.MaxUint32-n < total { + return errors.New("total pending stream element count would overflow a 32-bit unsigned integer") + } + vs := NewSlice[[]T](n) + for range n { + v, deferred, err := f(r, total) + if err != nil { + return fmt.Errorf("failed to read pending stream element %d: %w", total, err) + } + if deferred != nil { + wg.Add(1) + go func() { + defer wg.Done() + if err := deferred(ctx); err != nil { + errMu.Lock() + errs = append(errs, err) + errMu.Unlock() + } + }() + } + vs = append(vs, v) + total++ + } + select { + case ch <- vs: + case <-ctx.Done(): + return fmt.Errorf("failed to send pending stream chunk: %w", context.Cause(ctx)) } - vs = append(vs, v) } - return vs, nil - }), nil + }() + wg.Wait() + return errors.Join(append(errs, err)...) + }, nil +} + +// WriteByteStream writes a `stream` with chunks received from `v` to `w` +func WriteByteStream(v <-chan []byte, w ByteWriter) (func(IndexWriter) error, error) { + if v == nil { + return nil, errors.New("outgoing byte stream channel is nil") } - slog.Debug("reading ready stream") - vs, err := ReadList(r, f) - if err != nil { - return nil, fmt.Errorf("failed to read ready stream: %w", err) + slog.Debug("writing `stream::pending` status byte") + if err := w.WriteByte(0); err != nil { + return nil, fmt.Errorf("failed to write `stream::pending` byte: %w", err) } - slog.Debug("read ready stream", "len", len(vs)) - return NewCompleteReceiver(vs), nil + return func(w IndexWriter) error { + var buf bytes.Buffer + for chunk := range v { + if len(chunk) == 0 { + continue + } + slog.Debug("writing pending byte stream chunk", "len", len(chunk)) + buf.Reset() + if _, err := WriteByteList(chunk, &buf); err != nil { + go drain(v) + return fmt.Errorf("failed to encode pending byte stream chunk: %w", err) + } + if _, err := w.Write(buf.Bytes()); err != nil { + go drain(v) + return fmt.Errorf("failed to write pending byte stream chunk: %w", err) + } + } + slog.Debug("writing pending byte stream end") + if err := w.WriteByte(0); err != nil { + return fmt.Errorf("failed to write pending byte stream end byte: %w", err) + } + return nil + }, nil } -func WriteByteStream(r io.Reader, w IndexWriter, chunk []byte, path ...uint32) (err error) { - slog.Debug("writing byte stream `stream::pending` status byte") - if err := w.WriteByte(0); err != nil { - return fmt.Errorf("failed to write `stream::pending` byte: %w", err) +// WriteStream writes a `stream` with chunks received from `v` to `w` using `f` to write its elements +func WriteStream[T any](v <-chan []T, w ByteWriter, f func(T, ByteWriter) (func(IndexWriter) error, error)) (func(IndexWriter) error, error) { + if v == nil { + return nil, errors.New("outgoing stream channel is nil") } - wi, err := w.Index(path...) - if err != nil { - return fmt.Errorf("failed to index reader: %w", err) + slog.Debug("writing `stream::pending` status byte") + if err := w.WriteByte(0); err != nil { + return nil, fmt.Errorf("failed to write `stream::pending` byte: %w", err) } - s := &ByteStreamWriter{r, chunk} - defer func() { - if cErr := wi.Close(); cErr != nil { - if err == nil { - err = fmt.Errorf("failed to close pending byte stream: %w", cErr) - } else { - slog.Warn("failed to close pending byte stream", "err", cErr) + return func(w IndexWriter) error { + var wg sync.WaitGroup + var errMu sync.Mutex + var errs []error + err := func() error { + var total uint32 + var buf bytes.Buffer + writes := make(map[uint32]func(IndexWriter) error) + for chunk := range v { + n := len(chunk) + if n == 0 { + continue + } + if n > math.MaxUint32 || math.MaxUint32-uint32(n) < total { + return errors.New("total pending stream element count would overflow a 32-bit unsigned integer") + } + slog.Debug("writing pending stream chunk", "len", n) + buf.Reset() + if _, err := WriteUint32(uint32(n), &buf); err != nil { + return fmt.Errorf("failed to encode pending stream chunk length of %d: %w", n, err) + } + clear(writes) + for _, v := range chunk { + write, err := f(v, &buf) + if err != nil { + return fmt.Errorf("failed to encode pending stream element %d: %w", total, err) + } + if write != nil { + writes[total] = write + } + total++ + } + if _, err := w.Write(buf.Bytes()); err != nil { + return fmt.Errorf("failed to write pending stream chunk: %w", err) + } + for i, write := range writes { + w, err := w.Index(i) + if err != nil { + return fmt.Errorf("failed to index nested stream writer: %w", err) + } + wg.Add(1) + go func() { + defer wg.Done() + if err := write(w); err != nil { + errMu.Lock() + errs = append(errs, err) + errMu.Unlock() + } + }() + } + } + slog.Debug("writing pending stream end") + if err := w.WriteByte(0); err != nil { + return fmt.Errorf("failed to write pending stream end byte: %w", err) } + return nil + }() + if err != nil { + go drain(v) } - }() - if err := s.WriteTo(wi); err != nil { - return fmt.Errorf("failed to write stream contents: %w", err) + wg.Wait() + return errors.Join(append(errs, err)...) + }, nil +} + +func closeNested(r IndexReadCloser) { + if err := r.Close(); err != nil { + slog.Debug("failed to close nested reader", "err", err) + } +} + +func drain[T any](v <-chan T) { + for range v { } - return nil } diff --git a/go/wrpc.go b/go/wrpc.go index 56bf67fb7..fcd11a988 100644 --- a/go/wrpc.go +++ b/go/wrpc.go @@ -3,7 +3,10 @@ package wrpc import ( "bufio" "context" + "errors" "io" + "slices" + "sync" ) // Invoke is the client-side transport handle @@ -110,9 +113,29 @@ type ByteReader interface { io.Reader } -type Receiver[T any] interface { - Receive() (T, error) - io.Closer +// JoinDeferred returns a function, which calls all non-nil `fs` concurrently and returns their joined errors. +// It returns `nil` if all `fs` are `nil`. +func JoinDeferred(fs ...func(context.Context) error) func(context.Context) error { + fs = slices.DeleteFunc(fs, func(f func(context.Context) error) bool { return f == nil }) + switch len(fs) { + case 0: + return nil + case 1: + return fs[0] + } + return func(ctx context.Context) error { + errs := make([]error, len(fs)) + var wg sync.WaitGroup + for i, f := range fs { + wg.Add(1) + go func() { + defer wg.Done() + errs[i] = f(ctx) + }() + } + wg.Wait() + return errors.Join(errs...) + } } type ByteReadCloser interface { @@ -120,65 +143,6 @@ type ByteReadCloser interface { io.Closer } -type CompleteReceiver[T any] struct { - v T - ready bool -} - -func (r *CompleteReceiver[T]) Receive() (T, error) { - defer func() { - r.ready = false - }() - if !r.ready { - return CompleteReceiver[T]{}.v, io.EOF - } - return r.v, nil -} - -func (r *CompleteReceiver[T]) Close() error { - if c, ok := (any)(r.v).(io.Closer); ok { - return c.Close() - } - return nil -} - -func NewCompleteReceiver[T any](v T) *CompleteReceiver[T] { - return &CompleteReceiver[T]{v, true} -} - -type DecodeReceiver[T any] struct { - r IndexReadCloser - decode func(IndexReadCloser) (T, error) -} - -func (r *DecodeReceiver[T]) Receive() (T, error) { - return r.decode(r.r) -} - -func (r *DecodeReceiver[T]) Close() error { - return r.r.Close() -} - -func NewDecodeReceiver[T any](r IndexReadCloser, decode func(IndexReadCloser) (T, error)) *DecodeReceiver[T] { - return &DecodeReceiver[T]{r, decode} -} - -type nestedReceiver[T any, A Receiver[B], B Receiver[T]] struct { - rx A -} - -func (r nestedReceiver[T, A, B]) Receive() (Receiver[T], error) { - return r.rx.Receive() -} - -func (r nestedReceiver[T, A, B]) Close() error { - return r.rx.Close() -} - -func NewNestedReceiver[T any, A Receiver[B], B Receiver[T]](rx A) Receiver[Receiver[T]] { - return nestedReceiver[T, A, B]{rx} -} - type BufReadCloser struct { *bufio.Reader io.Closer diff --git a/js/README.md b/js/README.md index f85c2d955..354dad07d 100644 --- a/js/README.md +++ b/js/README.md @@ -60,11 +60,12 @@ that I/O fails, the invocation fails and the transport is aborted. ### Streams and futures -`t.stream(elem)` and `t.future(some)` describe asynchronous values. A `stream` -parameter is given as an (async-)iterable of chunks (a `Uint8Array` for -`stream`, an array of elements otherwise); a `future` as a value, a promise, -or a thunk. Decoded `stream` results are async iterables of chunks and `future` -results are promises: +`t.stream(elem)` and `t.future(some)` describe asynchronous values, and +`t.future()` a bare `future` without a payload. A `stream` parameter is given as +an (async-)iterable of chunks (a `Uint8Array` for `stream`, an array of +elements otherwise); a `future` as a value, a promise, or a thunk. Decoded +`stream` results are async iterables of chunks and `future` results are +promises, resolving to `undefined` for a bare `future`: ```js import { t, invoke, fromWebStreams } from "@bytecodealliance/wrpc"; @@ -98,13 +99,19 @@ consumed, as in the Rust implementation. So consume streams concurrently with each other, with awaiting futures, and with `done`: awaiting a future while an earlier stream is left unread can wait forever, since the future's data may be queued behind the stream's on the transport. Leave a `for await` loop early (or call -`cancel()`) to discard the rest of a stream you no longer need, including the -streams and futures nested in the chunks you did not receive. Those nested in -the chunks you did receive stay live, so consume or cancel them too. +`cancel()`) to discard the rest of a stream you no longer need. + +As in the Rust implementation, received `list` and `stream` elements may not +carry pending `stream` or `future` values: receiving one fails the invocation, as +does sending one in a `stream` element. Data the peer sends for a sub-stream that the decoded values do not carry — for a `stream` in an `option` that is `none`, say — fails the invocation. +Received values never fail: if receiving a `future` fails, its promise never +settles, and if receiving a `stream` fails, it ends. The failure is reported by +`done`, as in the Rust implementation. + ### Serving `accept` is the server-side counterpart: read an invocation's header off a diff --git a/js/package-lock.json b/js/package-lock.json index 2179cd4b8..51acb0f18 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bytecodealliance/wrpc", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bytecodealliance/wrpc", - "version": "0.2.1", + "version": "0.3.0", "license": "Apache-2.0 WITH LLVM-exception", "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/js/package.json b/js/package.json index faa0df32f..a2267df87 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "@bytecodealliance/wrpc", - "version": "0.2.1", + "version": "0.3.0", "description": "Transport-agnostic, component-native wRPC client and server for JavaScript", "type": "module", "license": "Apache-2.0 WITH LLVM-exception", diff --git a/js/src/index.d.ts b/js/src/index.d.ts index 4d1027462..6b45b22a2 100644 --- a/js/src/index.d.ts +++ b/js/src/index.d.ts @@ -26,7 +26,7 @@ export type Type = | { kind: "variant"; cases: { name: string; ty: Type | null }[] } | { kind: "enum"; names: string[] } | { kind: "flags"; names: string[] } - | { kind: "future"; some: Type } + | { kind: "future"; some: Type | null } | { kind: "stream"; elem: Type } | { kind: "own" | "borrow"; name: string }; @@ -46,20 +46,16 @@ export type Value = /** * A descriptor recorded by {@link decodeValue} for a pending `stream`/`future` - * value, naming the relative sub-stream `path` and the placeholder to feed, or - * for a `stream` sent `inline` as a single ready chunk whose elements carry - * pending values. + * value, naming the relative sub-stream `path` and the placeholder to feed. */ export type DecodeDeferred = | { path: number[]; kind: "future"; - ty: Type; + ty: Type | null; resolve: (value: Value) => void; - reject: (err: unknown) => void; } - | { path: number[]; kind: "stream"; ty: Type; queue: Chan } - | { path: number[]; kind: "inline"; queue: Chan }; + | { path: number[]; kind: "stream"; ty: Type; queue: Chan }; // ----- types.js ------------------------------------------------------------- @@ -79,7 +75,7 @@ export namespace types { export const string: Type; export function list(elem: Type): Type; export function option(some: Type): Type; - export function future(some: Type): Type; + export function future(some?: Type | null): Type; export function stream(elem: Type): Type; export function result(ok?: Type | null, err?: Type | null): Type; export function tuple(...elems: Type[]): Type; @@ -138,8 +134,6 @@ export class Chan implements AsyncIterable { readonly capacity: number; readonly closed: boolean; readonly cancelled: boolean; - /** The number of values handed to the consumer. */ - readonly taken: number; /** * Call `f` once the consumer cancels the channel, or now if it already has. * Hooks are dropped once the consumer has received the end of the channel. @@ -181,7 +175,7 @@ export function encodeValue( w: Writer, ty: Type, v: Value, - sink?: { path: number[]; kind: "stream" | "future"; ty: Type; value: Value }[], + sink?: { path: number[]; kind: "stream" | "future"; ty: Type | null; value: Value }[], path?: number[], ): void; export function decodeValue(r: Reader, ty: Type, sink?: DecodeDeferred[], path?: number[]): Value; @@ -223,6 +217,7 @@ export class Outgoing { constructor(mux: Mux, path: number[]); path: number[]; write(data: Uint8Array): Promise; + close(): Promise; index(rel: number[]): Outgoing; } @@ -239,8 +234,7 @@ export class Mux { incoming(path: number[]): AsyncReader; /** * Declare the sub-streams of the pending values in the value decoded at - * `path` (the root tuple, a future's value, or a stream's element, in - * order), with paths relative to it. + * `path` (the root tuple, or a future's value), with paths relative to it. */ seal(path: number[], expected: { path: number[]; nested: boolean }[]): void; /** @@ -249,10 +243,8 @@ export class Mux { * fails the invocation. */ finish(path: number[], reader?: AsyncReader): void; - /** Discard the sub-stream at `path` and those nested in its children from index `from` (default 0) on. */ - discard(path: number[], from?: number): void; - /** Call `f` once the sub-stream at `path` is discarded. */ - ondiscard(path: number[], f: () => void): void; + /** Discard the sub-stream at `path`. */ + discard(path: number[]): void; outgoing(path: number[]): Outgoing; abort(err?: unknown): void; closeWrite(): Promise; diff --git a/js/src/io.js b/js/src/io.js index 552eb132a..e47756fac 100644 --- a/js/src/io.js +++ b/js/src/io.js @@ -269,8 +269,6 @@ export class Chan { this.senders = []; this.closed = false; this.cancelled = false; - /** The number of values handed to the consumer. */ - this.taken = 0; /** @type {unknown} */ this.error = undefined; } @@ -297,7 +295,6 @@ export class Chan { this.queue.push(value); return; } - this.taken++; w.resolve({ value, done: false }); } @@ -352,7 +349,6 @@ export class Chan { next() { if (this.queue.length) { const value = /** @type {T} */ (this.queue.shift()); - this.taken++; this.senders.shift()?.(); return Promise.resolve({ value, done: false }); } diff --git a/js/src/mux.js b/js/src/mux.js index 7e4b93283..3b7c09842 100644 --- a/js/src/mux.js +++ b/js/src/mux.js @@ -34,6 +34,7 @@ export const DISCARDED = new Error("sub-stream discarded"); * @property {number[]} path * @property {Chan} chan * @property {boolean} empty whether only empty frames arrived for it + * @property {boolean} closed whether an empty frame closed it */ /** @@ -49,13 +50,9 @@ export const DISCARDED = new Error("sub-stream discarded"); * @property {Chan | undefined} chan the data of the sub-stream at * this path, if one is expected here * @property {boolean} sealed whether every sub-stream nested in it is known - * @property {number} decided the number of stream elements known, whose - * children are all in the trie * @property {boolean} done whether all its own data has been read - * @property {Set | undefined} pruned the indices of the children - * forgotten once finished, beyond those of the `decided` stream elements - * @property {number} discardFrom `Infinity`, unless the node is discarded: then - * its own data and that of its children from this index on is dropped + * @property {boolean} closed whether an empty frame closed it + * @property {boolean} discarded whether its data is dropped * @property {Map> | undefined} pending the data * buffered for the paths through each missing child, by path key */ @@ -65,10 +62,9 @@ const node = (sealed) => ({ children: new Map(), chan: undefined, sealed, - decided: 0, done: false, - pruned: undefined, - discardFrom: Infinity, + closed: false, + discarded: false, pending: undefined, }); @@ -100,7 +96,7 @@ function* readFrameHeader(r) { } /** @param {number[]} path */ -const unexpectedError = (path) => new Error(`unexpected data for sub-stream [${path}]`); +export const unexpectedError = (path) => new Error(`unexpected data for sub-stream [${path}]`); /** @param {number[]} path @returns {never} */ function unexpected(path) { @@ -150,11 +146,25 @@ export class Outgoing { this.path = path; } - /** @param {Uint8Array} data @returns {Promise} */ + /** + * Write `data` as a frame, unless it is empty, since an empty frame closes + * the sub-stream. + * @param {Uint8Array} data @returns {Promise} + */ write(data) { + if (data.length === 0) return Promise.resolve(); return this.mux._writeFrame(this.path, data); } + /** + * Close the sub-stream by writing an empty frame. No more data may be + * written to it afterwards. + * @returns {Promise} + */ + close() { + return this.mux._writeFrame(this.path, new Uint8Array(0)); + } + /** @param {number[]} rel @returns {Outgoing} */ index(rel) { return new Outgoing(this.mux, [...this.path, ...rel]); @@ -170,7 +180,9 @@ export class Outgoing { * the root sub-stream `[]`, and those declared with {@link Mux.seal} as each * value carrying pending `stream`/`future` values is decoded, until they are * {@link Mux.finish finished}. Data for a path that may still be declared is - * buffered until it is; data for any other path fails the invocation. + * buffered until it is; data for any other path fails the invocation. Pending + * values nested in `list` or `stream` elements are not supported, so the + * expected sub-streams are bounded by the types of the decoded values. */ export class Mux { /** @param {Transport} transport */ @@ -216,8 +228,8 @@ export class Mux { if (!(await m.fill(1))) break; const [path, n] = await m.decode(readFrameHeader); if (n === 0) { - // Peers may send empty frames, even for sub-streams that are finished. - this._route(path, true); + // An empty frame closes its sub-stream, which may already be finished. + this._route(path, true)?.close(); continue; } const chan = this._route(path); @@ -234,44 +246,43 @@ export class Mux { } /** - * The channel of the sub-stream at `path`, `null` if its data is discarded, - * or where the lookup stopped if it is not yet known whether it is expected. - * With `empty`, for an empty frame, a finished sub-stream also gives `null`. + * The node of the expected sub-stream at `path`, or where the lookup + * stopped if it is not yet known whether it is expected. With `empty`, for + * an empty frame, a finished sub-stream is found too. * @param {number[]} path * @param {boolean} [empty] - * @returns {Chan | null | Undecided} + * @returns {Node | Undecided} */ _find(path, empty = false) { let n = this.root; for (const i of path) { - if (i >= n.discardFrom) return null; const child = n.children.get(i); - if (!child) { - const finished = i < n.decided || !!n.pruned?.has(i); - if (empty && finished) return null; - return n.sealed || finished ? unexpected(path) : { at: n, i }; - } + if (!child) return n.sealed ? unexpected(path) : { at: n, i }; n = child; } - if (n.done) return empty ? null : unexpected(path); - if (n.discardFrom !== Infinity) return null; - return n.chan ?? unexpected(path); + if (!n.chan || n.closed || (n.done && !empty)) unexpected(path); + return n; } /** * The channel to route the data of the sub-stream at `path` to, or - * `undefined` to drop it. With `empty`, for an empty frame, the data of a - * path that may still be expected is buffered as if it were finished. + * `undefined` to drop it. With `empty`, for an empty frame, the sub-stream + * is closed, and the data of a path that may still be expected is buffered + * as if it were finished. * @param {number[]} path * @param {boolean} [empty] */ _route(path, empty = false) { const found = this._find(path, empty); - if (found === null) return undefined; - if (found instanceof Chan) return found; + if (!("at" in found)) { + found.closed ||= empty; + return found.done || found.discarded ? undefined : found.chan; + } let pending = found.at.pending?.get(found.i)?.get(key(path)); - if (!pending) attach(found, (pending = { path, chan: new Chan(), empty })); + if (pending?.closed) unexpected(path); + if (!pending) attach(found, (pending = { path, chan: new Chan(), empty, closed: false })); pending.empty &&= empty; + pending.closed ||= empty; return pending.chan; } @@ -284,7 +295,6 @@ export class Mux { _takePending(path) { let n = this.root; for (const i of path) { - if (i >= n.discardFrom) return undefined; const child = n.children.get(i); if (!child) { const group = n.pending?.get(i); @@ -299,15 +309,14 @@ export class Mux { } /** - * The node at `path`, `undefined` if there is none, or `null` if it is - * discarded by an ancestor. With `create`, missing nodes are created. + * The node at `path`, `undefined` if there is none. With `create`, missing + * nodes are created. * @param {number[]} path * @param {boolean} [create] */ _get(path, create = false) { let n = this.root; for (const i of path) { - if (i >= n.discardFrom) return null; let child = n.children.get(i); if (!child) { if (!create) return undefined; @@ -321,39 +330,27 @@ export class Mux { /** * Declare the sub-streams nested in the value at `path` once it is decoded: - * the root tuple at `[]`, a future's value at its own path, or element `k` - * of a stream at `[...stream, k]`, decoded in order. `expected` lists the - * paths, relative to `path`, of the value's pending `stream`/`future` - * values, and whether each may nest sub-streams of its own. Data for any - * other path nested in the value fails the invocation. + * the root tuple at `[]`, or a future's value at its own path. `expected` + * lists the paths, relative to `path`, of the value's pending + * `stream`/`future` values, and whether each may nest sub-streams of its + * own. Data for any other path nested in the value fails the invocation. * @param {number[]} path * @param {{ path: number[], nested: boolean }[]} expected */ seal(path, expected) { /** @type {Undecided[]} */ const settle = []; - if (path.length) { - const parent = this._get(path.slice(0, -1)); - if (parent && !parent.sealed) { - const from = parent.decided; - parent.decided = Math.max(from, path[path.length - 1] + 1); - for (let i = from; i < parent.decided; i++) settle.push({ at: parent, i }); - } - } const taken = expected.map((e) => this._takePending([...path, ...e.path])); const scope = this._get(path, expected.length > 0); if (scope) scope.sealed = true; expected.forEach((e, j) => { const t = taken[j]; if (t) settle.push(t); - const n = this._get([...path, ...e.path], true); - if (!n) { - t?.pending?.chan.cancel(DISCARDED); - return; - } + const n = /** @type {Node} */ (this._get([...path, ...e.path], true)); n.sealed = !e.nested; if (n.chan) return; n.chan = t?.pending?.chan ?? new Chan(); + n.closed = t?.pending?.closed ?? false; n.chan.capacity = CAPACITY; if (this.inboundClosed) n.chan.close(this.inboundError); }); @@ -363,8 +360,7 @@ export class Mux { /** * Declare that the sub-stream at `path` has no more data: its stream ended, - * or its future's value was decoded. Once the sub-streams nested in it - * finish too, it is forgotten, and any further data for it fails the + * or its future's value was decoded. Any further data for it fails the * invocation, as does any data of it left unread in `reader`. * @param {number[]} path * @param {AsyncReader} [reader] the reader it was read over @@ -379,68 +375,23 @@ export class Mux { n.done = true; n.chan?.close(); for (const i of [...(n.pending?.keys() ?? [])]) this._settle({ at: n, i }); - this._prune(path); } /** - * Forget the node at `path` and its ancestors as long as they are finished, - * without children, and not discarded. + * Discard the sub-stream at `path`: drop its buffered and future data, and + * fail its reader with {@link DISCARDED}. * @param {number[]} path */ - _prune(path) { - const nodes = [this.root]; - for (const i of path) { - const child = nodes[nodes.length - 1].children.get(i); - if (!child) return; - nodes.push(child); - } - for (let d = path.length; d > 0; d--) { - const n = nodes[d]; - if (n.children.size || n.discardFrom !== Infinity || (n.chan && !n.done)) return; - const parent = nodes[d - 1]; - const i = path[d - 1]; - parent.children.delete(i); - if (i >= parent.decided) (parent.pruned ??= new Set()).add(i); - } - } - - /** - * Discard the sub-stream at `path` and those nested in its children from - * index `from` on: drop their buffered and future data, and fail their - * readers with {@link DISCARDED}. - * @param {number[]} path - * @param {number} [from] - */ - discard(path, from = 0) { + discard(path) { const n = this._get(path); - if (!n || (n.done && ![...n.children.keys()].some((i) => i >= from))) return; - n.discardFrom = Math.min(n.discardFrom, from); + if (!n || n.done) return; + n.discarded = true; n.chan?.cancel(DISCARDED); - for (const [i, group] of n.pending ?? []) { - if (i < from) continue; - for (const { chan } of group.values()) chan.cancel(DISCARDED); - n.pending?.delete(i); - } - for (const [i, child] of n.children) { - if (i < from) continue; - eachChan(child, (chan) => chan.cancel(DISCARDED)); - n.children.delete(i); - } - } - - /** - * Call `f` once the sub-stream at `path` is discarded. - * @param {number[]} path @param {() => void} f - */ - ondiscard(path, f) { - const chan = this._get(path)?.chan; - if (chan) chan.onCancel(f); - else f(); } /** * Look up the paths buffered in the group at `at` again: drop or reject - * those decided since, and keep the rest where their lookup stops now. + * those declared since, and keep the rest where their lookup stops now. * @param {Undecided} at */ _settle(at) { @@ -455,8 +406,8 @@ export class Mux { this.abort(err); return; } - if (found === null || found instanceof Chan) pending.chan.cancel(DISCARDED); - else attach(found, pending); + if ("at" in found) attach(found, pending); + else pending.chan.cancel(DISCARDED); } } @@ -469,17 +420,14 @@ export class Mux { } /** - * The reader for the expected sub-stream at `path` (the root is `[]`). The - * reader of a discarded sub-stream fails with {@link DISCARDED}. + * The reader for the expected sub-stream at `path` (the root is `[]`). * @param {number[]} path * @returns {AsyncReader} */ incoming(path) { - const n = this._get(path); - if (n !== null && !n?.chan) throw new Error(`no sub-stream is expected at [${path}]`); - const chan = n?.chan; + const chan = this._get(path)?.chan; + if (!chan) throw new Error(`no sub-stream is expected at [${path}]`); return new AsyncReader(async () => { - if (!chan) throw DISCARDED; const { value, done } = await chan.next(); return done ? null : value; }); diff --git a/js/src/session.js b/js/src/session.js index 4d56fa395..e8749069f 100644 --- a/js/src/session.js +++ b/js/src/session.js @@ -10,7 +10,7 @@ // writer task per sub-stream. import { Writer, atomic, readVarU32 } from "./io.js"; -import { DISCARDED, Mux, PROTOCOL } from "./mux.js"; +import { DISCARDED, Mux, PROTOCOL, unexpectedError } from "./mux.js"; import { decodeSteps, encodeValue, hasAsync, readChunk, toBytes } from "./value.js"; /** @typedef {import("./index.d.ts").Type} Type */ @@ -63,22 +63,20 @@ function writeDeferred(out, entry) { : writeStream(out, entry.ty, entry.value); } -/** @param {Outgoing} out @param {Type} ty @param {any} value */ +/** @param {Outgoing} out @param {Type | null} ty @param {any} value */ async function writeFuture(out, ty, value) { const v = await resolveFuture(value); const w = new Writer(); /** @type {any[]} */ const sink = []; - encodeValue(w, ty, v, sink, []); - await out.write(w.finish()); + if (ty) encodeValue(w, ty, v, sink, []); + const bytes = w.finish(); + await (bytes.length > 0 ? out.write(bytes) : out.close()); await Promise.all(sink.map((e) => writeDeferred(out.index(e.path), e))); } /** @param {Outgoing} out @param {Type} elem @param {any} src */ async function writeStream(out, elem, src) { - let base = 0; - /** @type {Promise[]} */ - const tasks = []; for await (const chunk of streamChunks(src, elem)) { const w = new Writer(); if (elem.kind === "u8") { @@ -87,29 +85,21 @@ async function writeStream(out, elem, src) { if (bytes.length === 0) continue; w.varU(bytes.length); w.bytes(bytes); - base += bytes.length; } else { if (!Array.isArray(chunk)) throw new TypeError("expected an array for a `stream` chunk"); if (chunk.length === 0) continue; w.varU(chunk.length); - for (let k = 0; k < chunk.length; k++) { - /** @type {any[]} */ - const sink = []; - encodeValue(w, elem, chunk[k], sink, []); - for (const e of sink) { - const task = writeDeferred(out.index([base + k, ...e.path]), e); - task.catch(() => {}); - tasks.push(task); - } - } - base += chunk.length; + /** @type {any[]} */ + const sink = []; + for (const v of chunk) encodeValue(w, elem, v, sink, []); + if (sink.length) + throw new Error("pending values in `list` or `stream` elements are not supported"); } await out.write(w.finish()); } const end = new Writer(); end.varU(0); // a stream is terminated by an empty chunk await out.write(end.finish()); - await Promise.all(tasks); } // ----- decoding side -------------------------------------------------------- @@ -167,7 +157,7 @@ async function receiveTuple(mux, root, types) { function expect(mux, path, sink) { mux.seal( path, - sink.flatMap((e) => (e.kind === "inline" ? [] : [{ path: e.path, nested: hasAsync(e.ty) }])), + sink.map((e) => ({ path: e.path, nested: e.kind === "future" && !!e.ty && hasAsync(e.ty) })), ); } @@ -191,14 +181,6 @@ function readNested(mux, path, sink, ar) { */ async function readDeferred(mux, path, entry, ar) { switch (entry.kind) { - case "inline": { - // Discard the sub-streams nested in the chunk unless the consumer received it. - const queue = entry.queue; - queue.onCancel(() => { - if (queue.taken === 0) mux.discard(path); - }); - return; - } case "future": return readFuture(mux, path, entry, ar ?? mux.incoming(path)); case "stream": @@ -213,16 +195,17 @@ async function readDeferred(mux, path, entry, ar) { async function readFuture(mux, path, entry, ar) { let decoded; try { - decoded = await decodeOne(ar, entry.ty); + if (entry.ty) decoded = await decodeOne(ar, entry.ty); + else if (await ar.fill(1)) throw unexpectedError(path); + else decoded = { value: undefined, sink: [] }; } catch (err) { - entry.reject(err); if (err === DISCARDED) return; throw err; } const { value, sink } = decoded; expect(mux, path, sink); // A value nested at the future's own path continues reading it. - if (!sink.some((e) => e.kind !== "inline" && e.path.length === 0)) mux.finish(path, ar); + if (!sink.some((e) => e.path.length === 0)) mux.finish(path, ar); const nested = readNested(mux, path, sink, ar); entry.resolve(value); await nested; @@ -230,70 +213,28 @@ async function readFuture(mux, path, entry, ar) { /** * Receive a stream's chunks over `ar` into `queue`, waiting while it is full. - * If the consumer cancels the queue, the rest of the stream is discarded, - * along with the sub-streams nested in the elements it did not receive. + * If the consumer cancels the queue, the rest of the stream is discarded. * @param {Mux} mux @param {number[]} path @param {Type} elem * @param {import("./index.d.ts").Chan} queue @param {AsyncReader} ar */ async function readStream(mux, path, elem, queue, ar) { - const nested = hasAsync(elem); - /** @type {number[]} the index of the first element of each chunk not yet taken */ - const starts = []; - let taken = 0; - let base = 0; - queue.onCancel(() => mux.discard(path, starts[queue.taken - taken] ?? base)); - // Nobody receives the rest of a discarded stream. - mux.ondiscard(path, () => queue.cancel()); - /** @type {Promise[]} */ - const tasks = []; + queue.onCancel(() => mux.discard(path)); try { for (;;) { - // The peer may close the connection instead of sending the empty terminator. - if (!(await ar.fill(1))) { - mux.finish(path); - break; - } const len = await ar.decode((r) => atomic(r, readVarU32)); if (len === 0) { mux.finish(path, ar); break; } - if (nested) { - starts.splice(0, queue.taken - taken); - taken = queue.taken; - starts.push(base); - } - let chunk; - if (elem.kind === "u8") { - chunk = await ar.take(len); - } else if (!nested) { - chunk = await ar.decode((r) => readChunk(r, elem, len)); - } else { - chunk = []; - for (let k = 0; k < len; k++) { - const { value, sink } = await decodeOne(ar, elem); - chunk.push(value); - const at = [...path, base + k]; - expect(mux, at, sink); - if (!sink.length) continue; - const task = readNested(mux, at, sink); - task.catch(() => {}); - tasks.push(task); - } - } - base += len; + const chunk = + elem.kind === "u8" ? await ar.take(len) : await ar.decode((r) => readChunk(r, elem, len)); if (!(await queue.send(chunk))) break; } } catch (err) { - if (!queue.cancelled && err !== DISCARDED) { - // Surface a transport/decode failure to whoever is consuming the stream, - // rather than leaving the consumer hanging on a queue that never closes. - queue.close(err); - throw err; - } + if (!queue.cancelled && err !== DISCARDED) throw err; + } finally { + queue.close(); } - queue.close(); - await Promise.all(tasks); } // ----- public API ----------------------------------------------------------- diff --git a/js/src/types.js b/js/src/types.js index 61ee461ec..dcd5a2309 100644 --- a/js/src/types.js +++ b/js/src/types.js @@ -40,10 +40,11 @@ export const option = (some) => ({ kind: "option", some }); /** * `future`. Carries a single asynchronous value, multiplexed over the * invocation's framed stream. Requires the async session API (`invoke` / - * `accept`); the synchronous `encode`/`decode` cannot transmit it. - * @param {Type} some + * `accept`); the synchronous `encode`/`decode` cannot transmit it. Pass no + * type for a bare `future` without a payload. + * @param {Type | null} [some] */ -export const future = (some) => ({ kind: "future", some }); +export const future = (some = null) => ({ kind: "future", some }); /** * `stream`. Carries an asynchronous sequence of `elem` chunks, diff --git a/js/src/value.js b/js/src/value.js index 384ead3cd..958554c23 100644 --- a/js/src/value.js +++ b/js/src/value.js @@ -135,19 +135,22 @@ const COMPOSITE = new Set([ /** * Decode a single stream/list chunk of `n` elements of type `elem` from `r`. * `stream` chunks are returned as a `Uint8Array`; all others as an array. + * Elements carrying pending `stream`/`future` values are not supported. * @param {Reader} r * @param {Type} elem * @param {number} n - * @param {import("./index.d.ts").DecodeDeferred[]} [sink] - * @param {number[]} [path] * @returns {Generator} */ -export function* readChunk(r, elem, n, sink, path) { +export function* readChunk(r, elem, n) { if (elem.kind === "u8") return yield* atomic(r, (r) => r.take(n).slice()); const out = []; if (COMPOSITE.has(elem.kind)) { + /** @type {import("./index.d.ts").DecodeDeferred[]} */ + const sink = []; for (let i = 0; i < n; i++) { - out.push(yield* decodeSteps(r, elem, sink, childPath(sink, path, i))); + out.push(yield* decodeSteps(r, elem, sink, [])); + if (sink.length) + throw new Error("pending values in `list` or `stream` elements are not supported"); } return out; } @@ -178,7 +181,7 @@ export function* readChunk(r, elem, n, sink, path) { * @param {Writer} w * @param {Type} ty * @param {any} v - * @param {{ path: number[], kind: "stream" | "future", ty: Type, value: any }[]} [sink] + * @param {{ path: number[], kind: "stream" | "future", ty: Type | null, value: any }[]} [sink] * @param {number[]} [path] relative index path to `v` (only tracked with `sink`) */ export function encodeValue(w, ty, v, sink, path) { @@ -351,8 +354,8 @@ export function encodeValue(w, ty, v, sink, path) { * iterable of chunks for a `stream` — and a descriptor recording the relative * `path` and the placeholder's resolver/queue is appended to `sink`. Without a * `sink`, encountering one throws. A `stream` sent inline as a single ready - * chunk is decoded either way, and recorded in `sink` as `inline` if its - * elements carry pending values. + * chunk is decoded either way. Pending values nested in `list` or `stream` + * elements are not supported. * * @param {Reader} r * @param {Type} ty @@ -378,7 +381,7 @@ export function decodeValue(r, ty, sink, path) { export function* decodeSteps(r, ty, sink, path) { switch (ty.kind) { case "list": - return yield* readChunk(r, ty.elem, yield* atomic(r, readVarU32), sink, path); + return yield* readChunk(r, ty.elem, yield* atomic(r, readVarU32)); case "tuple": { const out = []; for (let i = 0; i < ty.elems.length; i++) { @@ -417,32 +420,23 @@ export function* decodeSteps(r, ty, sink, path) { } case "future": { if (yield* atomic(r, optionStatus)) { - return Promise.resolve(yield* decodeSteps(r, ty.some, sink, path)); + return Promise.resolve(ty.some ? yield* decodeSteps(r, ty.some, sink, path) : undefined); } if (!sink) throw new Error(asyncRequired(ty.kind)); /** @type {(value: any) => void} */ let resolve = () => {}; - /** @type {(err: unknown) => void} */ - let reject = () => {}; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - // The failure also surfaces through `done`, so an ignored future must not be unhandled. - promise.catch(() => {}); - sink.push({ path: path ?? [], kind: "future", ty: ty.some, resolve, reject }); + const promise = new Promise((res) => (resolve = res)); + sink.push({ path: path ?? [], kind: "future", ty: ty.some, resolve }); return promise; } case "stream": { if (yield* atomic(r, optionStatus)) { // A stream sent inline as a single ready chunk. const n = yield* atomic(r, readVarU32); - const before = sink?.length ?? 0; - const chunk = yield* readChunk(r, ty.elem, n, sink, path); + const chunk = yield* readChunk(r, ty.elem, n); const q = new Chan(); if (n > 0) q.push(chunk); q.close(); - if (sink && sink.length > before) sink.push({ path: path ?? [], kind: "inline", queue: q }); return q; } if (!sink) throw new Error(asyncRequired(ty.kind)); diff --git a/js/test/session.test.js b/js/test/session.test.js index 80734ffae..8ccdab0f2 100644 --- a/js/test/session.test.js +++ b/js/test/session.test.js @@ -128,26 +128,6 @@ test("aborting after the peer closed fails the sub-streams declared later", asyn await assert.rejects(mux.incoming([0]).u8(), /boom/); }); -test("finished sub-streams are forgotten", { timeout: 1000 }, async () => { - const [client, server] = duplexPair(); - const ty = t.stream(t.option(t.future(t.u8))); - const elems = Array.from({ length: 1000 }, (_, i) => (i % 2 ? i & 0xff : undefined)); - const serving = (async () => { - const inv = await accept(server); - const { params, done } = await inv.receiveParams([ty]); - const futures = []; - for await (const chunk of params[0]) futures.push(...chunk); - const values = await Promise.all(futures); - await done; - assert.equal(inv.mux.root.children.size, 0); - await inv.sendResults([], []); - return values; - })(); - const { done } = await invoke(client, STREAMS, "get", [ty], [[elems]], []); - await done; - assert.deepEqual(await serving, elems); -}); - test("a failed invocation leaves no unhandled rejection", async () => { const transport = { read: () => Promise.reject(new Error("connection refused")), @@ -237,47 +217,6 @@ test("aborting a web stream transport cancels and aborts both sides", async () = ]); }); -test("a failed stream> parameter leaves no unhandled rejection", async () => { - async function* chunks() { - yield [ - () => { - throw new Error("future failed"); - }, - ]; - throw new Error("stream failed"); - } - const transport = { - read: () => new Promise((resolve) => setTimeout(() => resolve(null), 10)), - write: () => {}, - }; - await assert.rejects( - invoke(transport, STREAMS, "echo", [t.stream(t.future(t.string))], [chunks()], [t.u8]), - /stream failed/, - ); -}); - -test( - "a transport failure rejects pending stream> parameters", - { timeout: 1000 }, - async () => { - const frames = []; - const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); - await peer.writeHeader(STREAMS, "echo"); - await peer.outgoing([]).write(Uint8Array.of(0)); - await peer.outgoing([0]).write(Uint8Array.of(1, 0)); - const transport = { - read: async () => frames.shift() ?? Promise.reject(new Error("connection reset")), - write: () => {}, - }; - const inv = await accept(transport); - const { params } = await inv.receiveParams([t.stream(t.future(t.string))]); - const chunks = params[0][Symbol.asyncIterator](); - const { value: chunk } = await chunks.next(); - await assert.rejects(chunk[0], /connection reset/); - await assert.rejects(chunks.next(), /connection reset/); - }, -); - test("frame data is routed as it arrives", async () => { const bytes = [0x01, 0x00, 0x03, 0x0a, 0x14, 0x1e, 0x01, 0x00, 0x02, 0x28]; const transport = { @@ -335,15 +274,17 @@ test("invalid arguments fail before anything is written and abort the transport" assert.equal(aborted.length, 2); }); -test("stream chunks must be bytes", async () => { - const transport = { - read: () => new Promise((resolve) => setTimeout(() => resolve(null), 10)), - write: () => {}, - }; - await assert.rejects( - invoke(transport, STREAMS, "echo", [t.stream(t.u8)], [[1, 2, 3]], [t.u8]), - /expected `Uint8Array`/, - ); +test("invalid stream chunks fail the invocation", async () => { + for (const [ty, chunks, error] of [ + [t.stream(t.u8), [1, 2, 3], /expected `Uint8Array`/], + [t.stream(t.future(t.string)), [["foo"]], /not supported/], + ]) { + const transport = { + read: () => new Promise((resolve) => setTimeout(() => resolve(null), 10)), + write: () => {}, + }; + await assert.rejects(invoke(transport, STREAMS, "echo", [ty], [chunks], [t.u8]), error); + } }); for (const [name, values, error] of [ @@ -409,77 +350,6 @@ test("a cancelled stream result is discarded", { timeout: 1000 }, async () => { assert.deepEqual(await results[0].next(), { value: undefined, done: true }); }); -test( - "cancelling a stream keeps the streams nested in the chunks it delivered", - { timeout: 1000 }, - async () => { - const ty = t.tuple(t.stream(t.stream(t.u8)), t.stream(t.u8)); - const bytes = function* () { - for (let i = 0; i < 1000; i++) yield Uint8Array.of(i & 0xff); - }; - const { result, done, serving } = await echo(ty, [[[bytes(), bytes()], [bytes()]], bytes()]); - const [nested, sibling] = result; - let kept = []; - for await (const chunk of nested) { - kept = chunk; - break; - } - assert.equal(kept.length, 2); - kept[1].cancel(); - // Undiscarded nested data would stop the transport from being read. - const [received, siblings] = await Promise.all([collect(kept[0]), collect(sibling)]); - assert.equal(received.length, 1000); - assert.equal(siblings.length, 1000); - await Promise.all([done, serving]); - }, -); - -test( - "cancelling a received stream discards the streams nested in it", - { timeout: 1000 }, - async () => { - const frames = []; - const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); - await peer.outgoing([]).write(Uint8Array.of(0, 0)); - await peer.outgoing([0]).write(Uint8Array.of(1, 0)); - await peer.outgoing([0]).write(Uint8Array.of(0)); - for (let i = 0; i < 1000; i++) await peer.outgoing([0, 0]).write(Uint8Array.of(1, i & 0xff)); - await peer.outgoing([0, 0]).write(Uint8Array.of(0)); - await peer.outgoing([1]).write(Uint8Array.of(1, 7)); - await peer.outgoing([1]).write(Uint8Array.of(0)); - const transport = { read: async () => frames.shift() ?? null, write: () => {} }; - const ty = [t.stream(t.stream(t.u8)), t.stream(t.u8)]; - const { results, done } = await invoke(transport, STREAMS, "get", [], [], ty); - while (!results[0].closed) await tick(); - let hooked = false; - results[0].onCancel(() => (hooked = true)); - results[0].cancel(); - assert.ok(hooked); - assert.deepEqual(await collect(results[1]), [7]); - await done; - }, -); - -test( - "cancelling an inline stream discards the streams nested in it", - { timeout: 1000 }, - async () => { - const frames = []; - const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); - await peer.outgoing([]).write(Uint8Array.of(1, 1, 0, 0)); - for (let i = 0; i < 1000; i++) await peer.outgoing([0, 0]).write(Uint8Array.of(1, i & 0xff)); - await peer.outgoing([0, 0]).write(Uint8Array.of(0)); - await peer.outgoing([1]).write(Uint8Array.of(1, 7)); - await peer.outgoing([1]).write(Uint8Array.of(0)); - const transport = { read: async () => frames.shift() ?? null, write: () => {} }; - const ty = [t.stream(t.stream(t.u8)), t.stream(t.u8)]; - const { results, done } = await invoke(transport, STREAMS, "get", [], [], ty); - results[0].cancel(); - assert.deepEqual(await collect(results[1]), [7]); - await done; - }, -); - test("data arriving before its sub-stream is decoded is kept", { timeout: 1000 }, async () => { const frames = []; const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); @@ -511,35 +381,6 @@ test( }, ); -test("element data arriving ahead of a large chunk is kept", { timeout: 5000 }, async () => { - const n = 4000; - const frames = []; - const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); - await peer.outgoing([]).write(Uint8Array.of(0)); - for (let i = 0; i < n; i++) await peer.outgoing([0, i]).write(Uint8Array.of(i & 0xff)); - const w = new Writer(); - w.varU(n); - for (let i = 0; i < n; i++) w.u8(0); - await peer.outgoing([0]).write(w.finish()); - await peer.outgoing([0]).write(Uint8Array.of(0)); - const transport = { read: async () => frames.shift() ?? null, write: () => {} }; - const { results, done } = await invoke( - transport, - STREAMS, - "get", - [], - [], - [t.stream(t.future(t.u8))], - ); - const out = []; - for await (const chunk of results[0]) out.push(...(await Promise.all(chunk))); - assert.deepEqual( - out, - Array.from({ length: n }, (_, i) => i & 0xff), - ); - await done; -}); - test( "more data than a sub-stream buffers arriving ahead of its declaration is kept", { timeout: 1000 }, @@ -550,66 +391,36 @@ test( await peer.outgoing([]).write(Uint8Array.of(0)); for (let i = 0; i < n; i++) await peer.outgoing([0, 0]).write(Uint8Array.of(1, i & 0xff)); await peer.outgoing([0, 0]).write(Uint8Array.of(0)); - await peer.outgoing([0]).write(Uint8Array.of(1, 0)); await peer.outgoing([0]).write(Uint8Array.of(0)); const transport = { read: async () => frames.shift() ?? null, write: () => {} }; - const { results, done } = await invoke( - transport, - STREAMS, - "get", - [], - [], - [t.stream(t.stream(t.u8))], - ); - const inner = []; - for await (const chunk of results[0]) inner.push(...chunk); - assert.equal(inner.length, 1); + const ty = [t.future(t.tuple(t.stream(t.u8)))]; + const { results, done } = await invoke(transport, STREAMS, "get", [], [], ty); + const [inner] = await results[0]; assert.deepEqual( - await collect(inner[0]), + await collect(inner), Array.from({ length: n }, (_, i) => i & 0xff), ); await done; }, ); - -test("data for a sub-stream the reply does not carry fails it", { timeout: 1000 }, async () => { - const frames = []; - const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); - await peer.outgoing([]).write(Uint8Array.of(0, 0)); - for (let i = 0; i < 100; i++) await peer.outgoing([0]).write(Uint8Array.of(1, i)); - await peer.outgoing([1]).write(Uint8Array.of(1, 7)); - await peer.outgoing([1]).write(Uint8Array.of(0)); - let aborted; - const transport = { - read: async () => frames.shift() ?? null, - write: () => {}, - abort: (reason) => void (aborted = reason), - }; - const ty = [t.option(t.stream(t.u8)), t.stream(t.u8)]; - const { results, done } = await invoke(transport, STREAMS, "get", [], [], ty); - assert.equal(results[0], undefined); - await assert.rejects(collect(results[1]), /unexpected data for sub-stream \[0\]/); - await assert.rejects(done, /unexpected data for sub-stream \[0\]/); - assert.match(aborted.message, /unexpected data/); -}); - -test( - "an empty frame for a sub-stream the reply does not carry fails it", - { timeout: 1000 }, - async () => { +test("an invocation sends no frames for empty data", async () => { + for (const [types, args, expected] of [ + [[], [], "\x00\x03foo\x03bar"], + [[t.future()], [Promise.resolve()], "\x00\x03foo\x03bar\x00\x01\x00\x01\x00\x00"], + ]) { const frames = []; - const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); - await peer.outgoing([]).write(Uint8Array.of(0, 0)); - await peer.outgoing([0]).write(new Uint8Array(0)); - await peer.outgoing([1]).write(Uint8Array.of(1, 7)); - await peer.outgoing([1]).write(Uint8Array.of(0)); - const transport = { read: async () => frames.shift() ?? null, write: () => {} }; - const ty = [t.option(t.stream(t.u8)), t.stream(t.u8)]; - const { results, done } = await invoke(transport, STREAMS, "get", [], [], ty); - await assert.rejects(collect(results[1]), /unexpected data for sub-stream \[0\]/); - await assert.rejects(done, /unexpected data for sub-stream \[0\]/); - }, -); + let closed = false; + const transport = { + read: async () => null, + write: (bytes) => void frames.push(bytes), + closeWrite: () => void (closed = true), + }; + const { done } = await invoke(transport, "foo", "bar", types, args, []); + await done; + assert.ok(closed); + assert.deepEqual(Buffer.concat(frames), Buffer.from(expected, "latin1")); + } +}); /** A transport fed by the frames a peer writes, until `inbound` is closed. */ function feed() { @@ -626,6 +437,24 @@ function feed() { return { inbound, peer, transport, aborted }; } +test("a future without a payload resolves at the end of the transport", async () => { + for (const data of [false, true]) { + const { inbound, peer, transport } = feed(); + await peer.outgoing([]).write(Uint8Array.of(0)); + if (data) await peer.outgoing([0]).write(Uint8Array.of(0)); + inbound.close(); + const { results, done } = await invoke(transport, STREAMS, "get", [], [], [t.future()]); + if (data) { + await assert.rejects(done, /unexpected data for sub-stream \[0\]/); + const pending = Symbol("pending"); + assert.equal(await Promise.race([results[0], pending]), pending); + } else { + assert.equal(await results[0], undefined); + await done; + } + } +}); + test("an empty frame for a finished sub-stream is accepted", { timeout: 1000 }, async () => { const { inbound, peer, transport, aborted } = feed(); await peer.outgoing([]).write(Uint8Array.of(0)); @@ -634,7 +463,7 @@ test("an empty frame for a finished sub-stream is accepted", { timeout: 1000 }, const { results, done } = await invoke(transport, STREAMS, "get", [], [], [t.stream(t.u8)]); assert.deepEqual(await collect(results[0]), [7]); await done; - await peer.outgoing([0]).write(new Uint8Array(0)); + await peer.outgoing([0]).close(); inbound.close(); const outcome = await Promise.race([ aborted, @@ -646,58 +475,22 @@ test("an empty frame for a finished sub-stream is accepted", { timeout: 1000 }, }); test( - "an empty frame for a sub-stream the reply does not carry fails it after a sibling's", - { timeout: 1000 }, - async () => { - const { inbound, peer, transport, aborted } = feed(); - await peer.outgoing([]).write(Uint8Array.of(0, 0)); - await peer.outgoing([1]).write(Uint8Array.of(7)); - const ty = [t.option(t.stream(t.u8)), t.future(t.u8)]; - const { results } = await invoke(transport, STREAMS, "get", [], [], ty); - assert.equal(await results[1], 7); - await peer.outgoing([0]).write(new Uint8Array(0)); - inbound.close(); - assert.match(String(await aborted), /unexpected data for sub-stream \[0\]/); - }, -); - -test( - "an empty frame arriving before its sub-stream finishes is accepted", + "an empty frame arriving before its sub-stream is declared closes it", { timeout: 1000 }, async () => { const frames = []; const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); await peer.outgoing([]).write(Uint8Array.of(0)); - await peer.outgoing([0, 0]).write(new Uint8Array(0)); - await peer.outgoing([0]).write(Uint8Array.of(1, 0)); + await peer.outgoing([0, 0]).close(); await peer.outgoing([0]).write(Uint8Array.of(0)); const transport = { read: async () => frames.shift() ?? null, write: () => {} }; - const ty = [t.stream(t.option(t.future(t.u8)))]; + const ty = [t.future(t.tuple(t.future()))]; const { results, done } = await invoke(transport, STREAMS, "get", [], [], ty); - assert.deepEqual(await collect(results[0]), [undefined]); + const [inner] = await results[0]; + assert.equal(await inner, undefined); await done; }, ); - -test( - "data for a stream after its end fails it, even once cancelled", - { timeout: 1000 }, - async () => { - const { inbound, peer, transport, aborted } = feed(); - await peer.outgoing([]).write(Uint8Array.of(0)); - await peer.outgoing([0]).write(Uint8Array.of(1, 0)); - await peer.outgoing([0]).write(Uint8Array.of(0)); - const ty = [t.stream(t.future(t.u8))]; - const { results } = await invoke(transport, STREAMS, "get", [], [], ty); - while (!results[0].closed) await tick(); - results[0].cancel(); - await peer.outgoing([0, 0]).write(Uint8Array.of(5)); - await peer.outgoing([0]).write(Uint8Array.of(1, 0)); - inbound.close(); - assert.match(String(await aborted), /unexpected data for sub-stream \[0\]/); - }, -); - test("frame data beyond the end of a sub-stream fails it", { timeout: 1000 }, async () => { for (const [ty, value, extra, split] of [ [t.future(t.u8), [5], 40, true], @@ -733,17 +526,18 @@ test( const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); await peer.outgoing([]).write(Uint8Array.of(0)); await peer.outgoing([0, 0]).write(Uint8Array.of(1, 0)); - await peer.outgoing([0]).write(Uint8Array.of(1, 0)); + await peer.outgoing([0]).write(Uint8Array.of(0)); for (let i = 0; i < 1000; i++) await peer.outgoing([0, 0]).write(Uint8Array.of(1, i & 0xff)); let reads = 0; const transport = { read: async () => (reads++, frames.shift() ?? null), write: () => {} }; - const { results } = await invoke(transport, STREAMS, "get", [], [], [t.stream(t.stream(t.u8))]); - await results[0][Symbol.asyncIterator]().next(); + const ty = [t.future(t.tuple(t.stream(t.u8)))]; + const { results } = await invoke(transport, STREAMS, "get", [], [], ty); + const [inner] = await results[0]; + await inner[Symbol.asyncIterator]().next(); await new Promise((resolve) => setTimeout(resolve, 50)); assert.ok(reads < 500, `read ${reads} frames`); }, ); - test("data for the root after the reply fails it", { timeout: 1000 }, async () => { const frames = []; const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); @@ -753,41 +547,28 @@ test("data for the root after the reply fails it", { timeout: 1000 }, async () = await peer.outgoing([0]).write(Uint8Array.of(0)); const transport = { read: async () => frames.shift() ?? null, write: () => {} }; const { results, done } = await invoke(transport, STREAMS, "get", [], [], [t.stream(t.u8)]); - await assert.rejects(collect(results[0]), /unexpected data for sub-stream \[\]/); + await collect(results[0]); await assert.rejects(done, /unexpected data for sub-stream \[\]/); }); -test("cancelling an inline stream discards the sub-streams nested in it", () => { - const mux = new Mux({ read: () => new Promise(() => {}), write: () => {} }); - mux.seal([], [{ path: [0], nested: true }]); - mux.seal([0], [{ path: [0], nested: false }]); - mux.finish([0]); - const nested = mux.incoming([0, 0]); - mux.discard([0]); - return assert.rejects(nested.u8(), /sub-stream discarded/); -}); - -test("a stream truncated mid-frame fails", { timeout: 1000 }, async () => { - const frames = []; - const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); - await peer.outgoing([]).write(Uint8Array.of(0)); - frames.push(Uint8Array.of(1, 0, 5, 1, 6)); - const transport = { read: async () => frames.shift() ?? null, write: () => {} }; - const { results, done } = await invoke(transport, STREAMS, "get", [], [], [t.stream(t.u8)]); - await assert.rejects(collect(results[0]), /unexpected end of input/); - await assert.rejects(done, /unexpected end of input/); -}); - -test("a stream truncated mid-length fails", { timeout: 1000 }, async () => { - const frames = []; - const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); - await peer.outgoing([]).write(Uint8Array.of(0)); - await peer.outgoing([0]).write(Uint8Array.of(1, 5)); - await peer.outgoing([0]).write(Uint8Array.of(0x80)); - const transport = { read: async () => frames.shift() ?? null, write: () => {} }; - const { results, done } = await invoke(transport, STREAMS, "get", [], [], [t.stream(t.u8)]); - await assert.rejects(collect(results[0]), /unexpected end of input/); - await assert.rejects(done, /unexpected end of input/); +test("a truncated stream fails", { timeout: 1000 }, async () => { + for (const send of [ + (peer, frames) => frames.push(Uint8Array.of(1, 0, 5, 1, 6)), + (peer) => peer.outgoing([0]).write(Uint8Array.of(1, 5, 0x80)), + async (peer) => { + await peer.outgoing([0]).write(Uint8Array.of(1, 7)); + await peer.outgoing([0]).close(); + }, + ]) { + const frames = []; + const peer = new Mux({ read: async () => null, write: (bytes) => void frames.push(bytes) }); + await peer.outgoing([]).write(Uint8Array.of(0)); + await send(peer, frames); + const transport = { read: async () => frames.shift() ?? null, write: () => {} }; + const { results, done } = await invoke(transport, STREAMS, "get", [], [], [t.stream(t.u8)]); + await collect(results[0]); + await assert.rejects(done, /unexpected end of input/); + } }); const futures = "wrpc-test:async/futures"; @@ -871,28 +652,79 @@ test("future> resolves to a byte stream", async () => { await Promise.all([done, serving]); }); -test("stream> yields chunks of promises", async () => { - const ty = t.stream(t.future(t.string)); - const { result, done, serving } = await echo(ty, [["a", "b"], ["c"]]); - const out = []; - for await (const chunk of result) out.push(...(await Promise.all(chunk))); - assert.deepEqual(out, ["a", "b", "c"]); - await Promise.all([done, serving]); +for (const [kind, ty, chunk] of [ + ["list", t.list(t.future(t.u8)), Uint8Array.of(1, 0)], + ["stream", t.stream(t.future(t.u8)), Uint8Array.of(0)], +]) { + test(`pending values in ${kind} elements are not supported`, { timeout: 1000 }, async () => { + const { inbound, peer, transport, aborted } = feed(); + await peer.outgoing([]).write(chunk); + await peer.outgoing([0]).write(Uint8Array.of(1, 0)); + inbound.close(); + const invoked = invoke(transport, STREAMS, "get", [], [], [ty]); + if (kind === "stream") { + const { results, done } = await invoked; + await collect(results[0]); + await assert.rejects(done, /not supported/); + } else await assert.rejects(invoked, /not supported/); + assert.match(String(await aborted), /not supported/); + }); +} + +test("ready values in stream elements are received", { timeout: 1000 }, async () => { + const { inbound, peer, transport } = feed(); + await peer.outgoing([]).write(Uint8Array.of(0)); + await peer.outgoing([0]).write(Uint8Array.of(2, 1, 5, 1, 6)); + await peer.outgoing([0]).write(Uint8Array.of(0)); + inbound.close(); + const ty = [t.stream(t.future(t.u8))]; + const { results, done } = await invoke(transport, STREAMS, "get", [], [], ty); + assert.deepEqual(await Promise.all(await collect(results[0])), [5, 6]); + await done; }); -test("stream> yields chunks of byte streams", async () => { - const ty = t.stream(t.stream(t.u8)); - // one outer chunk carrying two inner byte streams - const { result, done, serving } = await echo(ty, [ - [new Uint8Array([1, 2]), new Uint8Array([3, 4])], - ]); - const out = []; - for await (const chunk of result) { - for (const inner of chunk) out.push(await collect(inner)); +for (const [name, frame] of [ + ["data", (out) => out.write(Uint8Array.of(7))], + ["an empty frame", (out) => out.close()], +]) { + test( + `${name} for a sub-stream the reply does not carry fails it`, + { timeout: 1000 }, + async () => { + const { inbound, peer, transport, aborted } = feed(); + await peer.outgoing([]).write(Uint8Array.of(0, 0)); + await frame(peer.outgoing([0])); + await peer.outgoing([1]).write(Uint8Array.of(1, 7, 0)); + inbound.close(); + const ty = [t.option(t.stream(t.u8)), t.stream(t.u8)]; + const { results, done } = await invoke(transport, STREAMS, "get", [], [], ty); + await collect(results[1]); + await assert.rejects(done, /unexpected data for sub-stream \[0\]/); + assert.match(String(await aborted), /unexpected data/); + }, + ); + + for (const [declared, path, ty, init] of [ + [true, [0], t.stream(t.u8), [[[0], Uint8Array.of(1, 7, 0)]]], + [false, [0, 0], t.future(t.tuple(t.future(t.u8))), []], + ]) { + test( + `${name} for a closed sub-stream ${declared ? "" : "not yet declared "}fails it`, + { timeout: 1000 }, + async () => { + const { inbound, peer, transport, aborted } = feed(); + await peer.outgoing([]).write(Uint8Array.of(0)); + for (const [p, bytes] of init) await peer.outgoing(p).write(bytes); + await peer.outgoing(path).close(); + await frame(peer.outgoing(path)); + if (!declared) await peer.outgoing([0]).write(Uint8Array.of(0)); + inbound.close(); + await invoke(transport, STREAMS, "get", [], [], [ty]); + assert.match( + String(await aborted), + new RegExp(`unexpected data for sub-stream \\[${path}\\]`), + ); + }, + ); } - assert.deepEqual(out, [ - [1, 2], - [3, 4], - ]); - await Promise.all([done, serving]); -}); +} diff --git a/js/test/value.test.js b/js/test/value.test.js index 68c6b2453..8228f9ecb 100644 --- a/js/test/value.test.js +++ b/js/test/value.test.js @@ -264,7 +264,7 @@ test("stream and future cannot be encoded or decoded synchronously", () => { assert.throws(() => decode(t.future(t.string), new Uint8Array([0])), /async session API/); }); -test("ready streams are decoded synchronously", async () => { +test("ready streams and futures are decoded synchronously", async () => { const collect = async (chunks) => { const out = []; for await (const chunk of chunks) out.push(chunk); @@ -277,6 +277,8 @@ test("ready streams are decoded synchronously", async () => { ["a"], ]); assert.deepEqual(await collect(decode(t.stream(t.u8), Uint8Array.of(1, 0))), []); + assert.equal(await decode(t.future(t.u8), Uint8Array.of(1, 7)), 7); + assert.equal(await decode(t.future(), Uint8Array.of(1)), undefined); }); test("the wasi:keyvalue store types", () => { diff --git a/tests/go/async.go b/tests/go/async.go index 46a494afc..a4801dcee 100644 --- a/tests/go/async.go +++ b/tests/go/async.go @@ -3,34 +3,30 @@ package integration import ( - "bytes" "context" "errors" - "io" "log/slog" - wrpc "wrpc.io/go" "wrpc.io/tests/go/bindings/async_server/exports/wrpc_test/integration/async" + "wrpc.io/tests/go/internal" ) type AsyncHandler struct{} -func (AsyncHandler) WithStreams(ctx context.Context) (io.ReadCloser, wrpc.Receiver[[][]string], error) { +func (AsyncHandler) WithStreams(ctx context.Context) (<-chan []byte, <-chan [][]string, error) { slog.DebugContext(ctx, "handling `with-streams`") - buf := io.NopCloser(bytes.NewBuffer([]byte("test"))) - str := wrpc.NewCompleteReceiver([][]string{{"foo"}, {"bar", "baz"}}) - return buf, str, nil + return internal.Ready([]byte("test")), internal.Ready([][]string{{"foo"}, {"bar", "baz"}}), nil } -func (AsyncHandler) WithFuture(ctx context.Context, x *async.Something, s io.ReadCloser) (wrpc.Receiver[io.ReadCloser], error) { +func (AsyncHandler) WithFuture(ctx context.Context, x *async.Something, s <-chan []byte) (<-chan <-chan []byte, error) { slog.DebugContext(ctx, "handling `with-future`", "x", x) if x.Foo != "bar" { return nil, errors.New("`foo` is not `bar`") } - return wrpc.NewCompleteReceiver(s), nil + return internal.Ready(s), nil } -func (AsyncHandler) IdentityNestedAsync(ctx context.Context, v wrpc.Receiver[wrpc.Receiver[wrpc.Receiver[wrpc.Receiver[[]string]]]]) (wrpc.Receiver[wrpc.Receiver[wrpc.Receiver[wrpc.Receiver[[]string]]]], error) { +func (AsyncHandler) IdentityNestedAsync(ctx context.Context, v <-chan <-chan <-chan <-chan []string) (<-chan <-chan <-chan <-chan []string, error) { slog.DebugContext(ctx, "handling `identity-nested-async`") return v, nil } diff --git a/tests/go/async_test.go b/tests/go/async_test.go index 2894755bc..aa3a21d45 100644 --- a/tests/go/async_test.go +++ b/tests/go/async_test.go @@ -4,38 +4,18 @@ package integration_test import ( "context" - "io" "log/slog" - "reflect" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - wrpc "wrpc.io/go" integration "wrpc.io/tests/go" "wrpc.io/tests/go/bindings/async_client/wrpc_test/integration/async" "wrpc.io/tests/go/bindings/async_server" "wrpc.io/tests/go/internal" ) -type chunkReceiver[T any] struct { - chunks [][]T -} - -func (r *chunkReceiver[T]) Receive() ([]T, error) { - if len(r.chunks) == 0 { - return nil, io.EOF - } - var v []T - v, r.chunks = r.chunks[0], r.chunks[1:] - return v, nil -} - -func (r *chunkReceiver[T]) Close() error { - return nil -} - func TestAsync(t *testing.T) { var cancel func() ctx := context.Background() @@ -58,87 +38,67 @@ func TestAsync(t *testing.T) { t.Run("with-streams", func(t *testing.T) { slog.DebugContext(ctx, "calling `wrpc-test:integration/async.with-streams`") - byteRx, stringListRx, err := async.WithStreams(ctx, client) - if err != nil { - t.Errorf("failed to call `wrpc-test:integration/async.with-streams`: %s", err) - return - } - b, err := io.ReadAll(byteRx) - if err != nil { - t.Errorf("failed to read from stream: %s", err) - return + byteRx, stringListRx, errCh, err := async.WithStreams(ctx, client) + require.NoError(t, err, "failed to call `wrpc-test:integration/async.with-streams`") + var b []byte + for chunk := range byteRx { + b = append(b, chunk...) } - if string(b) != "test" { - t.Errorf("expected: `test`, got: %s", string(b)) - return + assert.Equal(t, "test", string(b)) + var ss [][]string + for chunk := range stringListRx { + ss = append(ss, chunk...) } - if err := byteRx.Close(); err != nil { - t.Errorf("failed to close byte reader: %s", err) - return + assert.Equal(t, [][]string{{"foo"}, {"bar", "baz"}}, ss) + for err := range errCh { + require.NoError(t, err, "failed to receive async value from peer") } + }) - ss, err := stringListRx.Receive() - if err != nil { - t.Errorf("failed to receive ready list stream: %s", err) - return - } - expected := [][]string{{"foo"}, {"bar", "baz"}} - if !reflect.DeepEqual(ss, expected) { - t.Errorf("expected: `%#v`, got: %#v", expected, ss) - return + t.Run("with-streams-cancel", func(t *testing.T) { + callCtx, callCancel := context.WithCancel(ctx) + _, stringListRx, errCh, err := async.WithStreams(callCtx, client) + require.NoError(t, err, "failed to call `wrpc-test:integration/async.with-streams`") + for range stringListRx { } - ss, err = stringListRx.Receive() - if ss != nil || err != io.EOF { - t.Errorf("ready list should have returned (nil, io.EOF), got: (%#v, %v)", ss, err) - return - } - if err := stringListRx.Close(); err != nil { - t.Errorf("failed to close string list receiver: %s", err) - return + callCancel() + timeout := time.After(5 * time.Second) + for { + select { + case _, ok := <-errCh: + if !ok { + return + } + case <-timeout: + t.Fatal("error channel not closed after cancellation") + } } }) t.Run("identity-nested-async", func(t *testing.T) { slog.DebugContext(ctx, "calling `wrpc-test:integration/async.identity-nested-async`") + stream := make(chan []string, 3) + stream <- []string{"foo"} + stream <- []string{} + stream <- []string{"bar", "baz"} + close(stream) fut0, errCh, err := async.IdentityNestedAsync(ctx, client, - wrpc.NewNestedReceiver(wrpc.NewCompleteReceiver( - wrpc.NewCompleteReceiver( - wrpc.NewNestedReceiver(wrpc.NewCompleteReceiver( - &chunkReceiver[string]{[][]string{{"foo"}, {}, {"bar", "baz"}}}, - )), - ), - )), + internal.Ready(internal.Ready(internal.Ready((<-chan []string)(stream)))), ) - if err != nil { - t.Errorf("failed to call `wrpc-test:integration/async.identity-nested-async`: %s", err) - return - } - for err := range errCh { - t.Errorf("failed to send async value to peer: %s", err) - return - } - fut1, err := fut0.Receive() - require.NoError(t, err, "failed to receive future 0") - fut2, err := fut1.Receive() - require.NoError(t, err, "failed to receive future 1") - - stream, err := fut2.Receive() - require.NoError(t, err, "failed to receive future 2") - + require.NoError(t, err, "failed to call `wrpc-test:integration/async.identity-nested-async`") + fut1, ok := <-fut0 + require.True(t, ok, "failed to receive future 0") + fut2, ok := <-fut1 + require.True(t, ok, "failed to receive future 1") + rx, ok := <-fut2 + require.True(t, ok, "failed to receive future 2") var ss []string - for err != io.EOF { - var v []string - v, err = stream.Receive() - if err != io.EOF { - require.NoError(t, err, "failed to receive stream element") - } - ss = append(ss, v...) + for chunk := range rx { + ss = append(ss, chunk...) } - assert.Equal(t, ss, []string{"foo", "bar", "baz"}) - - if err := fut0.Close(); err != nil { - t.Errorf("failed to close async value receiver: %s", err) - return + assert.Equal(t, []string{"foo", "bar", "baz"}, ss) + for err := range errCh { + require.NoError(t, err, "failed to exchange async values with peer") } }) diff --git a/tests/go/internal/internal.go b/tests/go/internal/internal.go index 957fa86b5..d01250941 100644 --- a/tests/go/internal/internal.go +++ b/tests/go/internal/internal.go @@ -64,3 +64,11 @@ func Accept(ctx context.Context, srv *wrpctcp.Server) { } }() } + +// Ready returns a closed channel, which contains `v` +func Ready[T any](v T) <-chan T { + ch := make(chan T, 1) + ch <- v + close(ch) + return ch +}