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