Skip to main content

dex_sdk/
join.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Sustainable Use License 1.0.
4// You may not use this file except in compliance with the License.
5// See the LICENSE file in the repository root.
6//
7// SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
9use serde::{Deserialize, Serialize};
10
11use crate::registry::validate_map_instance;
12use crate::{
13    ChannelMap, Condition, Context, HandlerError, HandlerResult, StepDecision, StepMovement, Value,
14};
15
16/// Identifies one round of a [`Join`] and the number of branches in that round.
17///
18/// [`Join::fork`] and [`Join::fork_with_round_key`] create the round. Pass the same value, unchanged,
19/// to every branch Step, to the join Step, and to the next round's split Step when a loop resets the
20/// Join. Treat it as an opaque token: the application never computes the key or the count itself.
21///
22/// A `JoinRound` is persisted inside Step inputs as the JSON object
23/// `{"roundKey": string, "branchCount": integer}`. Every Dex SDK uses these field names, so a
24/// round created by one SDK decodes in another. Deserializing a negative `branchCount` fails.
25///
26/// See [`Join`] for a complete example.
27///
28/// Since 1.44.0.
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct JoinRound {
32    /// The Channel-map instance that holds this round's branch results.
33    ///
34    /// [`Join::fork`] uses the split Step's execution ID, such as `RequestQuotes-2`.
35    /// [`Join::fork_with_round_key`] uses the caller's key. Dex Web shows it as the instance key of
36    /// the Join's Channel map. It is never empty and never contains `/` when the SDK creates it.
37    pub round_key: String,
38    /// The number of branches the round started, which is the number of branch builders passed to
39    /// the fork.
40    ///
41    /// [`Join::for_all_branches`] waits for this many results. It may be zero, in which case that
42    /// condition completes immediately.
43    pub branch_count: usize,
44}
45
46/// Synchronizes the parallel branches of one Flow by counting the results they report.
47///
48/// Use a Join when one Step starts several branches and a later Step must continue after all of
49/// them, the first of them, or any `n` of them have finished. The Join produces the branch count and
50/// the round key, so the application keeps no counter and no round number.
51///
52/// A round has three roles:
53///
54/// 1. The split Step calls [`Self::fork`] in `execute`. It passes one builder per branch and one
55///    builder for the join Step. Each builder receives the round's [`JoinRound`] and returns a
56///    [`StepMovement`]. Return the movements with [`StepDecision::go_to_many`].
57/// 2. Each branch reports exactly one result. A single-Step branch returns [`Self::end_branch`]. A
58///    branch that continues after reporting calls [`Self::report_branch_result`].
59/// 3. The join Step waits in `wait_for` with [`Self::for_all_branches`], [`Self::for_first_branch`],
60///    or [`Self::for_branches`]. Its `execute` reads the results with [`Self::branch_results`].
61///
62/// A Join is a [`ChannelMap`] of `T` with the same name. Each round uses its own instance, keyed by
63/// [`JoinRound::round_key`]. A branch result is stored as the plain `T` value, so Dex Web,
64/// pending-message reads, and Channel-map loads show and read the Join like any Channel map.
65/// Results of another round never satisfy this round's condition. The conditions are ordinary
66/// [`Condition`] values. Combine them with [`crate::Wait::all_of`], [`crate::Wait::any_of`], or
67/// [`crate::Wait::any_combination_of`], for example with a [`crate::Timer`] as a deadline.
68///
69/// After a first-of-m or n-of-m join, the other branches still report. To reuse the Join in a loop,
70/// reset the finished round, unless a cancelling join finished it. Pass its `JoinRound` to the next
71/// round's split Step. That Step waits in `wait_for` with [`Self::for_remaining_branches`] and the
72/// count the join consumed. Its `execute` can read the late results with
73/// [`Self::remaining_branch_results`]. The next round therefore cannot start before the previous
74/// round's branches have all reported. Without a reset, late results stay pending in the round's
75/// instance until the Flow closes. They never count toward another round.
76///
77/// Add the Join to the Flow with [`crate::PersistenceSchema::join`]. Declare it as a module-level
78/// [`std::sync::LazyLock`] static. The name follows the Channel-map rules: it is stable, it contains
79/// no `/`, and it differs from every other persistence definition of the Flow.
80///
81/// # Rules and traps
82///
83/// - Each branch reports exactly one result per round. A branch that never reports keeps an all-of
84///   join waiting forever. A branch that reports twice counts twice. When an execute failure routes
85///   a branch to a recovery Step, that Step must report a result that represents the failure.
86/// - A multi-Step branch passes its `JoinRound` down to the Step that reports. A branch passes the
87///   round only to [`Self::end_branch`] or [`Self::report_branch_result`]. It never waits on a join
88///   condition.
89/// - Only the join Step that the fork started waits for a round. Two Steps that wait for the same
90///   round split its results between them.
91/// - The `received_count` of [`Self::for_remaining_branches`] equals the number of results the
92///   join consumed: `count` after [`Self::for_branches`], and `1` after [`Self::for_first_branch`].
93/// - A reset waits for every branch that has not reported yet, so it never completes while a
94///   canceled branch has not reported. Never reset the round of a cancelling join. The next round
95///   forks a new round without the reset wait, as the first round does. Without a reset, the
96///   round's late results stay pending until the Flow closes.
97/// - One invocation forks one Join at most once with [`Self::fork`]. Use a second Join, or
98///   [`Self::fork_with_round_key`] with distinct keys, for two independent joins.
99/// - An RPC has no Step execution ID, so it must use [`Self::fork_with_round_key`].
100/// - A cancelling join adds [`StepDecision::cancel_sibling_step`] for each branch Step type to the
101///   join Step's decision. The fork starts the branches and the join Step from one decision, so they
102///   are siblings, and only this round's branches are selected. A later Step of a multi-Step branch
103///   is not a sibling. Select it with [`StepDecision::cancel_step`], which is correct only while one
104///   round is active. Steps started by an RPC count as siblings of every Step started by any call of
105///   that RPC. For rounds that an RPC forks, also use [`StepDecision::cancel_step`] with one active
106///   round.
107/// - A canceled branch never reports, and no other code can safely report for it. Dex decides at
108///   commit time whether a cancellation takes effect, so the code that cancels a branch cannot know
109///   whether the branch already reported. If it did, the cancellation does nothing, and an extra
110///   report counts that branch twice, so an all-of join can complete while another branch has not
111///   reported. To keep an all-of join from waiting forever for a canceled branch, cancel the join
112///   Step together with its branches, or wait on [`crate::Wait::any_of`] with the join condition
113///   and a [`crate::Timer`] and handle the timeout in `execute`.
114/// - When a join Step is canceled after its wait matched, what happens to the results it consumed
115///   depends on the server version. A server that requeues them puts them back at the front of
116///   the round's instance, unless the canceling decision closes the Flow. On a server that drops
117///   them, a new join Step for that round never completes. See the server-zd change log at
118///   <https://docs.superdurable.io/reference/server/changelog>.
119///
120/// # Examples
121///
122/// A structured partial join that loops: each round asks three suppliers, continues after the
123/// first `required_quotes` quotes, and resets before the next round.
124///
125/// ```
126/// use std::sync::LazyLock;
127///
128/// use dex_sdk::{
129///     Context, Flow, HandlerResult, Join, JoinRound, PersistenceSchema, Step, StepDecision,
130///     StepList, StepMovement, Wait,
131/// };
132/// use serde::{Deserialize, Serialize};
133///
134/// static QUOTE_RECEIVED: LazyLock<Join<String>> = LazyLock::new(|| Join::new("quoteReceived"));
135///
136/// #[derive(Serialize, Deserialize)]
137/// struct QuoteRequest {
138///     required_quotes: usize,
139///     round_number: u32,
140///     round_count: u32,
141///     previous_round: Option<JoinRound>,
142/// }
143///
144/// #[derive(Serialize, Deserialize)]
145/// struct SupplierQuote {
146///     supplier: String,
147///     round: JoinRound,
148/// }
149///
150/// #[derive(Serialize, Deserialize)]
151/// struct AcceptQuotesInput {
152///     request: QuoteRequest,
153///     round: JoinRound,
154/// }
155///
156/// // Split: resets the previous round, then forks one branch per supplier and the join.
157/// struct RequestQuotes;
158///
159/// impl Step for RequestQuotes {
160///     type Input = QuoteRequest;
161///
162///     fn wait_for(&self, _context: &mut Context, request: QuoteRequest) -> HandlerResult<Wait> {
163///         match &request.previous_round {
164///             None => Ok(Wait::skip_immediately()),
165///             Some(previous_round) => Ok(Wait::until(
166///                 QUOTE_RECEIVED.for_remaining_branches(previous_round, request.required_quotes),
167///             )),
168///         }
169///     }
170///
171///     fn execute(
172///         &self,
173///         context: &mut Context,
174///         request: QuoteRequest,
175///     ) -> HandlerResult<StepDecision> {
176///         if let Some(previous_round) = &request.previous_round {
177///             let late_quotes = QUOTE_RECEIVED.remaining_branch_results(context, previous_round)?;
178///             context.record_event("lateQuotes", late_quotes)?;
179///         }
180///         let branch_builders = ["A", "B", "C"]
181///             .into_iter()
182///             .map(|supplier| {
183///                 Box::new(move |round: &JoinRound| {
184///                     StepMovement::to(
185///                         &QuoteFromSupplier,
186///                         SupplierQuote {
187///                             supplier: supplier.to_string(),
188///                             round: round.clone(),
189///                         },
190///                     )
191///                 }) as Box<dyn FnOnce(&JoinRound) -> StepMovement>
192///             })
193///             .collect();
194///         let movements = QUOTE_RECEIVED.fork(context, branch_builders, |round| {
195///             StepMovement::to(&AcceptQuotes, AcceptQuotesInput { request, round })
196///         })?;
197///         Ok(StepDecision::go_to_many(movements))
198///     }
199/// }
200///
201/// // Branch: reports one result and ends.
202/// struct QuoteFromSupplier;
203///
204/// impl Step for QuoteFromSupplier {
205///     type Input = SupplierQuote;
206///
207///     fn execute(
208///         &self,
209///         context: &mut Context,
210///         quote: SupplierQuote,
211///     ) -> HandlerResult<StepDecision> {
212///         QUOTE_RECEIVED.end_branch(context, &quote.round, quote.supplier)
213///     }
214/// }
215///
216/// // Join: continues after the required number of quotes, then starts the next round.
217/// struct AcceptQuotes;
218///
219/// impl Step for AcceptQuotes {
220///     type Input = AcceptQuotesInput;
221///
222///     fn wait_for(&self, _context: &mut Context, input: AcceptQuotesInput) -> HandlerResult<Wait> {
223///         Ok(Wait::until(
224///             QUOTE_RECEIVED.for_branches(&input.round, input.request.required_quotes),
225///         ))
226///     }
227///
228///     fn execute(
229///         &self,
230///         context: &mut Context,
231///         input: AcceptQuotesInput,
232///     ) -> HandlerResult<StepDecision> {
233///         let accepted_quotes = QUOTE_RECEIVED.branch_results(context, &input.round)?;
234///         let request = input.request;
235///         if request.round_number == request.round_count {
236///             return Ok(StepDecision::graceful_complete(accepted_quotes));
237///         }
238///         Ok(StepDecision::go_to(
239///             &RequestQuotes,
240///             QuoteRequest {
241///                 round_number: request.round_number + 1,
242///                 previous_round: Some(input.round),
243///                 ..request
244///             },
245///         ))
246///     }
247/// }
248///
249/// struct QuoteFlow {
250///     request_quotes: RequestQuotes,
251///     quote_from_supplier: QuoteFromSupplier,
252///     accept_quotes: AcceptQuotes,
253/// }
254///
255/// impl Flow for QuoteFlow {
256///     type StartInput = QuoteRequest;
257///
258///     fn steps(&self) -> StepList<'_, QuoteRequest> {
259///         StepList::start(&self.request_quotes)
260///             .and(&self.quote_from_supplier)
261///             .and(&self.accept_quotes)
262///     }
263///
264///     fn persistence(&self) -> PersistenceSchema {
265///         PersistenceSchema::new().join(&QUOTE_RECEIVED)
266///     }
267/// }
268/// ```
269///
270/// Since 1.44.0.
271pub struct Join<T> {
272    channel_map: ChannelMap<T>,
273}
274
275impl<T> Join<T> {
276    /// Defines a Join whose branch results use the Channel map named `name`.
277    ///
278    /// The name is the durable identity of the Join. Keep it stable while any Flow that uses it is
279    /// running. Slash is prohibited because it is a reserved character. A Join that replaces a
280    /// hand-written Channel-map join keeps in-flight Flows working when it reuses that map's name and
281    /// value type and forks with [`Self::fork_with_round_key`] and the same instance keys.
282    ///
283    /// Since 1.44.0.
284    pub fn new(name: impl Into<String>) -> Self {
285        Self {
286            channel_map: ChannelMap::new(name),
287        }
288    }
289
290    /// Starts one round from a split Step: builds every branch movement and the join movement.
291    ///
292    /// The round key is the current Step execution ID, which stays the same across retries and
293    /// time travel. The branch count is `branch_builders.len()`. The SDK calls each branch builder
294    /// once, in order, with the round, and then calls `join_builder` with the same round. The
295    /// returned movements are the branch movements in builder order followed by the join movement.
296    /// Pass them to [`StepDecision::go_to_many`], optionally with more movements. The call stages no
297    /// durable change.
298    ///
299    /// An empty `branch_builders` creates a round with zero branches. Its
300    /// [`Self::for_all_branches`] condition completes immediately, so a multi-choice split that
301    /// selects no branch needs no special case.
302    ///
303    /// Each closure has its own type, so box every branch builder. Map a runtime list to builders to
304    /// start one branch per element; see the example on [`Join`].
305    ///
306    /// # Errors
307    ///
308    /// Returns a [`HandlerError`] when the Context has no Step execution ID, as in an RPC; use
309    /// [`Self::fork_with_round_key`] there. Also returns an error when this invocation already
310    /// forked this Join with `fork`. Both errors are deterministic, so a retry fails the same way.
311    ///
312    /// Since 1.44.0.
313    #[expect(
314        clippy::type_complexity,
315        reason = "the boxed builder type stays inline so Join and JoinRound remain the only public types"
316    )]
317    pub fn fork<'builder>(
318        &self,
319        context: &mut Context,
320        branch_builders: Vec<Box<dyn FnOnce(&JoinRound) -> StepMovement + 'builder>>,
321        join_builder: impl FnOnce(JoinRound) -> StepMovement,
322    ) -> HandlerResult<Vec<StepMovement>> {
323        let round_key = context.step_execution_id().to_string();
324        if round_key.is_empty() {
325            return Err(HandlerError::new(
326                "dex_sdk::HandlerError",
327                format!(
328                    "Join {} fork requires a Step execution ID; use fork_with_round_key in an RPC",
329                    self.name()
330                ),
331            ));
332        }
333        context.record_join_fork(self.name())?;
334        self.fork_with_round_key(&round_key, branch_builders, join_builder)
335    }
336
337    /// Starts one round with a caller-supplied round key; otherwise behaves like [`Self::fork`].
338    ///
339    /// Use it in an RPC, which has no Step execution ID, and add the returned movements to the
340    /// [`crate::RpcResult`] with [`crate::RpcResult::then`]. Also use it to keep the instance keys of
341    /// a hand-written Channel-map join, or to fork one Join twice in one invocation with distinct
342    /// keys. The key must be unique among the Join's active rounds; a reused key mixes results of two
343    /// rounds. This method does not check for a repeated fork.
344    ///
345    /// # Errors
346    ///
347    /// Returns a [`HandlerError`] when `round_key` is empty or contains `/`.
348    ///
349    /// Since 1.44.0.
350    #[expect(
351        clippy::type_complexity,
352        reason = "the boxed builder type stays inline so Join and JoinRound remain the only public types"
353    )]
354    pub fn fork_with_round_key<'builder>(
355        &self,
356        round_key: &str,
357        branch_builders: Vec<Box<dyn FnOnce(&JoinRound) -> StepMovement + 'builder>>,
358        join_builder: impl FnOnce(JoinRound) -> StepMovement,
359    ) -> HandlerResult<Vec<StepMovement>> {
360        validate_map_instance(round_key).map_err(|error| {
361            HandlerError::new(
362                "dex_sdk::HandlerError",
363                format!("Join {} round key is invalid: {error}", self.name()),
364            )
365        })?;
366        let round = JoinRound {
367            round_key: round_key.to_string(),
368            branch_count: branch_builders.len(),
369        };
370        let mut movements = Vec::with_capacity(branch_builders.len() + 1);
371        for branch_builder in branch_builders {
372            movements.push(branch_builder(&round));
373        }
374        movements.push(join_builder(round));
375        Ok(movements)
376    }
377
378    /// Reports this branch's result and returns [`StepDecision::dead_end`].
379    ///
380    /// Call it as the return value of a branch's last Step. The result is staged like any Channel
381    /// publication: it becomes durable only when the handler returns successfully, together with the
382    /// decision. A failed attempt reports nothing, and a retry does not report twice.
383    ///
384    /// # Errors
385    ///
386    /// Returns the errors of [`Self::report_branch_result`].
387    ///
388    /// Since 1.44.0.
389    pub fn end_branch(
390        &self,
391        context: &mut Context,
392        round: &JoinRound,
393        result: T,
394    ) -> HandlerResult<StepDecision>
395    where
396        T: Value,
397    {
398        self.report_branch_result(context, round, result)?;
399        Ok(StepDecision::dead_end())
400    }
401
402    /// Reports this branch's result without ending the branch.
403    ///
404    /// Use it when the branch continues after reporting, from a Step's `wait_for` or `execute`, for
405    /// example when a multi-Step branch reports from a Step other than its last. An RPC can report
406    /// a branch's result when that result arrives through the RPC and no Step of that branch
407    /// reports it, for example for a handoff branch in a round the RPC forked with
408    /// [`Self::fork_with_round_key`]; its input then carries the round. Never report for a canceled branch; see the rules on [`Join`]. The
409    /// result is staged and becomes durable only when the handler returns successfully. Results
410    /// queue in the order their handlers commit.
411    ///
412    /// # Errors
413    ///
414    /// Returns a [`HandlerError`] when the Join is not in the Flow's persistence schema, when the
415    /// round key is empty or contains `/`, or when `result` cannot be encoded.
416    ///
417    /// Since 1.44.0.
418    pub fn report_branch_result(
419        &self,
420        context: &mut Context,
421        round: &JoinRound,
422        result: T,
423    ) -> HandlerResult<()>
424    where
425        T: Value,
426    {
427        self.channel_map.publish(context, &round.round_key, result)
428    }
429
430    /// Creates a condition that consumes the results of every branch in the round.
431    ///
432    /// It consumes exactly [`JoinRound::branch_count`] results. A round with zero branches
433    /// completes immediately and consumes nothing.
434    ///
435    /// An invalid round, with an empty key or a key containing `/`, makes the Step's `wait_for`
436    /// fail when the SDK sends the Wait.
437    ///
438    /// Since 1.44.0.
439    pub fn for_all_branches(&self, round: &JoinRound) -> Condition {
440        self.counted_condition(round, round.branch_count)
441    }
442
443    /// Creates a condition that consumes the first reported result of the round.
444    ///
445    /// Use it for a discriminator. When several results are already queued, it consumes only the
446    /// earliest one; the others remain for [`Self::for_remaining_branches`] with a
447    /// `received_count` of `1`. Put the branch identity into `T` when the join needs to know which
448    /// branch won. It is [`Self::for_branches`] with a count of `1`.
449    ///
450    /// A round with zero branches, or an invalid round, makes the Step's `wait_for` fail when the
451    /// SDK sends the Wait.
452    ///
453    /// Since 1.44.0.
454    pub fn for_first_branch(&self, round: &JoinRound) -> Condition {
455        self.for_branches(round, 1)
456    }
457
458    /// Creates a condition that consumes the first `count` reported results of the round.
459    ///
460    /// Use it for a partial join that continues after `count` of the round's branches have
461    /// reported. Pass the same `count` to [`Self::for_remaining_branches`] when a loop resets the
462    /// round.
463    ///
464    /// A `count` outside `1..=round.branch_count`, or an invalid round, makes the Step's `wait_for`
465    /// fail when the SDK sends the Wait.
466    ///
467    /// Since 1.44.0.
468    pub fn for_branches(&self, round: &JoinRound, count: usize) -> Condition {
469        if count == 0 || count > round.branch_count {
470            return Condition::invalid(format!(
471                "Join {} for_branches count {count} is outside 1..={}",
472                self.name(),
473                round.branch_count
474            ));
475        }
476        self.counted_condition(round, count)
477    }
478
479    /// Creates a condition that consumes the results a finished round has not consumed yet.
480    ///
481    /// Use it in the next round's split Step `wait_for` to reset the Join. It waits for, and
482    /// consumes, `round.branch_count - received_count` results. `received_count` is the number of
483    /// results the join consumed: the `count` of [`Self::for_branches`], or `1` for
484    /// [`Self::for_first_branch`]. When nothing remains, it completes immediately.
485    ///
486    /// `received_count` counts only results the join consumed. Never count a canceled branch, which
487    /// does not report. The reset therefore waits for every canceled branch too, so never reset the
488    /// round of a cancelling join; see the rules on [`Join`].
489    ///
490    /// A `received_count` greater than `round.branch_count`, or an invalid round, makes the Step's
491    /// `wait_for` fail when the SDK sends the Wait.
492    ///
493    /// Since 1.44.0.
494    pub fn for_remaining_branches(&self, round: &JoinRound, received_count: usize) -> Condition {
495        let Some(remaining_count) = round.branch_count.checked_sub(received_count) else {
496            return Condition::invalid(format!(
497                "Join {} for_remaining_branches received count {received_count} is outside 0..={}",
498                self.name(),
499                round.branch_count
500            ));
501        };
502        self.counted_condition(round, remaining_count)
503    }
504
505    /// Returns the results the join Step's satisfied condition consumed, in report order.
506    ///
507    /// Call it in the join Step's `execute`. It returns an empty list when the round's condition
508    /// did not complete, for example when a Timer won a [`crate::Wait::any_of`].
509    ///
510    /// # Errors
511    ///
512    /// Returns a [`HandlerError`] when the Join is not in the Flow's persistence schema, when the
513    /// round key is invalid, or when a result cannot be decoded.
514    ///
515    /// Since 1.44.0.
516    pub fn branch_results(&self, context: &Context, round: &JoinRound) -> HandlerResult<Vec<T>>
517    where
518        T: Value,
519    {
520        self.channel_map
521            .condition_results(context, &round.round_key)
522    }
523
524    /// Returns the late results that a reset with [`Self::for_remaining_branches`] consumed.
525    ///
526    /// Call it in the `execute` of the Step that waited for the reset, usually the next round's
527    /// split Step. It reads the same consumed results as [`Self::branch_results`]; the separate name
528    /// states at the call site that these results arrived after the round's join.
529    ///
530    /// # Errors
531    ///
532    /// Returns the errors of [`Self::branch_results`].
533    ///
534    /// Since 1.44.0.
535    pub fn remaining_branch_results(
536        &self,
537        context: &Context,
538        round: &JoinRound,
539    ) -> HandlerResult<Vec<T>>
540    where
541        T: Value,
542    {
543        self.channel_map
544            .condition_results(context, &round.round_key)
545    }
546
547    /// Returns the Channel map that holds this Join's branch results.
548    ///
549    /// Use it to read a round's pending results with the existing Channel-map API. The instance key
550    /// is the round's [`JoinRound::round_key`]. For example, an RPC selects the round with
551    /// [`crate::Rpc::load_channel_map`] or [`ChannelMap::load_messages`], then calls
552    /// [`ChannelMap::pending_messages`] or [`ChannelMap::size`]. The returned definition is the one
553    /// that [`crate::PersistenceSchema::join`] registers, so every API that accepts a registered
554    /// Channel map accepts it.
555    ///
556    /// Never publish to it or delete from it directly. Report results only with
557    /// [`Self::end_branch`] or [`Self::report_branch_result`]. A direct publication counts as a
558    /// branch result and breaks the rule that each branch reports exactly once.
559    ///
560    /// ```
561    /// use std::sync::LazyLock;
562    ///
563    /// use dex_sdk::{Context, HandlerResult, Join, Rpc, RpcResult};
564    ///
565    /// static QUOTE_RECEIVED: LazyLock<Join<String>> = LazyLock::new(|| Join::new("quoteReceived"));
566    ///
567    /// const GET_PENDING_QUOTES: Rpc<String, Vec<String>> = Rpc::new("get_pending_quotes");
568    ///
569    /// // Register with .function(GET_PENDING_QUOTES.load_channel_map(QUOTE_RECEIVED.channel_map()), ...).
570    /// fn get_pending_quotes(context: &mut Context, round_key: String) -> HandlerResult<RpcResult<Vec<String>>> {
571    ///     let pending_quotes = QUOTE_RECEIVED
572    ///         .channel_map()
573    ///         .pending_messages(context, &round_key)?
574    ///         .into_iter()
575    ///         .map(|message| message.value)
576    ///         .collect();
577    ///     Ok(RpcResult::new(pending_quotes))
578    /// }
579    /// ```
580    ///
581    /// Since 1.44.0.
582    pub fn channel_map(&self) -> &ChannelMap<T> {
583        &self.channel_map
584    }
585
586    pub(crate) fn name(&self) -> &str {
587        self.channel_map.name()
588    }
589
590    fn counted_condition(&self, round: &JoinRound, count: usize) -> Condition {
591        if let Err(error) = validate_map_instance(&round.round_key) {
592            return Condition::invalid(format!(
593                "Join {} round key is invalid: {error}",
594                self.name()
595            ));
596        }
597        self.channel_map.for_n(&round.round_key, count)
598    }
599}
600
601impl<T> Clone for Join<T> {
602    fn clone(&self) -> Self {
603        Self {
604            channel_map: self.channel_map.clone(),
605        }
606    }
607}
608
609#[cfg(test)]
610#[path = "join_tests.rs"]
611mod tests;