Skip to main content

ruststream/runtime/handle/
axis.rs

1//! The input axis of the [`Handle`](super::Handle) trait: what the `In` parameter may be, and
2//! what each spelling means to the mount machinery.
3//!
4//! The form rule is uniform - `&T` is one message, `&[T]` a batch of them - and the lane is the
5//! type's own business: a `serde` type rides the codec, a [`Deserialized`] type constructs
6//! itself from the payload bytes, a [`Message<H, P>`](Message) pair decodes its typed header
7//! contract in the same stage, and a type on the carried lane is lent by the delivery that holds
8//! it ([`Carries`](crate::Carries)). The spellings:
9//!
10//! | `In` | delivery | body parameter |
11//! |---|---|---|
12//! | `T` | one decoded message | `&T` |
13//! | `Message<H, P>` | one decoded message + typed headers | `&Message<H, P>` |
14//! | `F<'_>` where `F` is [`Deserialized`] | one payload, self-constructed | `&F<'_>` |
15//! | `T` on the carried lane | the value one delivery holds | `&T` |
16//! | `[T]` | a batch of decoded messages | `&[T]` |
17//! | `[Message<H, P>]` | a batch with typed headers per element | `&[Message<H, P>]` |
18//! | `[F<'_>]` where `F` is [`Deserialized`] | a batch of self-constructed payloads | `&[F<'_>]` |
19//! | `[T]` on the carried lane | the values one batch holds, as one slice | `&[T]` |
20//!
21//! Every projection the machinery needs (the decode kind, the verdict family, the schema of the
22//! generated document) hangs off the lifetime-free [`Axis`] marker, so definitions can carry the
23//! axis without carrying a borrow.
24
25use serde::de::DeserializeOwned;
26
27use crate::runtime::input::{Carried, Decoded, DecodedPair, InputKind, Provided};
28
29use super::docs::DocState;
30use super::verdict::{Batched, OneByOne, VerdictFamily};
31
32/// One incoming message together with its decoded typed header contract.
33///
34/// As a [`Handle`](super::Handle) input (`Message<OrderHeaders, Order>`), the core decodes the
35/// payload and the headers in the same stage, under the same
36/// [`on_failure(decode = ..)`](crate::runtime::FailurePolicies) policy as the payload alone, and
37/// the body receives ready values. The pairing is structural: each consumer declares the
38/// contract it reads, so two subscribers of one subject may read different header sets.
39///
40/// As a reply type (`Handle<In, Message<EventHeaders, Confirmation>>`), the reply's headers ride
41/// the value: the runtime serializes the contract into the outgoing headers and the payload
42/// through the reply codec.
43///
44/// # Examples
45///
46/// ```
47/// # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
48/// # mod demo {
49/// use ruststream::memory::prelude::*;
50/// use serde::Deserialize;
51///
52/// #[derive(Deserialize, schemars::JsonSchema)]
53/// struct OrderHeaders {
54///     tenant: String,
55/// }
56///
57/// #[derive(Deserialize, schemars::JsonSchema)]
58/// struct Order {
59///     id: u64,
60/// }
61///
62/// struct Bill;
63///
64/// /// The payload and its header contract arrive decoded together.
65/// impl Handle<Message<OrderHeaders, Order>> for Bill {
66///     async fn handle(
67///         &self,
68///         msg: &Message<OrderHeaders, Order>,
69///         _outs: &(),
70///         _ctx: &mut Context<'_>,
71///     ) -> Result<(), HandlerOutcome> {
72///         tracing::info!(order = msg.body.id, tenant = msg.headers.tenant, "billed");
73///         Ok(())
74///     }
75/// }
76///
77/// fn app() -> RustStream {
78///     RustStream::new(AppInfo::new("billing", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
79///         b.include(subscriber("orders", Bill).build());
80///     })
81/// }
82/// # }
83/// # fn main() {}
84/// ```
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86pub struct Message<H, P> {
87    /// The decoded header contract.
88    pub headers: H,
89    /// The decoded payload.
90    pub body: P,
91}
92
93impl<H, P> Message<H, P> {
94    /// Pairs a header contract with a payload.
95    #[must_use]
96    pub const fn new(headers: H, body: P) -> Self {
97        Self { headers, body }
98    }
99}
100
101/// A type that deserializes itself from the payload bytes: `Deserialize` means the framework's
102/// codec does it, `Deserialized` means it is already done by the user's own type.
103///
104/// The core hands [`from_payload`](Self::from_payload) the delivery's bytes exactly as they
105/// arrived - no codec runs, no decode policy of a codec applies, and nothing is copied: the
106/// output borrows the broker's buffer. A failed construction is settled by the subscriber's
107/// [`on_failure(decode = ..)`](crate::runtime::FailurePolicies) policy, the same rung a codec
108/// decode failure lands on. This is the lane a zero-copy wire format (flatbuffers, capnp, a
109/// hand-rolled frame) rides without pretending to be `serde`.
110///
111/// # Implementing by hand
112///
113/// `#[derive(Deserialized)]` (under the `macros` feature) covers a newtype or single-field
114/// struct over `&'a [u8]`. Any other shape is a pair of short impls: the construction, and the
115/// [`Input`] spelling that routes the type onto the self-deserializing lane. The batch spelling
116/// comes for free - `&[Frame<'_>]` bodies mount off the same two impls:
117///
118/// ```
119/// use ruststream::prelude::*;
120///
121/// struct Frame<'a>(&'a [u8]);
122///
123/// impl Deserialized for Frame<'_> {
124///     type Output<'a> = Frame<'a>;
125///     type Error = core::convert::Infallible;
126///
127///     fn from_payload(payload: &[u8]) -> Result<Frame<'_>, Self::Error> {
128///         Ok(Frame(payload))
129///     }
130/// }
131///
132/// impl Input for Frame<'_> {
133///     type Axis = SoloDeserialized<Frame<'static>>;
134/// }
135/// # fn check() -> Result<(), Box<dyn std::error::Error>> {
136/// let frame = Frame::from_payload(b"bytes")?;
137/// assert_eq!(frame.0, b"bytes");
138/// # Ok(())
139/// # }
140/// # check().unwrap();
141/// ```
142pub trait Deserialized: Sized {
143    /// This type at the payload's lifetime (`Frame<'a>` for a borrowing `Frame`, `Self` for an
144    /// owning one).
145    type Output<'a>: Send + Sync;
146
147    /// The construction failure; `Infallible` for a plain view over the bytes.
148    type Error: std::fmt::Display;
149
150    /// Constructs the value from one delivery's payload, borrowing it.
151    ///
152    /// # Errors
153    ///
154    /// A constructor that validates (a flatbuffers verifier, a length check) reports the bad
155    /// payload here; the subscriber's decode failure policy settles the delivery.
156    fn from_payload(payload: &[u8]) -> Result<Self::Output<'_>, Self::Error>;
157}
158
159/// What a `#[wire(decode = ..)]` function returns, with its error erased so the derive need not
160/// name a type it cannot see. Machinery behind the derive.
161///
162/// Only the fallible shape is admitted, which every self-describing format's reader has anyway:
163/// a bare `T` and `Result<T, E>` overlap the moment the decoded type is itself a result, so the
164/// two cannot both be accepted.
165#[doc(hidden)]
166pub trait DecodeOutcome<T> {
167    /// Normalizes the outcome into an erased construction failure.
168    ///
169    /// # Errors
170    ///
171    /// Returns the format's own error, boxed.
172    fn finish(self) -> Result<T, Box<dyn std::error::Error + Send + Sync>>;
173}
174
175impl<T, E: std::error::Error + Send + Sync + 'static> DecodeOutcome<T> for Result<T, E> {
176    fn finish(self) -> Result<T, Box<dyn std::error::Error + Send + Sync>> {
177        self.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
178    }
179}
180
181/// A [`Handle`](super::Handle) input spelling: a decoded `T`, a [`Deserialized`] type, a
182/// [`Message<H, P>`](Message) pair, a value the delivery already holds
183/// ([`Carries`](crate::Carries)), or a batch (slice) of any of them.
184#[diagnostic::on_unimplemented(
185    message = "`{Self}` is not a handler input",
186    note = "a body's input is `&T` (T: DeserializeOwned), `&F<'_>` (F: Deserialized - derive it \
187            for a raw-payload type), `&Message<H, P>`, a value a broker's deliveries carry (the \
188            broker crate's derive puts its type on that lane), or a batch: `&[T]`, `&[F<'_>]`, \
189            `&[Message<H, P>]`"
190)]
191pub trait Input {
192    /// The lifetime-free marker every machinery projection hangs off.
193    type Axis: Axis;
194}
195
196/// The lifetime-free projection of one [`Input`] spelling. Machinery; never named in user code.
197#[doc(hidden)]
198pub trait Axis: Send + Sync + 'static {
199    /// The verdict family ([`OneByOne`] / [`Batched`]).
200    type Family: VerdictFamily;
201
202    /// The element's decode kind (for a batch, the kind of one element).
203    type Kind: InputKind;
204
205    /// The mount form of the axis's plain (no reply, no injections) definition.
206    type EagerForm;
207
208    /// The mount form of the axis's slot-carrying (no reply) definition.
209    type SlotForm;
210
211    /// The serialized payload schema under the documentation state `Doc`.
212    #[must_use]
213    fn payload_schema<Doc: AxisDocs<Self>>() -> Option<String> {
214        Doc::payload_schema()
215    }
216}
217
218/// What one documentation state reports for one axis: the payload schema, and the headers
219/// schema for a pair axis. Machinery behind documented-by-default; the `Doc` obligations
220/// ([`Documentable`](super::Documentable) on the message types) live in these impls.
221#[doc(hidden)]
222pub trait AxisDocs<A: Axis + ?Sized> {
223    /// The serialized JSON Schema of the payload type.
224    fn payload_schema() -> Option<String>;
225
226    /// The serialized JSON Schema of the typed header contract, for a pair axis.
227    #[must_use]
228    fn headers_schema() -> Option<String> {
229        None
230    }
231}
232
233/// The single decoded input: `In = T`.
234#[derive(Debug)]
235pub struct Solo<T>(core::marker::PhantomData<T>);
236
237/// The single self-deserializing input: `In = F<'_>` for a [`Deserialized`] `F`.
238///
239/// The parameter is the family's lifetime-free representative (`Frame<'static>`), which every
240/// lifetime of the input projects to.
241#[derive(Debug)]
242pub struct SoloDeserialized<F>(core::marker::PhantomData<F>);
243
244/// The single pair input: `In = Message<H, P>`.
245#[derive(Debug)]
246pub struct SoloPair<H, P>(core::marker::PhantomData<(H, P)>);
247
248/// The single carried input: `In = T` for a `T` the subscription's deliveries lend
249/// ([`Carries<T>`](crate::Carries)). A broker crate's derive selects it for the type, with
250/// `impl Input for Row { type Axis = SoloCarried<Row>; }`.
251///
252/// # Examples
253///
254/// A handler of one carried value and one of a batch, over a broker crate whose derive puts `SendEmail` on this lane:
255///
256/// ```
257/// # #[cfg(all(feature = "macros", feature = "memory"))]
258/// # mod demo {
259/// # mod db_queue {
260/// #     // The broker crate's side: its broker, its descriptor, its deliveries and pages of rows.
261/// #     use std::num::NonZeroUsize;
262/// #     use futures::Stream;
263/// #     use ruststream::memory::{ConnectedMemoryBroker, MemoryError};
264/// #     use ruststream::runtime::{Input, SoloCarried};
265/// #     use ruststream::{AckError, AddressedCopies, BatchSubscriber, Carries, CarriesBatch};
266/// #     use ruststream::{HeaderMap, IncomingMessage, RedeliveryAddress, RedeliveryAddressed};
267/// #     use ruststream::{Subscriber, SubscriptionSource};
268/// #     use super::SendEmail;
269/// #     pub mod prelude {
270/// #         pub use ruststream::prelude::*;
271/// #         pub use super::{QueueBroker, QueueTable};
272/// #     }
273/// #     pub type QueueBroker = ruststream::memory::MemoryBroker;
274/// #     #[derive(Clone)]
275/// #     pub struct QueueTable(&'static str);
276/// #     impl QueueTable { pub fn new(table: &'static str) -> Self { Self(table) } }
277/// #     pub struct Claims;
278/// #     pub struct Claimed { bytes: Vec<u8>, headers: HeaderMap, row: Option<SendEmail> }
279/// #     pub struct Page { rows: Vec<SendEmail>, claims: Vec<Claimed> }
280/// #     impl SubscriptionSource<ConnectedMemoryBroker> for QueueTable {
281/// #         type Subscriber = Claims;
282/// #         type Copies = AddressedCopies;
283/// #         fn name(&self) -> &str { self.0 }
284/// #         async fn subscribe(self, _: &ConnectedMemoryBroker) -> Result<Claims, MemoryError> {
285/// #             Ok(Claims)
286/// #         }
287/// #     }
288/// #     impl RedeliveryAddressed<ConnectedMemoryBroker> for QueueTable {
289/// #         async fn redelivery_address(&self, _: &ConnectedMemoryBroker)
290/// #             -> Result<RedeliveryAddress, MemoryError> {
291/// #             Ok(RedeliveryAddress::new(self.0))
292/// #         }
293/// #     }
294/// #     impl Subscriber for Claims {
295/// #         type Message = Claimed;
296/// #         type Error = MemoryError;
297/// #         fn stream(&mut self) -> impl Stream<Item = Result<Claimed, MemoryError>> + Send + '_ {
298/// #             futures::stream::empty()
299/// #         }
300/// #     }
301/// #     impl BatchSubscriber for Claims {
302/// #         type Batch = Page;
303/// #         fn batches(&mut self, _: NonZeroUsize)
304/// #             -> impl Stream<Item = Result<Page, MemoryError>> + Send + '_ {
305/// #             futures::stream::empty()
306/// #         }
307/// #     }
308/// #     impl Carries<SendEmail> for Claimed {
309/// #         fn carried(&self) -> Option<&SendEmail> { self.row.as_ref() }
310/// #     }
311/// #     impl CarriesBatch<SendEmail> for Page {
312/// #         fn carried(&self) -> &[SendEmail] { &self.rows }
313/// #     }
314/// #     impl IntoIterator for Page {
315/// #         type Item = Claimed;
316/// #         type IntoIter = std::vec::IntoIter<Claimed>;
317/// #         fn into_iter(self) -> Self::IntoIter { self.claims.into_iter() }
318/// #     }
319/// #     impl IncomingMessage for Claimed {
320/// #         fn payload(&self) -> &[u8] { &self.bytes }
321/// #         fn headers(&self) -> &HeaderMap { &self.headers }
322/// #         async fn ack(self) -> Result<(), AckError> { Ok(()) }
323/// #         async fn nack(self, _: bool) -> Result<(), AckError> { Ok(()) }
324/// #     }
325/// #     // What the broker crate's derive writes.
326/// #     impl Input for SendEmail { type Axis = SoloCarried<Self>; }
327/// # }
328/// use db_queue::prelude::*;
329///
330/// /// One row of the queue table, as the database driver reads it.
331/// // The broker crate's derive, beside `Clone`, puts the type on this lane.
332/// #[derive(Clone)]
333/// struct SendEmail {
334///     to: String,
335/// }
336///
337/// #[subscriber(QueueTable::new("email_jobs"))]
338/// async fn send(email: &SendEmail) -> HandlerOutcome {
339///     tracing::info!(to = %email.to, "sent");
340///     HandlerOutcome::ack()
341/// }
342///
343/// /// One SMTP session per page of rows.
344/// #[subscriber(QueueTable::new("newsletter_jobs"))]
345/// async fn send_newsletter(emails: &[SendEmail]) -> HandlerOutcome {
346///     tracing::info!(count = emails.len(), "sent a page");
347///     HandlerOutcome::ack()
348/// }
349///
350/// fn app() -> RustStream {
351///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(QueueBroker::new(), |b| {
352///         b.include(send);
353///         b.include(send_newsletter.batch(nonzero!(64)));
354///     })
355/// }
356/// # }
357/// # fn main() {}
358/// ```
359#[derive(Debug)]
360pub struct SoloCarried<T>(core::marker::PhantomData<T>);
361
362/// The decoded batch input: `In = [T]`.
363#[derive(Debug)]
364pub struct Batch<T>(core::marker::PhantomData<T>);
365
366/// The self-deserializing batch input: `In = [F<'_>]` for a [`Deserialized`] `F`. See
367/// [`SoloDeserialized`] for the parameter.
368#[derive(Debug)]
369pub struct BatchDeserialized<F>(core::marker::PhantomData<F>);
370
371/// The pair batch input: `In = [Message<H, P>]`.
372#[derive(Debug)]
373pub struct BatchPair<H, P>(core::marker::PhantomData<(H, P)>);
374
375/// The carried batch input: `In = [T]` for a `T` the subscription's batches lend as one slice
376/// ([`CarriesBatch<T>`](crate::CarriesBatch)). See [`SoloCarried`].
377#[derive(Debug)]
378pub struct BatchCarried<T>(core::marker::PhantomData<T>);
379
380impl<T: DeserializeOwned + Send + Sync + 'static> Input for T {
381    type Axis = Solo<T>;
382}
383
384impl<H, P> Input for Message<H, P>
385where
386    H: DeserializeOwned + Send + Sync + 'static,
387    P: DeserializeOwned + Send + Sync + 'static,
388{
389    type Axis = SoloPair<H, P>;
390}
391
392/// The batch axis of one single-delivery axis: what `[T]` rides given what `T` rides.
393///
394/// The one slice `Input` impl projects through this, so every single spelling's batch comes for
395/// free - a `Deserialized` type's own `Input` impl (or derive) makes `&[Frame<'_>]` bodies
396/// mountable without a second impl, which the orphan rule would forbid downstream anyway
397/// (`[T]` is nobody's local type). Machinery; never named in user code.
398#[doc(hidden)]
399pub trait BatchedFrom {
400    /// The batch counterpart.
401    type Batch: Axis;
402}
403
404impl<T: Send + Sync + 'static> BatchedFrom for Solo<T> {
405    type Batch = Batch<T>;
406}
407
408impl<F: Send + Sync + 'static> BatchedFrom for SoloDeserialized<F> {
409    type Batch = BatchDeserialized<F>;
410}
411
412impl<H: Send + Sync + 'static, P: Send + Sync + 'static> BatchedFrom for SoloPair<H, P> {
413    type Batch = BatchPair<H, P>;
414}
415
416impl<T: Clone + Send + Sync + 'static> BatchedFrom for SoloCarried<T> {
417    type Batch = BatchCarried<T>;
418}
419
420// No overlap with the decoded blanket above: that one is implicitly `Sized`, a slice is not.
421impl<E> Input for [E]
422where
423    E: Input,
424    E::Axis: BatchedFrom,
425{
426    type Axis = <E::Axis as BatchedFrom>::Batch;
427}
428
429impl<T: Send + Sync + 'static> Axis for Solo<T> {
430    type Family = OneByOne;
431    type Kind = Decoded<T>;
432    type EagerForm = crate::runtime::router::forms::Subscribing;
433    type SlotForm = crate::runtime::router::forms::Out;
434}
435
436impl<F: Send + Sync + 'static> Axis for SoloDeserialized<F> {
437    type Family = OneByOne;
438    type Kind = Provided<F>;
439    type EagerForm = crate::runtime::router::forms::RawSubscribing;
440    type SlotForm = crate::runtime::router::forms::Out;
441}
442
443impl<H: Send + Sync + 'static, P: Send + Sync + 'static> Axis for SoloPair<H, P> {
444    type Family = OneByOne;
445    type Kind = DecodedPair<H, P>;
446    type EagerForm = crate::runtime::router::forms::Subscribing;
447    type SlotForm = crate::runtime::router::forms::Out;
448}
449
450impl<T: Clone + Send + Sync + 'static> Axis for SoloCarried<T> {
451    type Family = OneByOne;
452    type Kind = Carried<T>;
453    type EagerForm = crate::runtime::router::forms::Subscribing;
454    type SlotForm = crate::runtime::router::forms::Out;
455}
456
457impl<T: Send + Sync + 'static> Axis for Batch<T> {
458    type Family = Batched;
459    type Kind = Decoded<T>;
460    type EagerForm = crate::runtime::router::forms::Batch;
461    type SlotForm = crate::runtime::router::forms::BatchOut;
462}
463
464impl<F: Send + Sync + 'static> Axis for BatchDeserialized<F> {
465    type Family = Batched;
466    type Kind = Provided<F>;
467    type EagerForm = crate::runtime::router::forms::RawBatch;
468    type SlotForm = crate::runtime::router::forms::BatchOut;
469}
470
471impl<H: Send + Sync + 'static, P: Send + Sync + 'static> Axis for BatchPair<H, P> {
472    type Family = Batched;
473    type Kind = DecodedPair<H, P>;
474    type EagerForm = crate::runtime::router::forms::Batch;
475    type SlotForm = crate::runtime::router::forms::BatchOut;
476}
477
478impl<T: Clone + Send + Sync + 'static> Axis for BatchCarried<T> {
479    type Family = Batched;
480    type Kind = Carried<T>;
481    type EagerForm = crate::runtime::router::forms::CarriedBatch;
482    type SlotForm = crate::runtime::router::forms::BatchOut;
483}
484
485impl<T: Send + Sync + 'static, Doc: DocState<T>> AxisDocs<Solo<T>> for Doc {
486    fn payload_schema() -> Option<String> {
487        Doc::schema()
488    }
489}
490
491// A self-deserializing payload has no serde model, so no schema is demanded or produced in any
492// documentation state.
493impl<F, Doc> AxisDocs<SoloDeserialized<F>> for Doc
494where
495    F: Send + Sync + 'static,
496{
497    fn payload_schema() -> Option<String> {
498        None
499    }
500}
501
502// A carried value is the service's own type, so it documents itself exactly like a decoded one.
503impl<T: Clone + Send + Sync + 'static, Doc: DocState<T>> AxisDocs<SoloCarried<T>> for Doc {
504    fn payload_schema() -> Option<String> {
505        Doc::schema()
506    }
507}
508
509impl<H, P, Doc> AxisDocs<SoloPair<H, P>> for Doc
510where
511    H: Send + Sync + 'static,
512    P: Send + Sync + 'static,
513    Doc: DocState<P> + DocState<H>,
514{
515    fn payload_schema() -> Option<String> {
516        <Doc as DocState<P>>::schema()
517    }
518
519    fn headers_schema() -> Option<String> {
520        <Doc as DocState<H>>::schema()
521    }
522}
523
524impl<T: Send + Sync + 'static, Doc: DocState<T>> AxisDocs<Batch<T>> for Doc {
525    fn payload_schema() -> Option<String> {
526        Doc::schema()
527    }
528}
529
530impl<F, Doc> AxisDocs<BatchDeserialized<F>> for Doc
531where
532    F: Send + Sync + 'static,
533{
534    fn payload_schema() -> Option<String> {
535        None
536    }
537}
538
539impl<T: Clone + Send + Sync + 'static, Doc: DocState<T>> AxisDocs<BatchCarried<T>> for Doc {
540    fn payload_schema() -> Option<String> {
541        Doc::schema()
542    }
543}
544
545impl<H, P, Doc> AxisDocs<BatchPair<H, P>> for Doc
546where
547    H: Send + Sync + 'static,
548    P: Send + Sync + 'static,
549    Doc: DocState<P> + DocState<H>,
550{
551    fn payload_schema() -> Option<String> {
552        <Doc as DocState<P>>::schema()
553    }
554
555    fn headers_schema() -> Option<String> {
556        <Doc as DocState<H>>::schema()
557    }
558}
559
560/// A single-delivery axis: the plain, raw and pair spellings of one message at a time. The
561/// bound behind the forms that make no sense for a batch (a reply's destination is one message's
562/// business).
563#[doc(hidden)]
564pub trait SoloAxis: Axis<Family = OneByOne> {}
565
566impl<T: Send + Sync + 'static> SoloAxis for Solo<T> {}
567impl<F: Send + Sync + 'static> SoloAxis for SoloDeserialized<F> {}
568impl<H: Send + Sync + 'static, P: Send + Sync + 'static> SoloAxis for SoloPair<H, P> {}
569impl<T: Clone + Send + Sync + 'static> SoloAxis for SoloCarried<T> {}
570
571/// A batch axis: the slice spellings. The bound behind `.batch(..)`.
572#[doc(hidden)]
573pub trait BatchedAxis: Axis<Family = Batched> {}
574
575impl<T: Send + Sync + 'static> BatchedAxis for Batch<T> {}
576impl<F: Send + Sync + 'static> BatchedAxis for BatchDeserialized<F> {}
577impl<H: Send + Sync + 'static, P: Send + Sync + 'static> BatchedAxis for BatchPair<H, P> {}
578impl<T: Clone + Send + Sync + 'static> BatchedAxis for BatchCarried<T> {}