···5757- `AtUri::from_parts()` constructor for building URIs from components
5858- Proper Display and FromStr implementations
59596060-**Memory-based credential session** (`jacquard`)
6161-- `MemoryCredentialSession` for in-memory session storage
6262-- Useful for short-lived applications or testing
6363-- No file I/O required
6464-6565-**Collection record fetching improvements** (`jacquard-api`, `jacquard-lexicon`)
6666-- Generated `fetch_record()` convenience method on all record types
6767-- Fetches owned record without turbofish syntax: `Post::fetch_record(agent, uri).await`
6868-- Simplifies common pattern of fetching + converting to owned
6060+**Memory-based credential session helpers** (`jacquard`) (ty [@vielle.dev](https://tangled.org/@vielle.dev))
69617062**Axum improvements** (`jacquard-axum`)
7163- `XrpcError` now implements `IntoResponse` for better error handling
···7668- `subscribe_repos.rs`: Subscribe to PDS firehose with typed DAG-CBOR messages
7769- `subscribe_jetstream.rs`: Subscribe to Jetstream with typed JSON messages and optional compression
7870- `stream_get_blob.rs`: Download blobs using HTTP streaming
7979-- `app_password_example.rs`: App password authentication example
7171+- `app_password_example.rs`: App password authentication example (ty [@vielle.dev](https://tangled.org/@vielle.dev))
80728173**CID deserialization improvements** (`jacquard-common`)
8274- Fixed `Cid` type to properly deserialize CBOR tag 42 via `IpldCid::deserialize`
···9587- Override `decode_message()` in trait impls to use framed decoding
9688- All record types now have `fetch_uri()` and `fetch_record()` methods generated
97899898-**Dependencies** (`jacquard-axum`)
9090+**Dependencies** (`jacquard-axum`) (ty [@thoth.ptnote.dev](https://tangled.org/@thoth.ptnote.dev))
9991- Disabled default features for `jacquard` dependency to reduce bloat
1009210193### Fixed
10294103103-**Blob upload** (`jacquard`)
9595+**Blob upload** (`jacquard`) (ty [@vielle.dev](https://tangled.org/@vielle.dev) for reporting this one)
10496- Fixed `upload_blob()` authentication issues
10597- Properly authenticates while allowing custom Content-Type headers
10698
+19-39
README.md
···8899It is also designed around zero-copy/borrowed deserialization: types like [`Post<'_>`](https://tangled.org/@nonbinary.computer/jacquard/blob/main/crates/jacquard-api/src/app_bsky/feed/post.rs) can borrow data (via the [`CowStr<'_>`](https://docs.rs/jacquard/latest/jacquard/cowstr/enum.CowStr.html) type and a host of other types built on top of it) directly from the response buffer instead of allocating owned copies. Owned versions are themselves mostly inlined or reference-counted pointers and are therefore still quite efficient. The `IntoStatic` trait (which is derivable) makes it easy to get an owned version and avoid worrying about lifetimes.
10101111+## 0.6.0 Release Highlights:
1212+1313+- **WebSocket streaming** (gated behind feature: "streaming" in `jacquard` and "websocket" in `jacquard-common`)
1414+- Base level HTTP streamed responses and (on non-wasm platforms) request support (gated behind feature: "streaming" in `jacquard-common`)
1515+- **Support for atproto event stream endpoints** (e.g. subscribeRepos, subscribeLabels, firehose)
1616+- **Jetstream subscriber support and implementation**
1717+- **zstd compression support** for JSON websocket endpoints
1818+- **XRPC streaming procedure traits** for endpoints with large payloads, experimental manual implementations in `jacquard`
1919+- Fixed blob upload and download bugs, CID link deserialization issues.
2020+2121+### WARNING
2222+2323+A lot of the streaming code is still pretty experimental. The examples work, though.\
2424+The modules are also less well-documented, and don't have code examples. There are also a lot of utility functions for conveniently working with the streams and transforming them which are lacking. Use [`n0-future`](https://docs.rs/n0-future/latest/n0_future/index.html) to work with them, that is what Jacquard uses internally as much as possible.
2525+2626+### Changelog
2727+2828+[./CHANGELOG.md]
2929+1130## Goals and Features
12311332- Validated, spec-compliant, easy to work with, and performant baseline types
···2039- Lexicon Data value type for working with unknown atproto data (dag-cbor or json)
2140- An order of magnitude less boilerplate than some existing crates
2241- Use as much or as little from the crates as you need
2323-24422543## Example
2644···97115| `jacquard-identity` | Identity resolution | [](https://crates.io/crates/jacquard-identity) [](https://docs.rs/jacquard-identity) |
98116| `jacquard-lexicon` | Lexicon parsing and code generation | [](https://crates.io/crates/jacquard-lexicon) [](https://docs.rs/jacquard-lexicon) |
99117| `jacquard-derive` | Macros for lexicon types | [](https://crates.io/crates/jacquard-derive) [](https://docs.rs/jacquard-derive) |
100100-101101-## Changelog
102102-103103-[CHANGELOG.md](./CHANGELOG.md)
104104-105105-Highlights:
106106-107107-- initial streaming support
108108-- experimental WASM support
109109-- better value type deserialization helpers
110110-- service auth implementation
111111-- XrpcRequest derive Macros
112112-- more builders in generated api to make constructing things easier (lmk if compile time is awful)
113113-- `AgentSessionExt` trait with a host of convenience methods for working with records and preferences
114114-- Improvements to the `Collection` trait, code generation, and addition of the `VecUpdate` trait to enable that
115115-116116-117117-## Experimental WASM Support
118118-119119-Core crates (`jacquard-common`, `jacquard-api`, `jacquard-identity`, `jacquard-oauth`) compile for `wasm32-unknown-unknown`. Traits use [`trait-variant`](https://docs.rs/trait-variant) to conditionally exclude `Send` bounds on WASM targets. DNS-based handle resolution is gated behind the `dns` feature and unavailable on WASM (HTTPS well-known and PDS resolution still work).
120120-121121-Test WASM compilation:
122122-```bash
123123-just check-wasm
124124-# or: cargo build --target wasm32-unknown-unknown -p jacquard-common --no-default-features
125125-```
126126-127127-128128-### Initial Streaming Support
129129-130130-Jacquard is building out support for efficient streaming for large payloads:
131131-132132-- **Blob uploads/downloads**: Stream media without loading into memory
133133-- **CAR file streaming**: Efficient repo sync operations
134134-- **Thin forwarding**: Pipe data between endpoints
135135-- **WebSocket support**: Bidirectional streaming connections
136136-137137-Enable with the `streaming` feature flag. See `jacquard-common` documentation for details.
138118139119## Development
140120
···99use std::path::Path;
1010use std::{marker::PhantomData, pin::Pin};
11111212+/// Trait for streaming XRPC procedures (bidirectional streaming).
1313+///
1414+/// Defines frame encoding/decoding for procedures that send/receive streams of data.
1215pub trait XrpcProcedureStream {
1316 /// The NSID for this XRPC method
1417 const NSID: &'static str;
1518 /// The upload encoding
1619 const ENCODING: &'static str;
17202121+ /// Frame type for this streaming procedure
1822 type Frame<'de>;
19232424+ /// Associated request type
2025 type Request: XrpcRequest;
21262227 /// Response type returned from the XRPC call (marker struct)
2328 type Response: XrpcStreamResp;
24293030+ /// Encode a frame into bytes for transmission.
3131+ ///
3232+ /// Default implementation uses DAG-CBOR encoding.
2533 fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError>
2634 where
2735 Self::Frame<'de>: Serialize,
···5563 /// Response output type
5664 type Frame<'de>: IntoStatic;
57656666+ /// Encode a frame into bytes for transmission.
6767+ ///
6868+ /// Default implementation uses DAG-CBOR encoding.
5869 fn encode_frame<'de>(data: Self::Frame<'de>) -> Result<Bytes, StreamError>
5970 where
6071 Self::Frame<'de>: Serialize,
···7788 }
7889}
79909191+/// A single frame in a streaming XRPC request or response.
9292+///
9393+/// Wraps a buffer of bytes with optional type tagging via the phantom parameter.
8094#[repr(transparent)]
8195pub struct XrpcStreamFrame<F = ()> {
9696+ /// The frame data
8297 pub buffer: Bytes,
8398 _marker: PhantomData<F>,
8499}
8510086101impl XrpcStreamFrame {
102102+ /// Create a new untyped stream frame
87103 pub fn new(buffer: Bytes) -> Self {
88104 Self {
89105 buffer,
···93109}
9411095111impl<F> XrpcStreamFrame<F> {
112112+ /// Create a new typed stream frame
96113 pub fn new_typed<G>(buffer: Bytes) -> Self {
97114 Self {
98115 buffer,
···142159 pub Pin<Box<dyn n0_future::Sink<XrpcStreamFrame<F>, Error = StreamError> + Send>>,
143160);
144161162162+/// Typed streaming XRPC response.
163163+///
164164+/// Similar to `StreamingResponse` but with optional type-level frame tagging.
145165pub struct XrpcResponseStream<F = ()> {
146166 parts: http::response::Parts,
147167 body: Boxed<Result<XrpcStreamFrame<F>, StreamError>>,
148168}
149169150170impl XrpcResponseStream {
171171+ /// Create from a `StreamingResponse`
151172 pub fn from_bytestream(StreamingResponse { parts, body }: StreamingResponse) -> Self {
152173 Self {
153174 parts,
···158179 }
159180 }
160181182182+ /// Create from response parts and a byte stream
161183 pub fn from_parts(parts: http::response::Parts, body: ByteStream) -> Self {
162184 Self {
163185 parts,
···168190 }
169191 }
170192193193+ /// Consume and return parts and body separately
171194 pub fn into_parts(self) -> (http::response::Parts, ByteStream) {
172195 (
173196 self.parts,
···175198 )
176199 }
177200201201+ /// Consume and return just the body stream
178202 pub fn into_bytestream(self) -> ByteStream {
179203 ByteStream::new(self.body.map_ok(|f| f.buffer).boxed())
180204 }
181205}
182206183207impl<F: XrpcStreamResp> XrpcResponseStream<F> {
208208+ /// Create a typed response stream from a `StreamingResponse`
184209 pub fn from_stream(StreamingResponse { parts, body }: StreamingResponse) -> Self {
185210 Self {
186211 parts,
···191216 }
192217 }
193218219219+ /// Create a typed response stream from parts and body
194220 pub fn from_typed_parts(parts: http::response::Parts, body: ByteStream) -> Self {
195221 Self {
196222 parts,
···203229}
204230205231impl<F: XrpcStreamResp + 'static> XrpcResponseStream<F> {
232232+ /// Consume the typed stream and return just the raw byte stream
206233 pub fn into_bytestream(self) -> ByteStream {
207234 ByteStream::new(self.body.map_ok(|f| f.buffer).boxed())
208235 }
+11-1
crates/jacquard-common/src/xrpc/subscription.rs
···9898 }
9999}
100100101101+/// Header for framed DAG-CBOR subscription messages.
102102+///
103103+/// Used in ATProto subscription streams where each message has a CBOR-encoded header
104104+/// followed by the message body.
101105#[derive(Debug, serde::Deserialize)]
102106pub struct EventHeader {
107107+ /// Operation code
103108 pub op: i64,
104104- pub t: smol_str::SmolStr, // type discriminator like "#commit"
109109+ /// Event type discriminator (e.g., "#commit", "#identity")
110110+ pub t: smol_str::SmolStr,
105111}
106112113113+/// Parse a framed DAG-CBOR message header and return the header plus remaining body bytes.
114114+///
115115+/// Used for two-stage deserialization of subscription messages in formats like
116116+/// `com.atproto.sync.subscribeRepos`.
107117pub fn parse_event_header<'a>(bytes: &'a [u8]) -> Result<(EventHeader, &'a [u8]), DecodeError> {
108118 let mut cursor = std::io::Cursor::new(bytes);
109119 let header: EventHeader = ciborium::de::from_reader(&mut cursor)?;
-25
examples/streaming_download.rs
···11-//! Example: Download large file using streaming
22-use jacquard_common::http_client::HttpClientExt;
33-44-#[tokio::main]
55-async fn main() -> Result<(), Box<dyn std::error::Error>> {
66- let client = reqwest::Client::new();
77-88- let request = http::Request::builder()
99- .uri("https://httpbin.org/bytes/1024")
1010- .body(vec![])
1111- .unwrap();
1212-1313- let response = client.send_http_streaming(request).await?;
1414- println!("Status: {}", response.status());
1515- println!("Headers: {:?}", response.headers());
1616-1717- let (_parts, _body) = response.into_parts();
1818- println!("Received streaming response body (ByteStream)");
1919-2020- // Note: To iterate over chunks, use futures_lite::StreamExt on the pinned inner stream:
2121- // let mut stream = Box::pin(body.into_inner());
2222- // while let Some(chunk) = stream.as_mut().try_next().await? { ... }
2323-2424- Ok(())
2525-}
-32
examples/streaming_upload.rs
···11-//! Example: Upload data using streaming request body
22-33-use bytes::Bytes;
44-use futures::stream;
55-use jacquard_common::http_client::HttpClientExt;
66-77-#[tokio::main]
88-async fn main() -> Result<(), Box<dyn std::error::Error>> {
99- let client = reqwest::Client::new();
1010-1111- // Create a stream of data chunks
1212- let chunks = vec![
1313- Bytes::from("Hello, "),
1414- Bytes::from("streaming "),
1515- Bytes::from("world!"),
1616- ];
1717- let body_stream = stream::iter(chunks);
1818-1919- // Build request and split into parts
2020- let request = http::Request::builder()
2121- .method(http::Method::POST)
2222- .uri("https://httpbin.org/post")
2323- .body(())
2424- .unwrap();
2525-2626- let (parts, _) = request.into_parts();
2727-2828- let response = client.send_http_bidirectional(parts, body_stream).await?;
2929- println!("Status: {}", response.status());
3030-3131- Ok(())
3232-}