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}