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> {}