Skip to main content

dex_sdk/
channel.rs

1// Copyright (c) 2026 Super Durable, Inc.
2//
3// Licensed under the Super Durable Source 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-Super-Durable-1.0
8
9use std::marker::PhantomData;
10
11use crate::{Condition, Context, HandlerResult, Value};
12
13/// Defines one durable FIFO queue of typed messages.
14///
15/// Add the Channel to [`crate::PersistenceSchema`]. Clients and handlers may publish messages;
16/// Steps create [`Condition`] values that wait for queue-size bounds and read the messages consumed
17/// by the satisfied condition.
18///
19/// # Examples
20///
21/// ```
22/// use dex_sdk::{Channel, Wait};
23///
24/// let approvals = Channel::<String>::new("approvals");
25/// let wait = Wait::until(approvals.for_one());
26/// ```
27pub struct Channel<T> {
28    name: String,
29    marker: PhantomData<fn() -> T>,
30}
31
32impl<T> Channel<T> {
33    /// Defines a Channel with stable `name`.
34    pub fn new(name: impl Into<String>) -> Self {
35        Self {
36            name: name.into(),
37            marker: PhantomData,
38        }
39    }
40
41    /// Stages one message from the current Step or RPC invocation.
42    ///
43    /// # Errors
44    ///
45    /// Returns a [`crate::HandlerError`] when `value` cannot be encoded.
46    pub fn publish(&self, context: &mut Context, value: T) -> HandlerResult<()>
47    where
48        T: Value,
49    {
50        context.publish(self, value)
51    }
52
53    /// Returns the invocation snapshot's queued-message count.
54    pub fn size(&self, context: &Context) -> HandlerResult<usize> {
55        context.channel_size(self)
56    }
57
58    /// Decodes messages consumed by this Channel's satisfied condition.
59    ///
60    /// # Errors
61    ///
62    /// Returns a [`crate::HandlerError`] when a message cannot be decoded.
63    pub fn condition_results(&self, context: &Context) -> HandlerResult<Vec<T>>
64    where
65        T: Value,
66    {
67        context.channel_results(self)
68    }
69
70    /// Creates a condition that consumes exactly one queued message.
71    pub fn for_one(&self) -> Condition {
72        self.range(Some(1), Some(1))
73    }
74
75    /// Creates a condition that consumes exactly `count` queued messages.
76    pub fn for_n(&self, count: usize) -> Condition {
77        self.range(Some(count), Some(count))
78    }
79
80    /// Creates a condition satisfied when at least `count` messages are queued.
81    pub fn at_least(&self, count: usize) -> Condition {
82        self.range(Some(count), None)
83    }
84
85    /// Creates a condition satisfied when no more than `count` messages are queued.
86    pub fn at_most(&self, count: usize) -> Condition {
87        self.range(None, Some(count))
88    }
89
90    /// Creates a condition with optional inclusive lower and upper queue-size bounds.
91    pub fn range(&self, at_least: Option<usize>, at_most: Option<usize>) -> Condition {
92        Condition::channel(self.name.clone(), None, at_least, at_most)
93    }
94
95    /// Creates a guard used by conditional Flow completion to require an empty queue.
96    pub fn when_empty(&self) -> ChannelGuard {
97        ChannelGuard {
98            name: self.name.clone(),
99            instance: None,
100        }
101    }
102
103    pub(crate) fn name(&self) -> &str {
104        &self.name
105    }
106}
107
108impl<T> Clone for Channel<T> {
109    fn clone(&self) -> Self {
110        Self {
111            name: self.name.clone(),
112            marker: PhantomData,
113        }
114    }
115}
116
117/// Defines independently queued Channel instances under one name.
118///
119/// Supply an instance string for every publish, read, condition, and completion guard. Add the map
120/// definition once to [`crate::PersistenceSchema`].
121pub struct ChannelMap<T> {
122    name: String,
123    marker: PhantomData<fn() -> T>,
124}
125
126impl<T> ChannelMap<T> {
127    /// Defines a Channel map with stable `name`.
128    pub fn new(name: impl Into<String>) -> Self {
129        Self {
130            name: name.into(),
131            marker: PhantomData,
132        }
133    }
134
135    /// Stages one message for `instance` from the current invocation.
136    ///
137    /// # Errors
138    ///
139    /// Returns a [`crate::HandlerError`] when `value` cannot be encoded.
140    pub fn publish(&self, context: &mut Context, instance: &str, value: T) -> HandlerResult<()>
141    where
142        T: Value,
143    {
144        context.publish_map(self, instance, value)
145    }
146
147    /// Returns the invocation snapshot's message count for `instance`.
148    pub fn size(&self, context: &Context, instance: &str) -> HandlerResult<usize> {
149        context.channel_map_size(self, instance)
150    }
151
152    /// Returns the number of non-empty instances visible to the current RPC.
153    pub fn map_size(&self, context: &Context) -> HandlerResult<usize> {
154        Ok(self.all_instance_keys(context)?.len())
155    }
156
157    /// Returns sorted decoded non-empty keys, including buffered publishes.
158    pub fn all_instance_keys(&self, context: &Context) -> HandlerResult<Vec<String>> {
159        context.channel_map_keys(self)
160    }
161
162    /// Decodes messages consumed by `instance`'s satisfied condition.
163    ///
164    /// # Errors
165    ///
166    /// Returns a [`crate::HandlerError`] when a message cannot be decoded.
167    pub fn condition_results(&self, context: &Context, instance: &str) -> HandlerResult<Vec<T>>
168    where
169        T: Value,
170    {
171        context.channel_map_results(self, instance)
172    }
173
174    /// Creates an `instance` condition that consumes exactly one message.
175    pub fn for_one(&self, instance: &str) -> Condition {
176        self.range(instance, Some(1), Some(1))
177    }
178
179    /// Creates an `instance` condition that consumes exactly `count` messages.
180    pub fn for_n(&self, instance: &str, count: usize) -> Condition {
181        self.range(instance, Some(count), Some(count))
182    }
183
184    /// Creates an `instance` condition with an inclusive lower queue-size bound.
185    pub fn at_least(&self, instance: &str, count: usize) -> Condition {
186        self.range(instance, Some(count), None)
187    }
188
189    /// Creates an `instance` condition with an inclusive upper queue-size bound.
190    pub fn at_most(&self, instance: &str, count: usize) -> Condition {
191        self.range(instance, None, Some(count))
192    }
193
194    /// Creates an `instance` condition with optional inclusive lower and upper bounds.
195    pub fn range(
196        &self,
197        instance: &str,
198        at_least: Option<usize>,
199        at_most: Option<usize>,
200    ) -> Condition {
201        Condition::channel(
202            self.name.clone(),
203            Some(instance.to_string()),
204            at_least,
205            at_most,
206        )
207    }
208
209    /// Creates a conditional-completion guard requiring `instance` to be empty.
210    pub fn when_empty(&self, instance: &str) -> ChannelGuard {
211        ChannelGuard {
212            name: self.name.clone(),
213            instance: Some(instance.to_string()),
214        }
215    }
216
217    pub(crate) fn name(&self) -> &str {
218        &self.name
219    }
220}
221
222impl<T> Clone for ChannelMap<T> {
223    fn clone(&self) -> Self {
224        Self {
225            name: self.name.clone(),
226            marker: PhantomData,
227        }
228    }
229}
230
231#[derive(Clone, Debug, Eq, PartialEq)]
232/// Identifies a Channel or Channel-map instance that must be empty before conditional completion.
233pub struct ChannelGuard {
234    name: String,
235    instance: Option<String>,
236}
237
238impl ChannelGuard {
239    pub(crate) fn physical_name(&self) -> String {
240        match self.instance.as_deref() {
241            Some(instance) => crate::registry::physical_name(&self.name, instance),
242            None => self.name.clone(),
243        }
244    }
245}