Skip to main content

fission_core/
effect.rs

1//! Side-effect primitives for async operations.
2//!
3//! Reducers are pure functions -- they must not perform I/O. When a reducer
4//! needs to trigger an HTTP request, read a file, or show a system alert, it
5//! pushes an [`EffectEnvelope`] through the [`Effects`](crate::Effects) builder.
6//! The platform executor fulfils the effect outside the deterministic core and
7//! dispatches the `on_ok` / `on_err` callback actions back into the pipeline.
8
9use serde::{Deserialize, Serialize};
10use crate::action::ActionEnvelope;
11
12/// An opaque request identifier assigned to each emitted effect.
13///
14/// The platform executor returns this id when delivering the result so the
15/// runtime can correlate responses.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17pub struct ReqId(pub u64);
18
19/// An opaque handle to a platform-managed resource (e.g. a large binary blob).
20///
21/// Resources live outside the action pipeline to avoid copying large payloads.
22/// Use [`SystemEffect::ReleaseResource`] to free them when no longer needed.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct ResourceId(pub u64);
25
26use std::collections::HashMap;
27
28/// Built-in system effects that every platform executor must handle.
29///
30/// These cover the most common async operations an application needs.
31/// For app-specific effects, use [`Effect::App`] with an opaque byte payload.
32///
33/// # Example
34///
35/// ```rust,ignore
36/// fn fetch_data(state: &mut MyState, _action: FetchTodos, ctx: &mut ReducerContext<MyState>) {
37///     ctx.effects.http_get("https://api.example.com/todos")
38///         .on_ok(ctx.effects.bind(TodosLoaded, handle_loaded as fn(&mut MyState, TodosLoaded)));
39/// }
40/// ```
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum SystemEffect {
43    /// Show a native alert dialog with a title and message.
44    Alert {
45        title: String,
46        message: String,
47    },
48    /// Perform an HTTP GET request.
49    HttpGet {
50        url: String,
51        headers: HashMap<String, String>,
52    },
53    /// Read a file from the local filesystem.
54    FileRead {
55        path: String,
56    },
57    /// Cancel a previously issued effect by its request id.
58    Cancel {
59        req_id: u64,
60    },
61    /// Release a platform-managed resource.
62    ReleaseResource {
63        resource_id: u64,
64    },
65    /// Open a URL in the system browser or an in-app browser sheet.
66    ///
67    /// When `in_app` is `true`, the URL opens in a Custom Tab /
68    /// SFSafariViewController overlay. When `false`, the URL opens in the
69    /// external browser app.
70    OpenUrl {
71        url: String,
72        in_app: bool,
73    },
74    /// Initiate an OAuth / secure authentication session.
75    ///
76    /// The platform opens the `url` and listens for a redirect matching
77    /// `callback_scheme`. The redirect URL is delivered as the effect result.
78    Authenticate {
79        url: String,
80        callback_scheme: String,
81    },
82}
83
84/// A side-effect emitted by a reducer.
85///
86/// `System` variants are handled by the platform executor.
87/// `App` carries an opaque byte payload for application-defined effects
88/// (e.g. database writes, Bluetooth commands).
89#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
90pub enum Effect {
91    /// A built-in system effect (HTTP, file I/O, alerts, etc.).
92    System(SystemEffect),
93    /// An application-defined effect with an opaque byte payload.
94    App(Vec<u8>),
95}
96
97/// A queued effect with optional success/failure callbacks.
98///
99/// The platform executor processes the [`Effect`], then dispatches either
100/// `on_ok` or `on_err` back into the runtime. The `req_id` is assigned
101/// automatically by the runtime and is globally unique within a session.
102///
103/// # Example
104///
105/// ```rust,ignore
106/// // Built via the Effects builder -- you rarely construct this manually.
107/// ctx.effects.http_get("https://example.com/api")
108///     .on_ok(ok_envelope)
109///     .on_err(err_envelope);
110/// ```
111#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
112pub struct EffectEnvelope {
113    /// Unique request identifier (assigned by the runtime).
114    pub req_id: u64,
115    /// The effect to execute.
116    pub effect: Effect,
117    /// Action dispatched when the effect completes successfully.
118    pub on_ok: Option<ActionEnvelope>,
119    /// Action dispatched when the effect fails.
120    pub on_err: Option<ActionEnvelope>,
121}
122
123/// The payload delivered when an effect completes.
124///
125/// Small results are inlined as bytes; large results reference a
126/// platform-managed [`ResourceId`].
127#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
128pub enum EffectPayload {
129    /// The result data, serialised inline.
130    InlineBytes(Vec<u8>),
131    /// A handle to a platform-managed resource (avoids copying large blobs).
132    Resource(u64),
133    /// The effect produced no result data.
134    Empty,
135}
136
137/// Extra input data passed alongside an action dispatch.
138///
139/// When the platform delivers an effect result or a drag-and-drop event, it
140/// attaches an `ActionInput` so the reducer can access the associated data
141/// without encoding it in the action payload.
142///
143/// # Example
144///
145/// ```rust,ignore
146/// fn on_file_loaded(
147///     state: &mut MyState,
148///     _action: FileLoaded,
149///     ctx: &mut ReducerContext<MyState>,
150/// ) {
151///     if let Some(bytes) = ctx.input.as_bytes() {
152///         state.file_contents = String::from_utf8_lossy(bytes).into_owned();
153///     }
154/// }
155/// ```
156#[derive(Clone, Debug, PartialEq)]
157pub enum ActionInput {
158    /// No extra input.
159    None,
160    /// The effect completed successfully.
161    EffectOk { req_id: u64, payload: EffectPayload },
162    /// The effect failed with an error message.
163    EffectErr { req_id: u64, message: String },
164    /// Pointer coordinates and deltas (used by drag/gesture handlers).
165    Pointer { x: f32, y: f32, delta_x: f32, delta_y: f32 },
166    /// External file drop (e.g. from the OS file manager).
167    Drop { paths: Vec<String>, x: f32, y: f32 },
168    /// Internal drag-and-drop with an opaque byte payload.
169    InternalDrop { payload: Vec<u8>, x: f32, y: f32 },
170}
171
172impl ActionInput {
173    pub fn as_bytes(&self) -> Option<&[u8]> {
174        match self {
175            ActionInput::EffectOk { payload: EffectPayload::InlineBytes(b), .. } => Some(b),
176            ActionInput::InternalDrop { payload, .. } => Some(payload),
177            _ => None,
178        }
179    }
180    
181    pub fn as_pointer(&self) -> Option<(f32, f32, f32, f32)> {
182        match self {
183            ActionInput::Pointer { x, y, delta_x, delta_y } => Some((*x, *y, *delta_x, *delta_y)),
184            ActionInput::Drop { x, y, .. } => Some((*x, *y, 0.0, 0.0)),
185            ActionInput::InternalDrop { x, y, .. } => Some((*x, *y, 0.0, 0.0)),
186            _ => None,
187        }
188    }
189    
190    pub fn as_drop_paths(&self) -> Option<&[String]> {
191        match self {
192            ActionInput::Drop { paths, .. } => Some(paths),
193            _ => None,
194        }
195    }
196
197    pub fn as_internal_drop(&self) -> Option<&[u8]> {
198        match self {
199            ActionInput::InternalDrop { payload, .. } => Some(payload),
200            _ => None,
201        }
202    }
203}