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, "e.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("E_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;