A better Rust ATProto crate

add streaming integration tests and WASM compatibility check

- Add comprehensive integration tests for streaming HTTP support:
* Test streaming response delivers all bytes correctly
* Test streaming upload sends all chunks
* Test StreamError preserves source information
- Add WASM compatibility check script (scripts/check-wasm.sh)
- Update tangled CI workflow to include WASM build check
- Fix ByteStream to include Unpin bound for trait object
- All tests pass, WASM builds successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Orual 7da32bff fa087a22

Changed files
+91 -5
.tangled
workflows
crates
jacquard-common
scripts
+8
.tangled/workflows/build.yml
··· 14 14 - name: build crate 15 15 command: | 16 16 cargo build 17 + 18 + - name: check wasm compatibility 19 + command: | 20 + rustup target add wasm32-unknown-unknown 21 + cargo build -p jacquard-common \ 22 + --target wasm32-unknown-unknown \ 23 + --features streaming \ 24 + --no-default-features
+2 -2
crates/jacquard-common/examples/streaming_upload.rs
··· 1 1 //! Example: Upload data using streaming request body 2 2 #![cfg(all(feature = "streaming", feature = "reqwest-client"))] 3 3 4 - use jacquard_common::http_client::HttpClientExt; 5 - use futures::stream; 6 4 use bytes::Bytes; 5 + use futures::stream; 6 + use jacquard_common::http_client::HttpClientExt; 7 7 8 8 #[tokio::main] 9 9 async fn main() -> Result<(), Box<dyn std::error::Error>> {
+3 -3
crates/jacquard-common/src/stream.rs
··· 133 133 134 134 /// Platform-agnostic byte stream abstraction 135 135 pub struct ByteStream { 136 - inner: Box<dyn n0_future::Stream<Item = Result<Bytes, StreamError>>>, 136 + inner: Box<dyn n0_future::Stream<Item = Result<Bytes, StreamError>> + Unpin>, 137 137 } 138 138 139 139 impl ByteStream { 140 140 /// Create a new byte stream from any compatible stream 141 141 pub fn new<S>(stream: S) -> Self 142 142 where 143 - S: n0_future::Stream<Item = Result<Bytes, StreamError>> + 'static, 143 + S: n0_future::Stream<Item = Result<Bytes, StreamError>> + Unpin + 'static, 144 144 { 145 145 Self { 146 146 inner: Box::new(stream), ··· 153 153 } 154 154 155 155 /// Convert into the inner boxed stream 156 - pub fn into_inner(self) -> Box<dyn n0_future::Stream<Item = Result<Bytes, StreamError>>> { 156 + pub fn into_inner(self) -> Box<dyn n0_future::Stream<Item = Result<Bytes, StreamError>> + Unpin> { 157 157 self.inner 158 158 } 159 159 }
+66
crates/jacquard-common/tests/streaming_integration.rs
··· 1 + #![cfg(all(feature = "streaming", feature = "reqwest-client", not(target_arch = "wasm32")))] 2 + 3 + use jacquard_common::http_client::HttpClientExt; 4 + use jacquard_common::stream::{StreamError, StreamErrorKind}; 5 + use n0_future::StreamExt; 6 + use bytes::Bytes; 7 + 8 + #[tokio::test] 9 + async fn streaming_response_delivers_all_bytes() { 10 + let client = reqwest::Client::new(); 11 + 12 + let request = http::Request::builder() 13 + .uri("https://httpbin.org/bytes/1024") 14 + .body(vec![]) 15 + .unwrap(); 16 + 17 + let response = client.send_http_streaming(request).await.unwrap(); 18 + assert!(response.status().is_success()); 19 + 20 + let (_parts, body) = response.into_parts(); 21 + let stream = body.into_inner(); 22 + 23 + // Pin the stream for iteration 24 + tokio::pin!(stream); 25 + 26 + let mut total = 0; 27 + 28 + while let Some(result) = stream.next().await { 29 + let chunk = result.unwrap(); 30 + total += chunk.len(); 31 + } 32 + 33 + assert_eq!(total, 1024); 34 + } 35 + 36 + #[tokio::test] 37 + async fn streaming_upload_sends_all_chunks() { 38 + let client = reqwest::Client::new(); 39 + 40 + let chunks = vec![ 41 + Bytes::from("chunk1"), 42 + Bytes::from("chunk2"), 43 + Bytes::from("chunk3"), 44 + ]; 45 + let body_stream = futures::stream::iter(chunks.clone()); 46 + 47 + // Build a complete request and extract parts 48 + let request: http::Request<Vec<u8>> = http::Request::builder() 49 + .method(http::Method::POST) 50 + .uri("https://httpbin.org/post") 51 + .body(vec![]) 52 + .unwrap(); 53 + let (parts, _) = request.into_parts(); 54 + 55 + let response = client.send_http_bidirectional(parts, body_stream).await.unwrap(); 56 + assert!(response.status().is_success()); 57 + } 58 + 59 + #[tokio::test] 60 + async fn stream_error_preserves_source() { 61 + let io_error = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset"); 62 + let stream_error = StreamError::transport(io_error); 63 + 64 + assert_eq!(stream_error.kind(), &StreamErrorKind::Transport); 65 + assert!(stream_error.source().is_some()); 66 + }
+12
scripts/check-wasm.sh
··· 1 + #!/usr/bin/env bash 2 + set -e 3 + 4 + echo "Checking WASM compatibility for streaming support..." 5 + 6 + # Check jacquard-common builds for wasm32-unknown-unknown 7 + cargo build -p jacquard-common \ 8 + --target wasm32-unknown-unknown \ 9 + --features streaming \ 10 + --no-default-features 11 + 12 + echo "✓ WASM build successful"