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}