Skip to main content

dex_sdk/
rpc.rs

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