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