Skip to main content

dex_sdk/
channel.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 std::marker::PhantomData;
10
11use crate::{Condition, Context, HandlerResult, Value};
12
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub(crate) struct ChannelLoad {
15    pub(crate) name: String,
16}
17
18/// Selects one ChannelMap instance's pending messages for a handler invocation.
19///
20/// Create instance loads with [`ChannelMap::load_messages`], then attach them with
21/// [`Rpc::load_channel_map_instance`](crate::Rpc::load_channel_map_instance).
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct ChannelMapLoad {
24    pub(crate) name: String,
25    pub(crate) instance: Option<String>,
26}
27
28/// Identifies one typed value that is still pending in a Channel.
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct ChannelMessage<T> {
31    /// UUIDv7 assigned by Dex when the message was published.
32    pub message_id: String,
33    /// Decoded Channel value.
34    pub value: T,
35}
36
37/// Defines one durable FIFO queue of typed messages.
38///
39/// Add the Channel to [`crate::PersistenceSchema`]. Clients and handlers may publish messages;
40/// Steps create [`Condition`] values that wait for queue-size bounds and read the messages consumed
41/// by the satisfied condition.
42///
43/// Handler publications and deletions are staged until successful return. A satisfied wait
44/// consumes messages before Execute; it does not combine that consumption with later publications
45/// into one transaction. Pending-message reads are snapshots that other handlers can change.
46/// Select pending-message loads explicitly. Use transactional RPC execution for atomic deletion
47/// and publication, and a shared Attribute lock when all cooperating writers must serialize a
48/// decision about the queue. `T` must implement [`Value`] when publishing or reading messages.
49/// Declare reusable definitions as module-level [`std::sync::LazyLock`] statics.
50///
51/// To wait for parallel branches of one Flow, use [`crate::Join`] instead of counting signals.
52///
53/// # Examples
54///
55/// ```
56/// use dex_sdk::{Channel, Wait};
57///
58/// let approvals = Channel::<String>::new("approvals");
59/// let wait = Wait::until(approvals.for_one());
60/// ```
61pub struct Channel<T> {
62    name: String,
63    marker: PhantomData<fn() -> T>,
64}
65
66impl<T> Channel<T> {
67    /// Defines a Channel with a stable `name`.
68    ///
69    /// Slash is prohibited because it is a reserved character.
70    pub fn new(name: impl Into<String>) -> Self {
71        Self {
72            name: name.into(),
73            marker: PhantomData,
74        }
75    }
76
77    /// Stages one message from the current Step or RPC invocation.
78    ///
79    /// # Errors
80    ///
81    /// Returns a [`crate::HandlerError`] when `value` cannot be encoded.
82    pub fn publish(&self, context: &mut Context, value: T) -> HandlerResult<()>
83    where
84        T: Value,
85    {
86        context.publish(self, value)
87    }
88
89    /// Stages best-effort deletion of one pending message from a handler.
90    pub fn delete(&self, context: &mut Context, message_id: &str) -> HandlerResult<()> {
91        context.delete_channel_message(self, message_id)
92    }
93
94    /// Returns the invocation snapshot's queued-message count.
95    pub fn size(&self, context: &Context) -> HandlerResult<usize> {
96        context.channel_size(self)
97    }
98
99    /// Returns loaded pending messages from the invocation snapshot in FIFO order.
100    ///
101    /// # Errors
102    ///
103    /// Returns a [`crate::HandlerError`] when this Channel was not selected by the handler options
104    /// or when decoding fails.
105    pub fn pending_messages(&self, context: &Context) -> HandlerResult<Vec<ChannelMessage<T>>>
106    where
107        T: Value,
108    {
109        context.pending_channel_messages(self)
110    }
111
112    /// Finds one pending message by its Dex-assigned ID.
113    ///
114    /// Returns `Ok(None)` when the loaded snapshot does not contain `message_id`.
115    ///
116    /// # Errors
117    ///
118    /// Returns the same errors as [`Self::pending_messages`].
119    pub fn find_pending_message(
120        &self,
121        context: &Context,
122        message_id: &str,
123    ) -> HandlerResult<Option<ChannelMessage<T>>>
124    where
125        T: Value,
126    {
127        Ok(self
128            .pending_messages(context)?
129            .into_iter()
130            .find(|message| message.message_id == message_id))
131    }
132
133    /// Decodes messages consumed by this Channel's satisfied condition.
134    ///
135    /// # Errors
136    ///
137    /// Returns a [`crate::HandlerError`] when a message cannot be decoded.
138    pub fn condition_results(&self, context: &Context) -> HandlerResult<Vec<T>>
139    where
140        T: Value,
141    {
142        context.channel_results(self)
143    }
144
145    /// Creates a condition that consumes exactly one queued message.
146    pub fn for_one(&self) -> Condition {
147        self.range(Some(1), Some(1))
148    }
149
150    /// Creates a condition that consumes exactly `count` queued messages.
151    pub fn for_n(&self, count: usize) -> Condition {
152        self.range(Some(count), Some(count))
153    }
154
155    /// Creates a condition satisfied when at least `count` messages are queued.
156    pub fn at_least(&self, count: usize) -> Condition {
157        self.range(Some(count), None)
158    }
159
160    /// Creates a non-blocking condition consuming up to `count` queued messages.
161    ///
162    /// When its surrounding Wait completes, it consumes messages queued then. An empty queue yields
163    /// no messages.
164    pub fn at_most(&self, count: usize) -> Condition {
165        self.range(None, Some(count))
166    }
167
168    /// Creates a condition that waits for the lower bound, then consumes up to the upper bound.
169    ///
170    /// Omitting the lower bound makes the condition complete immediately. Consumption uses only
171    /// messages queued when the condition completes.
172    pub fn range(&self, at_least: Option<usize>, at_most: Option<usize>) -> Condition {
173        Condition::channel(self.name.clone(), None, at_least, at_most)
174    }
175
176    /// Creates a guard used by conditional Flow completion to require an empty queue.
177    pub fn when_empty(&self) -> ChannelGuard {
178        ChannelGuard {
179            name: self.name.clone(),
180            instance: None,
181        }
182    }
183
184    pub(crate) fn name(&self) -> &str {
185        &self.name
186    }
187}
188
189impl<T> Clone for Channel<T> {
190    fn clone(&self) -> Self {
191        Self {
192            name: self.name.clone(),
193            marker: PhantomData,
194        }
195    }
196}
197
198/// Defines independently queued Channel instances under one name.
199///
200/// Supply an instance string for every publish, read, condition, and completion guard. Add the map
201/// definition once to [`crate::PersistenceSchema`].
202/// Slash is prohibited in instance keys because it is a reserved character.
203/// To wait for parallel branches by round, use [`crate::Join`], which is built on a Channel map.
204pub struct ChannelMap<T> {
205    name: String,
206    marker: PhantomData<fn() -> T>,
207}
208
209impl<T> ChannelMap<T> {
210    /// Defines a Channel map with a stable `name`.
211    ///
212    /// Slash is prohibited because it is a reserved character.
213    pub fn new(name: impl Into<String>) -> Self {
214        Self {
215            name: name.into(),
216            marker: PhantomData,
217        }
218    }
219
220    /// Selects one slash-free map `instance`'s pending messages for a handler Context.
221    pub fn load_messages(&self, instance: impl Into<String>) -> ChannelMapLoad {
222        ChannelMapLoad {
223            name: self.name.clone(),
224            instance: Some(instance.into()),
225        }
226    }
227
228    /// Stages one message for `instance` from the current invocation.
229    ///
230    /// # Errors
231    ///
232    /// Returns a [`crate::HandlerError`] when `value` cannot be encoded.
233    pub fn publish(&self, context: &mut Context, instance: &str, value: T) -> HandlerResult<()>
234    where
235        T: Value,
236    {
237        context.publish_map(self, instance, value)
238    }
239
240    /// Stages best-effort deletion of one pending message from a Channel-map instance.
241    pub fn delete(
242        &self,
243        context: &mut Context,
244        instance: &str,
245        message_id: &str,
246    ) -> HandlerResult<()> {
247        context.delete_channel_map_message(self, instance, message_id)
248    }
249
250    /// Returns the invocation snapshot's message count for `instance`.
251    pub fn size(&self, context: &Context, instance: &str) -> HandlerResult<usize> {
252        context.channel_map_size(self, instance)
253    }
254
255    /// Returns one instance's loaded pending messages in FIFO order.
256    ///
257    /// # Errors
258    ///
259    /// Returns a [`crate::HandlerError`] when the instance was not selected by the handler options
260    /// or when a Value cannot be decoded.
261    pub fn pending_messages(
262        &self,
263        context: &Context,
264        instance: &str,
265    ) -> HandlerResult<Vec<ChannelMessage<T>>>
266    where
267        T: Value,
268    {
269        context.pending_channel_map_messages(self, instance)
270    }
271
272    /// Finds one pending message by ID within one loaded map instance.
273    ///
274    /// Returns `Ok(None)` when the loaded snapshot does not contain `message_id`.
275    ///
276    /// # Errors
277    ///
278    /// Returns the same errors as [`Self::pending_messages`].
279    pub fn find_pending_message(
280        &self,
281        context: &Context,
282        instance: &str,
283        message_id: &str,
284    ) -> HandlerResult<Option<ChannelMessage<T>>>
285    where
286        T: Value,
287    {
288        Ok(self
289            .pending_messages(context, instance)?
290            .into_iter()
291            .find(|message| message.message_id == message_id))
292    }
293
294    /// Returns the number of non-empty instances visible to the current handler.
295    pub fn map_size(&self, context: &Context) -> HandlerResult<usize> {
296        Ok(self.all_instance_keys(context)?.len())
297    }
298
299    /// Returns sorted decoded non-empty keys, including buffered publishes.
300    pub fn all_instance_keys(&self, context: &Context) -> HandlerResult<Vec<String>> {
301        context.channel_map_keys(self)
302    }
303
304    /// Decodes messages consumed by `instance`'s satisfied condition.
305    ///
306    /// # Errors
307    ///
308    /// Returns a [`crate::HandlerError`] when a message cannot be decoded.
309    pub fn condition_results(&self, context: &Context, instance: &str) -> HandlerResult<Vec<T>>
310    where
311        T: Value,
312    {
313        context.channel_map_results(self, instance)
314    }
315
316    /// Creates an `instance` condition that consumes exactly one message.
317    pub fn for_one(&self, instance: &str) -> Condition {
318        self.range(instance, Some(1), Some(1))
319    }
320
321    /// Creates an `instance` condition that consumes exactly `count` messages.
322    pub fn for_n(&self, instance: &str, count: usize) -> Condition {
323        self.range(instance, Some(count), Some(count))
324    }
325
326    /// Creates an `instance` condition with an inclusive lower queue-size bound.
327    pub fn at_least(&self, instance: &str, count: usize) -> Condition {
328        self.range(instance, Some(count), None)
329    }
330
331    /// Creates a non-blocking `instance` condition consuming up to `count` queued messages.
332    ///
333    /// When its surrounding Wait completes, it consumes messages queued then. An empty queue yields
334    /// no messages.
335    pub fn at_most(&self, instance: &str, count: usize) -> Condition {
336        self.range(instance, None, Some(count))
337    }
338
339    /// Creates an `instance` condition that waits for the lower bound, then consumes to its upper.
340    ///
341    /// Omitting the lower bound makes the condition complete immediately. Consumption uses only
342    /// messages queued when the condition completes.
343    pub fn range(
344        &self,
345        instance: &str,
346        at_least: Option<usize>,
347        at_most: Option<usize>,
348    ) -> Condition {
349        crate::registry::assert_map_instance(instance);
350        Condition::channel(
351            self.name.clone(),
352            Some(instance.to_string()),
353            at_least,
354            at_most,
355        )
356    }
357
358    /// Creates a conditional-completion guard requiring `instance` to be empty.
359    pub fn when_empty(&self, instance: &str) -> ChannelGuard {
360        crate::registry::assert_map_instance(instance);
361        ChannelGuard {
362            name: self.name.clone(),
363            instance: Some(instance.to_string()),
364        }
365    }
366
367    pub(crate) fn name(&self) -> &str {
368        &self.name
369    }
370}
371
372impl<T> Clone for ChannelMap<T> {
373    fn clone(&self) -> Self {
374        Self {
375            name: self.name.clone(),
376            marker: PhantomData,
377        }
378    }
379}
380
381#[derive(Clone, Debug, Eq, PartialEq)]
382/// Identifies a Channel or Channel-map instance that must be empty before conditional completion.
383pub struct ChannelGuard {
384    name: String,
385    instance: Option<String>,
386}
387
388impl ChannelGuard {
389    pub(crate) fn physical_name(&self) -> String {
390        match self.instance.as_deref() {
391            Some(instance) => crate::registry::physical_name(&self.name, instance),
392            None => self.name.clone(),
393        }
394    }
395}