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}