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