Skip to main content

dex_sdk/
rpc.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;
10use std::sync::Arc;
11use std::time::Duration;
12
13use dex_protocol::dex::Value as ProtoValue;
14
15use crate::attribute::{AttributeLock, AttributeMap, AttributeMapLoad};
16use crate::channel::{Channel, ChannelLoad, ChannelMap, ChannelMapLoad};
17use crate::step::{ErasedValue, TypedValue};
18use crate::value_mapper;
19use crate::{Context, HandlerResult, SdkResult, StepMovement, Value};
20
21/// Names one typed RPC exposed by a Flow.
22///
23/// Use the same value when binding the handler in [`RpcList`] and invoking it through
24/// [`crate::Client`]. Input and output types must implement [`Value`]. The stable name becomes part
25/// of the protocol and must remain compatible with running Flows.
26///
27/// # Examples
28///
29/// ```
30/// use dex_sdk::{Context, Flow, HandlerResult, Rpc, RpcList, RpcResult};
31///
32/// const STATUS: Rpc<(), String> = Rpc::new("status");
33/// struct OrderFlow;
34///
35/// impl OrderFlow {
36///     fn status(&self, _context: &mut Context) -> HandlerResult<RpcResult<String>> {
37///         Ok(RpcResult::new("ready".to_owned()))
38///     }
39/// }
40///
41/// impl Flow for OrderFlow {
42///     type StartInput = ();
43///
44///     fn rpcs(&self) -> RpcList<Self> {
45///         RpcList::new().function_without_input(STATUS, Self::status)
46///     }
47/// }
48/// ```
49pub struct Rpc<Input, Output> {
50    name: &'static str,
51    marker: PhantomData<fn(Input) -> Output>,
52}
53
54impl<Input, Output> Rpc<Input, Output> {
55    /// Defines an RPC with a stable protocol `name` and no option overrides.
56    pub const fn new(name: &'static str) -> Self {
57        Self {
58            name,
59            marker: PhantomData,
60        }
61    }
62
63    /// Converts this RPC into a definition with a handler execution timeout.
64    pub fn timeout(self, timeout: Duration) -> RpcDefinition<Input, Output> {
65        RpcDefinition::from(self).timeout(timeout)
66    }
67
68    /// Converts this RPC into a definition holding one Attribute lock during invocation.
69    pub fn lock(self, lock: AttributeLock) -> RpcDefinition<Input, Output> {
70        RpcDefinition::from(self).lock(lock)
71    }
72
73    /// Requests transactional reads and writes for this RPC.
74    pub fn is_transactional(self) -> RpcDefinition<Input, Output> {
75        RpcDefinition::from(self).is_transactional()
76    }
77
78    /// Converts this RPC into a definition that loads every current AttributeMap instance.
79    pub fn load_attribute_map<T>(
80        self,
81        attribute_map: &AttributeMap<T>,
82    ) -> RpcDefinition<Input, Output> {
83        RpcDefinition::from(self).load_attribute_map(attribute_map)
84    }
85
86    /// Converts this RPC into a definition that loads one AttributeMap instance.
87    pub fn load_attribute_map_instance(
88        self,
89        load: AttributeMapLoad,
90    ) -> RpcDefinition<Input, Output> {
91        RpcDefinition::from(self).load_attribute_map_instance(load)
92    }
93
94    /// Converts this RPC into a definition that loads one singleton Channel's messages.
95    pub fn load_channel<T>(self, channel: &Channel<T>) -> RpcDefinition<Input, Output> {
96        RpcDefinition::from(self).load_channel(channel)
97    }
98
99    /// Converts this RPC into a definition that loads every current ChannelMap instance.
100    pub fn load_channel_map<T>(self, channel_map: &ChannelMap<T>) -> RpcDefinition<Input, Output> {
101        RpcDefinition::from(self).load_channel_map(channel_map)
102    }
103
104    /// Converts this RPC into a definition that loads one ChannelMap instance.
105    pub fn load_channel_map_instance(self, load: ChannelMapLoad) -> RpcDefinition<Input, Output> {
106        RpcDefinition::from(self).load_channel_map_instance(load)
107    }
108
109    pub(crate) fn name(&self) -> &'static str {
110        self.name
111    }
112}
113
114impl<Input, Output> Clone for Rpc<Input, Output> {
115    fn clone(&self) -> Self {
116        *self
117    }
118}
119
120impl<Input, Output> Copy for Rpc<Input, Output> {}
121
122/// Adds execution options to a typed [`Rpc`] before handler binding.
123///
124/// Definitions are created implicitly from `Rpc` or explicitly through [`Rpc::timeout`] and
125/// [`Rpc::lock`]. Multiple locks are acquired together for the invocation.
126pub struct RpcDefinition<Input, Output> {
127    rpc: Rpc<Input, Output>,
128    timeout: Option<Duration>,
129    locks: Vec<AttributeLock>,
130    is_transactional: bool,
131    load_attribute_maps: Vec<AttributeMapLoad>,
132    load_channels: Vec<ChannelLoad>,
133    load_channel_maps: Vec<ChannelMapLoad>,
134}
135
136impl<Input, Output> RpcDefinition<Input, Output> {
137    /// Sets the maximum duration of one RPC handler attempt.
138    pub fn timeout(mut self, timeout: Duration) -> Self {
139        self.timeout = Some(timeout);
140        self
141    }
142
143    /// Adds an Attribute or Attribute-map instance lock.
144    pub fn lock(mut self, lock: AttributeLock) -> Self {
145        self.locks.push(lock);
146        self
147    }
148
149    /// Requests transactional reads and writes even when no Attribute lock is configured.
150    pub fn is_transactional(mut self) -> Self {
151        self.is_transactional = true;
152        self
153    }
154
155    /// Loads every current instance of one AttributeMap into the invocation snapshot.
156    pub fn load_attribute_map<T>(mut self, attribute_map: &AttributeMap<T>) -> Self {
157        self.load_attribute_maps.push(AttributeMapLoad {
158            name: attribute_map.name().to_owned(),
159            instance: None,
160        });
161        self
162    }
163
164    /// Loads one AttributeMap instance into the invocation snapshot.
165    pub fn load_attribute_map_instance(mut self, load: AttributeMapLoad) -> Self {
166        self.load_attribute_maps.push(load);
167        self
168    }
169
170    /// Adds one singleton Channel's pending messages to the invocation snapshot.
171    pub fn load_channel<T>(mut self, channel: &Channel<T>) -> Self {
172        self.load_channels.push(ChannelLoad {
173            name: channel.name().to_owned(),
174        });
175        self
176    }
177
178    /// Loads every current instance of one ChannelMap into the invocation snapshot.
179    pub fn load_channel_map<T>(mut self, channel_map: &ChannelMap<T>) -> Self {
180        self.load_channel_maps.push(ChannelMapLoad {
181            name: channel_map.name().to_owned(),
182            instance: None,
183        });
184        self
185    }
186
187    /// Loads one ChannelMap instance into the invocation snapshot.
188    pub fn load_channel_map_instance(mut self, load: ChannelMapLoad) -> Self {
189        self.load_channel_maps.push(load);
190        self
191    }
192}
193
194impl<Input, Output> From<Rpc<Input, Output>> for RpcDefinition<Input, Output> {
195    fn from(rpc: Rpc<Input, Output>) -> Self {
196        Self {
197            rpc,
198            timeout: None,
199            locks: Vec::new(),
200            is_transactional: false,
201            load_attribute_maps: Vec::new(),
202            load_channels: Vec::new(),
203            load_channel_maps: Vec::new(),
204        }
205    }
206}
207
208/// Binds typed RPC definitions to methods on one Flow type.
209///
210/// Return the completed list from [`crate::Flow::rpcs`]. Handler signatures encode whether input or
211/// output is present, preventing invalid calls at compile time.
212pub struct RpcList<FlowType> {
213    definitions: Vec<Box<dyn RpcBinder<FlowType>>>,
214}
215
216impl<FlowType> RpcList<FlowType>
217where
218    FlowType: Send + Sync + 'static,
219{
220    /// Creates an empty RPC list.
221    pub fn new() -> Self {
222        Self {
223            definitions: Vec::new(),
224        }
225    }
226
227    /// Binds an RPC that accepts typed input and returns typed output plus optional Step movements.
228    ///
229    /// Handler errors are returned to Dex and follow RPC failure semantics.
230    pub fn function<Input, Output>(
231        mut self,
232        definition: impl Into<RpcDefinition<Input, Output>>,
233        handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<RpcResult<Output>>,
234    ) -> Self
235    where
236        Input: Value,
237        Output: Value,
238    {
239        self.definitions.push(Box::new(FunctionBinder {
240            definition: definition.into(),
241            handler,
242        }));
243        self
244    }
245
246    /// Binds an RPC without input that returns typed output and optional Step movements.
247    pub fn function_without_input<Output>(
248        mut self,
249        definition: impl Into<RpcDefinition<(), Output>>,
250        handler: fn(&FlowType, &mut Context) -> HandlerResult<RpcResult<Output>>,
251    ) -> Self
252    where
253        Output: Value,
254    {
255        self.definitions.push(Box::new(FunctionWithoutInputBinder {
256            definition: definition.into(),
257            handler,
258        }));
259        self
260    }
261
262    /// Binds an RPC that accepts typed input and returns no output.
263    pub fn procedure<Input>(
264        mut self,
265        definition: impl Into<RpcDefinition<Input, ()>>,
266        handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<()>,
267    ) -> Self
268    where
269        Input: Value,
270    {
271        self.definitions.push(Box::new(ProcedureBinder {
272            definition: definition.into(),
273            handler,
274        }));
275        self
276    }
277
278    /// Binds an RPC with neither input nor output.
279    pub fn procedure_without_input(
280        mut self,
281        definition: impl Into<RpcDefinition<(), ()>>,
282        handler: fn(&FlowType, &mut Context) -> HandlerResult<()>,
283    ) -> Self {
284        self.definitions.push(Box::new(ProcedureWithoutInputBinder {
285            definition: definition.into(),
286            handler,
287        }));
288        self
289    }
290
291    pub(crate) fn bind(self, flow: Arc<FlowType>) -> Vec<RegisteredRpc> {
292        self.definitions
293            .into_iter()
294            .map(|definition| definition.bind(Arc::clone(&flow)))
295            .collect()
296    }
297}
298
299impl<FlowType> Default for RpcList<FlowType>
300where
301    FlowType: Send + Sync + 'static,
302{
303    fn default() -> Self {
304        Self::new()
305    }
306}
307
308/// Carries a typed RPC output and optional Step movements.
309///
310/// [`Self::then`] may be called repeatedly to schedule multiple active Steps after the RPC commits.
311pub struct RpcResult<Output> {
312    output: Output,
313    next_steps: Vec<StepMovement>,
314    cancel_step_types: Vec<&'static str>,
315}
316
317impl<Output: Value> RpcResult<Output> {
318    /// Creates a result with `output` and no Step movements.
319    pub fn new(output: Output) -> Self {
320        Self {
321            output,
322            next_steps: Vec::new(),
323            cancel_step_types: Vec::new(),
324        }
325    }
326
327    /// Appends one Step movement executed after the RPC succeeds.
328    pub fn then(mut self, movement: StepMovement) -> Self {
329        self.next_steps.push(movement);
330        self
331    }
332
333    /// Selects every queued or active execution of one registered Step type.
334    ///
335    /// Dex resolves cancellation after RPC persistence commits and before next Steps are queued.
336    /// Finished, already-canceled, and absent executions are no-ops. RPCs cannot select siblings.
337    pub fn cancel_step<SelectedStep>(mut self, step: &SelectedStep) -> Self
338    where
339        SelectedStep: crate::Step,
340    {
341        let step_type = step.step_type();
342        if !self.cancel_step_types.contains(&step_type) {
343            self.cancel_step_types.push(step_type);
344        }
345        self
346    }
347
348    /// Returns the typed output by reference.
349    pub fn output(&self) -> &Output {
350        &self.output
351    }
352
353    /// Returns the scheduled Step movements in insertion order.
354    pub fn next_steps(&self) -> &[StepMovement] {
355        &self.next_steps
356    }
357}
358
359pub(crate) struct ErasedRpcResult {
360    pub(crate) output: Box<dyn ErasedValue>,
361    pub(crate) next_steps: Vec<StepMovement>,
362    pub(crate) cancel_step_types: Vec<&'static str>,
363}
364
365#[derive(Clone)]
366pub(crate) struct RegisteredRpc {
367    pub(crate) name: &'static str,
368    pub(crate) timeout: Option<Duration>,
369    pub(crate) locks: Vec<AttributeLock>,
370    pub(crate) is_transactional: bool,
371    pub(crate) load_attribute_maps: Vec<AttributeMapLoad>,
372    pub(crate) load_channels: Vec<ChannelLoad>,
373    pub(crate) load_channel_maps: Vec<ChannelMapLoad>,
374    pub(crate) handler: Arc<dyn ErasedRpc>,
375}
376
377pub(crate) trait ErasedRpc: Send + Sync {
378    fn invoke(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<ErasedRpcResult>;
379}
380
381trait RpcBinder<FlowType>: Send + Sync {
382    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc;
383}
384
385struct FunctionBinder<FlowType, Input, Output> {
386    definition: RpcDefinition<Input, Output>,
387    handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<RpcResult<Output>>,
388}
389
390impl<FlowType, Input, Output> RpcBinder<FlowType> for FunctionBinder<FlowType, Input, Output>
391where
392    FlowType: Send + Sync + 'static,
393    Input: Value,
394    Output: Value,
395{
396    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc {
397        registered_rpc(
398            self.definition,
399            Arc::new(BoundFunction {
400                flow,
401                handler: self.handler,
402            }),
403        )
404    }
405}
406
407struct BoundFunction<FlowType, Input, Output> {
408    flow: Arc<FlowType>,
409    handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<RpcResult<Output>>,
410}
411
412impl<FlowType, Input, Output> ErasedRpc for BoundFunction<FlowType, Input, Output>
413where
414    FlowType: Send + Sync + 'static,
415    Input: Value,
416    Output: Value,
417{
418    fn invoke(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<ErasedRpcResult> {
419        let result = (self.handler)(&self.flow, context, value_mapper::decode_handler(input)?)?;
420        Ok(ErasedRpcResult {
421            output: Box::new(TypedValue(result.output)),
422            next_steps: result.next_steps,
423            cancel_step_types: result.cancel_step_types,
424        })
425    }
426}
427
428struct FunctionWithoutInputBinder<FlowType, Output> {
429    definition: RpcDefinition<(), Output>,
430    handler: fn(&FlowType, &mut Context) -> HandlerResult<RpcResult<Output>>,
431}
432
433impl<FlowType, Output> RpcBinder<FlowType> for FunctionWithoutInputBinder<FlowType, Output>
434where
435    FlowType: Send + Sync + 'static,
436    Output: Value,
437{
438    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc {
439        registered_rpc(
440            self.definition,
441            Arc::new(BoundFunctionWithoutInput {
442                flow,
443                handler: self.handler,
444            }),
445        )
446    }
447}
448
449struct BoundFunctionWithoutInput<FlowType, Output> {
450    flow: Arc<FlowType>,
451    handler: fn(&FlowType, &mut Context) -> HandlerResult<RpcResult<Output>>,
452}
453
454impl<FlowType, Output> ErasedRpc for BoundFunctionWithoutInput<FlowType, Output>
455where
456    FlowType: Send + Sync + 'static,
457    Output: Value,
458{
459    fn invoke(&self, context: &mut Context, _input: &ProtoValue) -> HandlerResult<ErasedRpcResult> {
460        let result = (self.handler)(&self.flow, context)?;
461        Ok(ErasedRpcResult {
462            output: Box::new(TypedValue(result.output)),
463            next_steps: result.next_steps,
464            cancel_step_types: result.cancel_step_types,
465        })
466    }
467}
468
469struct ProcedureBinder<FlowType, Input> {
470    definition: RpcDefinition<Input, ()>,
471    handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<()>,
472}
473
474impl<FlowType, Input> RpcBinder<FlowType> for ProcedureBinder<FlowType, Input>
475where
476    FlowType: Send + Sync + 'static,
477    Input: Value,
478{
479    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc {
480        registered_rpc(
481            self.definition,
482            Arc::new(BoundProcedure {
483                flow,
484                handler: self.handler,
485            }),
486        )
487    }
488}
489
490struct BoundProcedure<FlowType, Input> {
491    flow: Arc<FlowType>,
492    handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<()>,
493}
494
495impl<FlowType, Input> ErasedRpc for BoundProcedure<FlowType, Input>
496where
497    FlowType: Send + Sync + 'static,
498    Input: Value,
499{
500    fn invoke(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<ErasedRpcResult> {
501        (self.handler)(&self.flow, context, value_mapper::decode_handler(input)?)?;
502        Ok(empty_rpc_result())
503    }
504}
505
506struct ProcedureWithoutInputBinder<FlowType> {
507    definition: RpcDefinition<(), ()>,
508    handler: fn(&FlowType, &mut Context) -> HandlerResult<()>,
509}
510
511impl<FlowType> RpcBinder<FlowType> for ProcedureWithoutInputBinder<FlowType>
512where
513    FlowType: Send + Sync + 'static,
514{
515    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc {
516        registered_rpc(
517            self.definition,
518            Arc::new(BoundProcedureWithoutInput {
519                flow,
520                handler: self.handler,
521            }),
522        )
523    }
524}
525
526struct BoundProcedureWithoutInput<FlowType> {
527    flow: Arc<FlowType>,
528    handler: fn(&FlowType, &mut Context) -> HandlerResult<()>,
529}
530
531impl<FlowType> ErasedRpc for BoundProcedureWithoutInput<FlowType>
532where
533    FlowType: Send + Sync + 'static,
534{
535    fn invoke(&self, context: &mut Context, _input: &ProtoValue) -> HandlerResult<ErasedRpcResult> {
536        (self.handler)(&self.flow, context)?;
537        Ok(empty_rpc_result())
538    }
539}
540
541fn registered_rpc<Input, Output>(
542    definition: RpcDefinition<Input, Output>,
543    handler: Arc<dyn ErasedRpc>,
544) -> RegisteredRpc {
545    RegisteredRpc {
546        name: definition.rpc.name,
547        timeout: definition.timeout,
548        locks: definition.locks,
549        is_transactional: definition.is_transactional,
550        load_attribute_maps: definition.load_attribute_maps,
551        load_channels: definition.load_channels,
552        load_channel_maps: definition.load_channel_maps,
553        handler,
554    }
555}
556
557fn empty_rpc_result() -> ErasedRpcResult {
558    ErasedRpcResult {
559        output: Box::new(TypedValue(())),
560        next_steps: Vec::new(),
561        cancel_step_types: Vec::new(),
562    }
563}
564
565pub(crate) fn encode_rpc_output(output: &dyn ErasedValue) -> SdkResult<ProtoValue> {
566    output.encode()
567}