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}
226
227impl<Output: Value> RpcResult<Output> {
228    /// Creates a result with `output` and no Step movements.
229    pub fn new(output: Output) -> Self {
230        Self {
231            output,
232            next_steps: Vec::new(),
233        }
234    }
235
236    /// Appends one Step movement executed after the RPC succeeds.
237    pub fn then(mut self, movement: StepMovement) -> Self {
238        self.next_steps.push(movement);
239        self
240    }
241
242    /// Returns the typed output by reference.
243    pub fn output(&self) -> &Output {
244        &self.output
245    }
246
247    /// Returns the scheduled Step movements in insertion order.
248    pub fn next_steps(&self) -> &[StepMovement] {
249        &self.next_steps
250    }
251}
252
253pub(crate) struct ErasedRpcResult {
254    pub(crate) output: Box<dyn ErasedValue>,
255    pub(crate) next_steps: Vec<StepMovement>,
256}
257
258#[derive(Clone)]
259pub(crate) struct RegisteredRpc {
260    pub(crate) name: &'static str,
261    pub(crate) timeout: Option<Duration>,
262    pub(crate) locks: Vec<AttributeLock>,
263    pub(crate) handler: Arc<dyn ErasedRpc>,
264}
265
266pub(crate) trait ErasedRpc: Send + Sync {
267    fn invoke(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<ErasedRpcResult>;
268}
269
270trait RpcBinder<FlowType>: Send + Sync {
271    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc;
272}
273
274struct FunctionBinder<FlowType, Input, Output> {
275    definition: RpcDefinition<Input, Output>,
276    handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<RpcResult<Output>>,
277}
278
279impl<FlowType, Input, Output> RpcBinder<FlowType> for FunctionBinder<FlowType, Input, Output>
280where
281    FlowType: Send + Sync + 'static,
282    Input: Value,
283    Output: Value,
284{
285    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc {
286        registered_rpc(
287            self.definition,
288            Arc::new(BoundFunction {
289                flow,
290                handler: self.handler,
291            }),
292        )
293    }
294}
295
296struct BoundFunction<FlowType, Input, Output> {
297    flow: Arc<FlowType>,
298    handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<RpcResult<Output>>,
299}
300
301impl<FlowType, Input, Output> ErasedRpc for BoundFunction<FlowType, Input, Output>
302where
303    FlowType: Send + Sync + 'static,
304    Input: Value,
305    Output: Value,
306{
307    fn invoke(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<ErasedRpcResult> {
308        let result = (self.handler)(&self.flow, context, value_mapper::decode_handler(input)?)?;
309        Ok(ErasedRpcResult {
310            output: Box::new(TypedValue(result.output)),
311            next_steps: result.next_steps,
312        })
313    }
314}
315
316struct FunctionWithoutInputBinder<FlowType, Output> {
317    definition: RpcDefinition<(), Output>,
318    handler: fn(&FlowType, &mut Context) -> HandlerResult<RpcResult<Output>>,
319}
320
321impl<FlowType, Output> RpcBinder<FlowType> for FunctionWithoutInputBinder<FlowType, Output>
322where
323    FlowType: Send + Sync + 'static,
324    Output: Value,
325{
326    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc {
327        registered_rpc(
328            self.definition,
329            Arc::new(BoundFunctionWithoutInput {
330                flow,
331                handler: self.handler,
332            }),
333        )
334    }
335}
336
337struct BoundFunctionWithoutInput<FlowType, Output> {
338    flow: Arc<FlowType>,
339    handler: fn(&FlowType, &mut Context) -> HandlerResult<RpcResult<Output>>,
340}
341
342impl<FlowType, Output> ErasedRpc for BoundFunctionWithoutInput<FlowType, Output>
343where
344    FlowType: Send + Sync + 'static,
345    Output: Value,
346{
347    fn invoke(&self, context: &mut Context, _input: &ProtoValue) -> HandlerResult<ErasedRpcResult> {
348        let result = (self.handler)(&self.flow, context)?;
349        Ok(ErasedRpcResult {
350            output: Box::new(TypedValue(result.output)),
351            next_steps: result.next_steps,
352        })
353    }
354}
355
356struct ProcedureBinder<FlowType, Input> {
357    definition: RpcDefinition<Input, ()>,
358    handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<()>,
359}
360
361impl<FlowType, Input> RpcBinder<FlowType> for ProcedureBinder<FlowType, Input>
362where
363    FlowType: Send + Sync + 'static,
364    Input: Value,
365{
366    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc {
367        registered_rpc(
368            self.definition,
369            Arc::new(BoundProcedure {
370                flow,
371                handler: self.handler,
372            }),
373        )
374    }
375}
376
377struct BoundProcedure<FlowType, Input> {
378    flow: Arc<FlowType>,
379    handler: fn(&FlowType, &mut Context, Input) -> HandlerResult<()>,
380}
381
382impl<FlowType, Input> ErasedRpc for BoundProcedure<FlowType, Input>
383where
384    FlowType: Send + Sync + 'static,
385    Input: Value,
386{
387    fn invoke(&self, context: &mut Context, input: &ProtoValue) -> HandlerResult<ErasedRpcResult> {
388        (self.handler)(&self.flow, context, value_mapper::decode_handler(input)?)?;
389        Ok(empty_rpc_result())
390    }
391}
392
393struct ProcedureWithoutInputBinder<FlowType> {
394    definition: RpcDefinition<(), ()>,
395    handler: fn(&FlowType, &mut Context) -> HandlerResult<()>,
396}
397
398impl<FlowType> RpcBinder<FlowType> for ProcedureWithoutInputBinder<FlowType>
399where
400    FlowType: Send + Sync + 'static,
401{
402    fn bind(self: Box<Self>, flow: Arc<FlowType>) -> RegisteredRpc {
403        registered_rpc(
404            self.definition,
405            Arc::new(BoundProcedureWithoutInput {
406                flow,
407                handler: self.handler,
408            }),
409        )
410    }
411}
412
413struct BoundProcedureWithoutInput<FlowType> {
414    flow: Arc<FlowType>,
415    handler: fn(&FlowType, &mut Context) -> HandlerResult<()>,
416}
417
418impl<FlowType> ErasedRpc for BoundProcedureWithoutInput<FlowType>
419where
420    FlowType: Send + Sync + 'static,
421{
422    fn invoke(&self, context: &mut Context, _input: &ProtoValue) -> HandlerResult<ErasedRpcResult> {
423        (self.handler)(&self.flow, context)?;
424        Ok(empty_rpc_result())
425    }
426}
427
428fn registered_rpc<Input, Output>(
429    definition: RpcDefinition<Input, Output>,
430    handler: Arc<dyn ErasedRpc>,
431) -> RegisteredRpc {
432    RegisteredRpc {
433        name: definition.rpc.name,
434        timeout: definition.timeout,
435        locks: definition.locks,
436        handler,
437    }
438}
439
440fn empty_rpc_result() -> ErasedRpcResult {
441    ErasedRpcResult {
442        output: Box::new(TypedValue(())),
443        next_steps: Vec::new(),
444    }
445}
446
447pub(crate) fn encode_rpc_output(output: &dyn ErasedValue) -> SdkResult<ProtoValue> {
448    output.encode()
449}