Skip to main content

ruststream/runtime/handle/
reply.rs

1//! The reply cells of the matrix: a sealed definition whose body declared a reply mounts
2//! through the publishing machinery, with the policy the chain attached (or the broker's
3//! default) committed right at `include`.
4
5use std::any::type_name;
6
7use crate::runtime::batch::BatchResult;
8use crate::runtime::batch_publishing::{BatchPublishingCall, BatchPublishingDef};
9use crate::runtime::context::Context;
10use crate::runtime::handler::HandlerOutcome;
11use crate::runtime::metadata::{OutgoingKind, OutgoingMessageMetadata};
12use crate::runtime::publishing::{PublishingCall, PublishingDef};
13use crate::runtime::router::{IncludeDef, forms};
14use crate::{CallerName, FixedName, Name, OutgoingDestination, Unnamed};
15
16use super::Handle;
17use super::axis::{
18    Axis, AxisDocs, Batch, BatchPair, BatchedAxis, Deserialized, Input, Message, Solo, SoloAxis,
19    SoloCarried, SoloDeserialized, SoloPair,
20};
21use super::docs::DocState;
22use super::eager::construct;
23use super::value::{
24    DeclaredDest, EncodedReply, HandleValue, NamedDest, ReplyValue, ResolvedDest, Sealed,
25    SerializedReply,
26};
27use super::verdict::{Batched, OneByOne};
28// The self-serialized vocabulary lives with the publish builder (the general wire seam serves
29// every typed surface); re-exported here so the reply seam keeps reading as one module.
30pub use crate::runtime::publish::Serialized;
31
32/// The shape and wire of a reply value: what payload it publishes, what typed header contract
33/// rides it, and whether the framework's codec serializes it.
34///
35/// Implemented for every `serde::Serialize` type and every [`Message`] pair of them (the
36/// [`EncodedReply`] wire), and per-type for [`Serialized`] replies (the [`SerializedReply`]
37/// wire) - `#[derive(Serialized)]` writes that impl, or see [`Serialized`] for the hand-written
38/// form.
39#[diagnostic::on_unimplemented(
40    message = "`{Self}` is not a reply value",
41    note = "a reply is a `serde::Serialize` value (the reply codec encodes it), a \
42            `Message<Headers, Payload>` pair of them, or a `#[derive(Serialized)]` type (its \
43            bytes leave as they are)"
44)]
45pub trait ReplyShape: Send + Sync {
46    /// The published payload type (the pair's body, or the reply itself).
47    #[doc(hidden)]
48    type Body: Send + Sync;
49    /// The typed header contract riding the reply (`()` when none does).
50    #[doc(hidden)]
51    type Headers;
52    /// The reply's wire: [`EncodedReply`] or [`SerializedReply`].
53    type Wire;
54}
55
56impl<R: serde::Serialize + Send + Sync> ReplyShape for R {
57    type Body = R;
58    type Headers = ();
59    type Wire = EncodedReply;
60}
61
62impl<H, P> ReplyShape for Message<H, P>
63where
64    H: serde::Serialize + Send + Sync,
65    P: serde::Serialize + Send + Sync,
66{
67    type Body = P;
68    type Headers = H;
69    type Wire = EncodedReply;
70}
71
72/// The reply's headers schema, produced only where headers actually ride the reply (a unit
73/// contract reports nothing rather than the unit type's schema).
74#[doc(hidden)]
75pub trait ReplyHeadersSchema<Doc>: ReplyShape {
76    fn headers_schema() -> Option<String>;
77}
78
79impl<R: serde::Serialize + Send + Sync, Doc> ReplyHeadersSchema<Doc> for R {
80    fn headers_schema() -> Option<String> {
81        None
82    }
83}
84
85impl<H, P, Doc> ReplyHeadersSchema<Doc> for Message<H, P>
86where
87    H: serde::Serialize + Send + Sync,
88    P: serde::Serialize + Send + Sync,
89    Doc: DocState<H>,
90{
91    fn headers_schema() -> Option<String> {
92        Doc::schema()
93    }
94}
95
96/// What one documentation state reports for one reply wire: the encoded wire reports the
97/// reply's schemas, the serialized wire has no serde model to report. Machinery behind the
98/// generated document; keyed by the wire marker so a `Serialized` reply mounts documented
99/// without a `JsonSchema` obligation.
100#[doc(hidden)]
101pub trait WireDocs<R: ReplyShape + ?Sized, Doc> {
102    /// True on the serialized wire: the missing payload schema is by design there.
103    const SERIALIZED: bool;
104
105    /// The serialized JSON Schema of the reply payload.
106    fn payload_schema() -> Option<String>;
107
108    /// The serialized JSON Schema of the typed header contract riding the reply.
109    fn headers_schema() -> Option<String>;
110}
111
112impl<R, Doc> WireDocs<R, Doc> for EncodedReply
113where
114    R: ReplyShape + ReplyHeadersSchema<Doc>,
115    Doc: DocState<R::Body>,
116{
117    const SERIALIZED: bool = false;
118
119    fn payload_schema() -> Option<String> {
120        <Doc as DocState<R::Body>>::schema()
121    }
122
123    fn headers_schema() -> Option<String> {
124        <R as ReplyHeadersSchema<Doc>>::headers_schema()
125    }
126}
127
128impl<R: ReplyShape + ?Sized, Doc> WireDocs<R, Doc> for SerializedReply {
129    const SERIALIZED: bool = true;
130
131    fn payload_schema() -> Option<String> {
132        None
133    }
134
135    fn headers_schema() -> Option<String> {
136        None
137    }
138}
139
140/// Resolves one declared destination form against the mount site's name: a fixed name is the
141/// destination and the mount-site name does not apply, a type declaring none takes it.
142///
143/// [`NameTemplate`](crate::NameTemplate) has no impl on purpose: a reply is published by the
144/// runtime, which has nothing to bind the placeholders with.
145#[doc(hidden)]
146pub trait ResolveDestination<Form> {
147    /// The destination, given the name the mount site supplied.
148    fn resolve(default: &str) -> &str;
149}
150
151impl<R: OutgoingDestination<Form = FixedName>> ResolveDestination<FixedName> for R {
152    fn resolve(_default: &str) -> &str {
153        <R as OutgoingDestination>::DESTINATION
154    }
155}
156
157impl<R: OutgoingDestination<Form = CallerName>> ResolveDestination<CallerName> for R {
158    fn resolve(default: &str) -> &str {
159        default
160    }
161}
162
163/// Where a reply type is published, resolved from its payload's own declaration and the
164/// mount-site name.
165#[doc(hidden)]
166#[diagnostic::on_unimplemented(
167    message = "`{Self}` does not declare where it is published",
168    note = "derive `Outgoing` on the reply type: `#[outgoing(name = \"orders.done\")]` fixes the \
169            destination, and a derive without a name is published where the mount site says \
170            (`reply(\"..\")` on the attribute, `.to(\"..\")` on the chain). A name template has \
171            no placeholders to bind on the reply path."
172)]
173pub trait ReplyDestination {
174    /// The destination, given the name the mount site supplied.
175    fn destination(default: &str) -> &str;
176}
177
178// The two nested obligations are the machinery of the resolution, not the user's mistake: the
179// trait's own note names the derive, so the impl stays out of the error.
180#[diagnostic::do_not_recommend]
181impl<R> ReplyDestination for R
182where
183    R: ReplyShape<Body: OutgoingDestination>,
184    R::Body: ResolveDestination<<R::Body as OutgoingDestination>::Form>,
185{
186    fn destination(default: &str) -> &str {
187        <R::Body as ResolveDestination<<R::Body as OutgoingDestination>::Form>>::resolve(default)
188    }
189}
190
191/// The reply type a definition produces, as a mount chain can read it before the registration
192/// commits.
193///
194/// A definition names its reply type the moment it exists - the handler's return type does not
195/// depend on the policies a chain is still binding - so a step that has to know the reply, like a
196/// `.transform(..)` mounting a transform that names the destination, reads it here. Implemented by
197/// `#[subscriber]` next to the definition and by the value path's sealed reply definition; you
198/// never name it.
199#[doc(hidden)]
200pub trait DeclaresReply {
201    /// The reply type, as [`ReplyShape`] sees it.
202    type Reply;
203
204    /// The broker's typed per-delivery context the handler reads, which is what a transform on
205    /// this reply sees through its [`PublishContext`](crate::runtime::PublishContext).
206    type Context;
207}
208
209impl<A, R, O, C, H, Doc, Dest> DeclaresReply
210    for Sealed<ReplyValue<HandleValue<A, R, O, C, H, Doc>, Dest>>
211{
212    type Reply = R;
213    type Context = C;
214}
215
216/// A reply whose destination a mount chain may name per delivery.
217///
218/// A reply type that declares its own channel (`#[outgoing(name = "..")]`) is published there, and
219/// the generated document says so; letting a transform move it would put the two back out of
220/// step, which is the whole reason the destination lives on the type. So a transform that
221/// declares `Destination = Names` mounts only on a reply type that leaves the destination open -
222/// the mount site's `reply("dest")` name is then the declared fallback, and the transform names
223/// where each answer actually goes.
224#[doc(hidden)]
225#[diagnostic::on_unimplemented(
226    message = "`{Self}` declares where it is published, so a transform cannot name its destination",
227    label = "this reply's destination is its type's own",
228    note = "drop `#[outgoing(name = \"..\")]` from the reply type and name the fallback at the \
229            mount site (`reply(\"dest\")` on the attribute, `.to(\"dest\")` on the chain), or \
230            publish the reply where its declaration says and mount a transform that declares \
231            `Destination = Reads`"
232)]
233pub trait RedirectableReply {}
234
235// The nested obligation is the machinery of the resolution, not the user's mistake: the trait's
236// own note names the fix, so the impl stays out of the error.
237#[diagnostic::do_not_recommend]
238impl<R> RedirectableReply for R where R: ReplyShape<Body: OutgoingDestination<Form = CallerName>> {}
239
240/// The destination of one reply type at the mount-site name, resolved at the expansion site.
241/// A mount-site name the type's own declaration overrides rides along for the startup warning.
242/// Machinery behind the macro expansion; not part of the public API.
243#[doc(hidden)]
244#[must_use]
245pub fn reply_destination<R: ReplyDestination>(default: &'static str) -> ResolvedDest {
246    let name = R::destination(default);
247    // A type declaring no name resolves to the mount-site name itself, so only a declared name
248    // that differs from it tells the two apart.
249    ResolvedDest {
250        name,
251        ignored: (name != default).then_some(default),
252    }
253}
254
255/// The destination of one reply type that declares its own. Machinery behind the macro
256/// expansion; not part of the public API.
257#[doc(hidden)]
258#[must_use]
259pub fn declared_reply_destination<R>() -> ResolvedDest
260where
261    R: ReplyShape<Body: OutgoingDestination<Form = FixedName>>,
262{
263    ResolvedDest {
264        name: <R::Body as OutgoingDestination>::DESTINATION,
265        ignored: None,
266    }
267}
268
269/// Where a wired reply goes: the reply type's own declaration, or the mount-site name where the
270/// type declares none.
271#[doc(hidden)]
272pub trait ReplyDest<R>: Send + Sync {
273    /// The subject the reply publishes to.
274    fn name(&self) -> &str;
275
276    /// The name the mount site wrote when the reply type's own declaration overrode it: the
277    /// startup warning's subject. `None` where the mount site named nothing or the same name.
278    fn ignored(&self) -> Option<&str> {
279        None
280    }
281}
282
283// No obligation on `R`: the destination was resolved where the reply type was named, so a type
284// that declares none is reported at the handler rather than at the chain that mounts it.
285impl<R> ReplyDest<R> for ResolvedDest {
286    fn name(&self) -> &str {
287        self.name
288    }
289
290    fn ignored(&self) -> Option<&str> {
291        self.ignored
292    }
293}
294
295impl<R: ReplyDestination> ReplyDest<R> for NamedDest {
296    fn name(&self) -> &str {
297        R::destination(&self.0)
298    }
299
300    // A type declaring no name resolves to the mount-site name itself, so only a declared name
301    // that differs from it tells the two apart.
302    fn ignored(&self) -> Option<&str> {
303        let site: &str = &self.0;
304        (R::destination(site) != site).then_some(site)
305    }
306}
307
308impl<R> ReplyDest<R> for DeclaredDest
309where
310    R: ReplyShape<Body: OutgoingDestination<Form = FixedName>>,
311{
312    fn name(&self) -> &str {
313        <R::Body as OutgoingDestination>::DESTINATION
314    }
315}
316
317/// The form tokens of one reply wire on one verdict family: the sealed value-path tokens and
318/// the attribute path's builder-producing forms, with and without slots. The serialized wire
319/// has no batch form: a batch's replies publish through the reply codec.
320#[doc(hidden)]
321#[diagnostic::on_unimplemented(
322    message = "this reply's wire does not mount on this input family",
323    note = "an encoded reply (`serde::Serialize`) mounts one-by-one and per batch; a \
324            `Serialized` (raw-byte) reply mounts one-by-one only"
325)]
326pub trait ReplyFormFor<Fam> {
327    /// The mount token of a reply definition.
328    type Form;
329    /// The mount token of a reply definition that also carries slots.
330    type SlotForm;
331}
332
333impl ReplyFormFor<OneByOne> for EncodedReply {
334    type Form = forms::Publishing;
335    type SlotForm = forms::PublishingOut;
336}
337
338impl ReplyFormFor<Batched> for EncodedReply {
339    type Form = forms::BatchPublishing;
340    type SlotForm = forms::BatchPublishingOut;
341}
342
343impl ReplyFormFor<OneByOne> for SerializedReply {
344    type Form = forms::RawReply;
345    type SlotForm = forms::RawReplyOut;
346}
347
348/// The route of one reply type on one verdict family: its wire, and the form tokens that wire
349/// selects. One-by-one the reply type routes itself; per batch the `Vec<Reply>` verdict routes
350/// by its element. Machinery behind `include` and the reply chain; never named in user code.
351#[doc(hidden)]
352pub trait ReplyRoute<Fam> {
353    /// The reply's wire marker.
354    type Wire: ReplyFormFor<Fam>;
355    /// See [`ReplyFormFor::Form`].
356    type Form;
357    /// See [`ReplyFormFor::SlotForm`].
358    type SlotForm;
359}
360
361impl<R> ReplyRoute<OneByOne> for R
362where
363    R: ReplyShape,
364    R::Wire: ReplyFormFor<OneByOne>,
365{
366    type Wire = R::Wire;
367    type Form = <R::Wire as ReplyFormFor<OneByOne>>::Form;
368    type SlotForm = <R::Wire as ReplyFormFor<OneByOne>>::SlotForm;
369}
370
371impl<R> ReplyRoute<Batched> for Vec<R>
372where
373    R: ReplyShape,
374    R::Wire: ReplyFormFor<Batched>,
375{
376    type Wire = R::Wire;
377    type Form = <R::Wire as ReplyFormFor<Batched>>::Form;
378    type SlotForm = <R::Wire as ReplyFormFor<Batched>>::SlotForm;
379}
380
381impl<A, R, C, H, Doc, Dest> IncludeDef
382    for Sealed<ReplyValue<HandleValue<A, R, (), C, H, Doc>, Dest>>
383where
384    A: Axis,
385    R: ReplyRoute<A::Family>,
386{
387    type Form = R::Form;
388}
389impl<A, R, C, H, Doc, Dest> PublishingDef
390    for Sealed<ReplyValue<HandleValue<A, R, (), C, H, Doc>, Dest>>
391where
392    A: SoloAxis,
393    R: ReplyShape<Wire: WireDocs<R, Doc>>,
394    C: Send + Sync,
395    H: Send + Sync,
396    Doc: AxisDocs<A> + Send + Sync,
397    Dest: ReplyDest<R>,
398{
399    type Input = A::Kind;
400    type Injections = ();
401    type Reply = R;
402    type Context = C;
403    // See the eager cells: the settings builder carries the real source.
404    type Source = Unnamed<Name>;
405
406    fn source(&self) -> Unnamed<Name> {
407        Unnamed::new()
408    }
409
410    fn reply_name(&self) -> &str {
411        self.0.dest.name()
412    }
413
414    fn ignored_reply_name(&self) -> Option<&str> {
415        self.0.dest.ignored()
416    }
417
418    fn description(&self) -> Option<&str> {
419        self.0.value.docs.description()
420    }
421
422    fn input_schema(&self) -> Option<String> {
423        self.0
424            .value
425            .docs
426            .input_schema
427            .clone()
428            .or_else(Doc::payload_schema)
429    }
430
431    fn headers_schema(&self) -> Option<String> {
432        self.0
433            .value
434            .docs
435            .headers_schema
436            .clone()
437            .or_else(Doc::headers_schema)
438    }
439
440    fn message_name(&self) -> Option<&'static str> {
441        self.0.value.docs.message_name
442    }
443
444    fn message_description(&self) -> Option<&'static str> {
445        self.0.value.docs.message_description
446    }
447
448    fn outgoing(&self) -> Vec<OutgoingMessageMetadata> {
449        if let Some(declared) = &self.0.value.docs.outgoing {
450            return declared.clone();
451        }
452        vec![
453            OutgoingMessageMetadata::new(self.reply_name().to_owned(), type_name::<R::Body>())
454                .with_payload_schema(<R::Wire as WireDocs<R, Doc>>::payload_schema())
455                .with_headers_schema(<R::Wire as WireDocs<R, Doc>>::headers_schema())
456                .with_serialized(<R::Wire as WireDocs<R, Doc>>::SERIALIZED)
457                .with_kind(OutgoingKind::Answer),
458        ]
459    }
460}
461
462impl<T, R, C, S, H, Doc, Dest> PublishingCall<S>
463    for Sealed<ReplyValue<HandleValue<Solo<T>, R, (), C, H, Doc>, Dest>>
464where
465    Self: PublishingDef<Input = <Solo<T> as Axis>::Kind, Injections = (), Reply = R, Context = C>,
466    T: Input<Axis = Solo<T>> + Send + Sync + 'static,
467    R: ReplyShape,
468    C: Send + Sync,
469    S: Send + Sync,
470    H: Handle<T, R, (), C, S>,
471{
472    async fn call(
473        &self,
474        input: &T,
475        _injections: &(),
476        ctx: &mut Context<'_, C, S>,
477    ) -> Result<R, HandlerOutcome> {
478        self.0.value.body.handle(input, &(), ctx).await
479    }
480}
481
482impl<T, R, C, S, H, Doc, Dest> PublishingCall<S>
483    for Sealed<ReplyValue<HandleValue<SoloCarried<T>, R, (), C, H, Doc>, Dest>>
484where
485    Self: PublishingDef<
486            Input = <SoloCarried<T> as Axis>::Kind,
487            Injections = (),
488            Reply = R,
489            Context = C,
490        >,
491    T: Input<Axis = SoloCarried<T>> + Clone + Send + Sync + 'static,
492    R: ReplyShape,
493    C: Send + Sync,
494    S: Send + Sync,
495    H: Handle<T, R, (), C, S>,
496{
497    async fn call(
498        &self,
499        input: &T,
500        _injections: &(),
501        ctx: &mut Context<'_, C, S>,
502    ) -> Result<R, HandlerOutcome> {
503        self.0.value.body.handle(input, &(), ctx).await
504    }
505}
506
507impl<F, R, C, S, H, Doc, Dest> PublishingCall<S>
508    for Sealed<ReplyValue<HandleValue<SoloDeserialized<F>, R, (), C, H, Doc>, Dest>>
509where
510    Self: PublishingDef<
511            Input = <SoloDeserialized<F> as Axis>::Kind,
512            Injections = (),
513            Reply = R,
514            Context = C,
515        >,
516    F: Deserialized + Send + Sync + 'static,
517    for<'p> F::Output<'p>: Input<Axis = SoloDeserialized<F>>,
518    R: ReplyShape,
519    C: Send + Sync,
520    S: Send + Sync,
521    H: for<'p> Handle<F::Output<'p>, R, (), C, S>,
522{
523    async fn call(
524        &self,
525        input: &[u8],
526        _injections: &(),
527        ctx: &mut Context<'_, C, S>,
528    ) -> Result<R, HandlerOutcome> {
529        let input = construct::<F, C, S>(input, ctx)?;
530        self.0.value.body.handle(&input, &(), ctx).await
531    }
532}
533
534impl<Hd, P, R, C, S, H, Doc, Dest> PublishingCall<S>
535    for Sealed<ReplyValue<HandleValue<SoloPair<Hd, P>, R, (), C, H, Doc>, Dest>>
536where
537    Self: PublishingDef<
538            Input = <SoloPair<Hd, P> as Axis>::Kind,
539            Injections = (),
540            Reply = R,
541            Context = C,
542        >,
543    Message<Hd, P>: Input<Axis = SoloPair<Hd, P>>,
544    Hd: Send + Sync + 'static,
545    P: Send + Sync + 'static,
546    R: ReplyShape,
547    C: Send + Sync,
548    S: Send + Sync,
549    H: Handle<Message<Hd, P>, R, (), C, S>,
550{
551    async fn call(
552        &self,
553        input: &Message<Hd, P>,
554        _injections: &(),
555        ctx: &mut Context<'_, C, S>,
556    ) -> Result<R, HandlerOutcome> {
557        self.0.value.body.handle(input, &(), ctx).await
558    }
559}
560impl<A, R, C, H, Doc, Dest> BatchPublishingDef
561    for Sealed<ReplyValue<HandleValue<A, Vec<R>, (), C, H, Doc>, Dest>>
562where
563    A: BatchedAxis,
564    R: ReplyShape<Wire: WireDocs<R, Doc>>,
565    C: Send + Sync,
566    H: Send + Sync,
567    Doc: AxisDocs<A> + Send + Sync,
568    Dest: ReplyDest<R>,
569{
570    type Input = A::Kind;
571    type Injections = ();
572    type Context = C;
573    type Reply = R;
574    type Source = Unnamed<Name>;
575
576    fn source(&self) -> Unnamed<Name> {
577        Unnamed::new()
578    }
579
580    fn reply_name(&self) -> &str {
581        self.0.dest.name()
582    }
583
584    fn ignored_reply_name(&self) -> Option<&str> {
585        self.0.dest.ignored()
586    }
587
588    fn description(&self) -> Option<&str> {
589        self.0.value.docs.description()
590    }
591
592    fn input_schema(&self) -> Option<String> {
593        self.0
594            .value
595            .docs
596            .input_schema
597            .clone()
598            .or_else(Doc::payload_schema)
599    }
600
601    fn headers_schema(&self) -> Option<String> {
602        self.0
603            .value
604            .docs
605            .headers_schema
606            .clone()
607            .or_else(Doc::headers_schema)
608    }
609
610    fn message_name(&self) -> Option<&'static str> {
611        self.0.value.docs.message_name
612    }
613
614    fn message_description(&self) -> Option<&'static str> {
615        self.0.value.docs.message_description
616    }
617
618    fn outgoing(&self) -> Vec<OutgoingMessageMetadata> {
619        if let Some(declared) = &self.0.value.docs.outgoing {
620            return declared.clone();
621        }
622        vec![
623            OutgoingMessageMetadata::new(self.reply_name().to_owned(), type_name::<R::Body>())
624                .with_payload_schema(<R::Wire as WireDocs<R, Doc>>::payload_schema())
625                .with_headers_schema(<R::Wire as WireDocs<R, Doc>>::headers_schema())
626                .with_serialized(<R::Wire as WireDocs<R, Doc>>::SERIALIZED)
627                .with_kind(OutgoingKind::Answer),
628        ]
629    }
630}
631
632/// Applies the batch reply contract: one reply per element, or one outcome per element.
633pub(super) fn batch_reply_verdict<R>(
634    verdict: Result<Vec<R>, Vec<HandlerOutcome>>,
635    batch_len: usize,
636    subscription: &str,
637) -> Result<Vec<R>, BatchResult> {
638    match verdict {
639        Ok(replies) => {
640            assert!(
641                replies.len() == batch_len,
642                "subscriber '{subscription}' returned {} replies for a batch of {batch_len}",
643                replies.len(),
644            );
645            Ok(replies)
646        }
647        Err(outcomes) => {
648            assert!(
649                outcomes.len() == batch_len,
650                "subscriber '{subscription}' returned {} per-element outcomes for a batch of \
651                 {batch_len}",
652                outcomes.len(),
653            );
654            Err(BatchResult::PerElement(outcomes))
655        }
656    }
657}
658
659impl<T, R, C, S, H, Doc, Dest> BatchPublishingCall<S>
660    for Sealed<ReplyValue<HandleValue<Batch<T>, Vec<R>, (), C, H, Doc>, Dest>>
661where
662    Self: BatchPublishingDef<
663            Input = <Batch<T> as Axis>::Kind,
664            Injections = (),
665            Context = C,
666            Reply = R,
667        >,
668    [T]: Input<Axis = Batch<T>>,
669    T: Send + Sync + 'static,
670    R: ReplyShape,
671    C: Send + Sync,
672    S: Send + Sync,
673    H: Handle<[T], Vec<R>, (), C, S>,
674{
675    async fn call(
676        &self,
677        batch: &[T],
678        _injections: &(),
679        ctx: &mut Context<'_, C, S>,
680    ) -> Result<Vec<R>, BatchResult> {
681        let verdict = self.0.value.body.handle(batch, &(), ctx).await;
682        batch_reply_verdict(verdict, batch.len(), ctx.name())
683    }
684}
685
686impl<Hd, P, R, C, S, H, Doc, Dest> BatchPublishingCall<S>
687    for Sealed<ReplyValue<HandleValue<BatchPair<Hd, P>, Vec<R>, (), C, H, Doc>, Dest>>
688where
689    Self: BatchPublishingDef<
690            Input = <BatchPair<Hd, P> as Axis>::Kind,
691            Injections = (),
692            Context = C,
693            Reply = R,
694        >,
695    [Message<Hd, P>]: Input<Axis = BatchPair<Hd, P>>,
696    Hd: Send + Sync + 'static,
697    P: Send + Sync + 'static,
698    R: ReplyShape,
699    C: Send + Sync,
700    S: Send + Sync,
701    H: Handle<[Message<Hd, P>], Vec<R>, (), C, S>,
702{
703    async fn call(
704        &self,
705        batch: &[Message<Hd, P>],
706        _injections: &(),
707        ctx: &mut Context<'_, C, S>,
708    ) -> Result<Vec<R>, BatchResult> {
709        let verdict = self.0.value.body.handle(batch, &(), ctx).await;
710        batch_reply_verdict(verdict, batch.len(), ctx.name())
711    }
712}