Skip to main content

fission_core/
context.rs

1//! Reducer context and effect builder.
2//!
3//! When a reducer needs to emit side-effects or inspect the [`ActionInput`]
4//! that triggered it, it receives a [`ReducerContext`]. The context provides
5//! an [`Effects`] builder for issuing system effects (HTTP, file I/O, alerts)
6//! and binding callback actions.
7
8use crate::action::{Action, ActionEnvelope, ActionId, AppState};
9use crate::effect::{ActionInput, Effect, EffectEnvelope, SystemEffect, EffectPayload};
10use crate::NodeId;
11use crate::registry::{ActionRegistry, IntoHandler};
12use std::collections::HashMap;
13use std::marker::PhantomData;
14use serde::Serialize;
15
16/// The context passed to modern 3-argument reducer handlers.
17///
18/// Provides access to the [`Effects`] builder (for emitting side-effects) and
19/// the [`ActionInput`] that accompanied the dispatch (e.g. effect results,
20/// pointer coordinates, drop payloads).
21///
22/// # Example
23///
24/// ```rust,ignore
25/// fn handle_click(
26///     state: &mut AppState,
27///     action: ClickAction,
28///     ctx: &mut ReducerContext<AppState>,
29/// ) {
30///     // Read pointer position from the input
31///     if let Some((x, y, _, _)) = ctx.input.as_pointer() {
32///         state.last_click = (x, y);
33///     }
34///     // Issue an HTTP GET effect
35///     ctx.effects.http_get("https://api.example.com/clicked");
36/// }
37/// ```
38pub struct ReducerContext<'a, 'b, 'c, S: AppState> {
39    /// Mutable reference to the effects builder.
40    pub effects: &'a mut Effects<'b, S>,
41    /// The input data that accompanied this action dispatch.
42    pub input: &'c ActionInput,
43}
44
45/// Builder for emitting side-effects from within a reducer.
46///
47/// `Effects` accumulates [`EffectEnvelope`] values that the runtime collects
48/// after the reducer returns. Each effect can carry optional `on_ok` and
49/// `on_err` callbacks.
50///
51/// # Example
52///
53/// ```rust,ignore
54/// fn handle_save(
55///     state: &mut MyState,
56///     _action: Save,
57///     ctx: &mut ReducerContext<MyState>,
58/// ) {
59///     ctx.effects.http_get("https://api.example.com/save")
60///         .on_ok(ctx.effects.bind(SaveOk, handle_save_ok as fn(&mut MyState, SaveOk)))
61///         .on_err(ctx.effects.bind(SaveErr, handle_save_err as fn(&mut MyState, SaveErr)));
62/// }
63/// ```
64pub struct Effects<'a, S: AppState> {
65    /// Accumulated effect envelopes, drained by the runtime after the reducer.
66    pub out: Vec<EffectEnvelope>,
67    next_req_id: u64,
68    pub(crate) registry: Option<&'a mut ActionRegistry<S>>,
69    _phantom: PhantomData<S>,
70}
71
72impl<'a, S: AppState> Effects<'a, S> {
73    pub fn new(next_req_id: u64, registry: &'a mut ActionRegistry<S>) -> Self {
74        Self {
75            out: Vec::new(),
76            next_req_id,
77            registry: Some(registry),
78            _phantom: PhantomData,
79        }
80    }
81
82    pub fn new_headless(next_req_id: u64) -> Self {
83        Self {
84            out: Vec::new(),
85            next_req_id,
86            registry: None,
87            _phantom: PhantomData,
88        }
89    }
90
91    pub fn bind<A: Action, H>(&mut self, action: A, handler: H) -> ActionEnvelope 
92    where H: IntoHandler<S, A> + Send + Sync + 'static 
93    {
94        if let Some(registry) = &mut self.registry {
95            registry.register(handler);
96        }
97        ActionEnvelope {
98            id: A::static_id(),
99            payload: action.encode(),
100        }
101    }
102
103    pub fn add(&mut self, effect: SystemEffect) -> u64 {
104        let req_id = self.next_req_id;
105        self.next_req_id += 1;
106        
107        self.out.push(EffectEnvelope {
108            req_id,
109            effect: Effect::System(effect),
110            on_ok: None,
111            on_err: None,
112        });
113        req_id
114    }
115
116    pub fn system_effect(&mut self, effect: SystemEffect) -> EffectBuilder<'_, 'a, S> {
117        let req_id = self.next_req_id;
118        self.next_req_id += 1;
119        
120        let index = self.out.len();
121        self.out.push(EffectEnvelope {
122            req_id,
123            effect: Effect::System(effect),
124            on_ok: None,
125            on_err: None,
126        });
127        
128        EffectBuilder {
129            effects: self,
130            index,
131        }
132    }
133
134    pub fn http_get(&mut self, url: impl Into<String>) -> EffectBuilder<'_, 'a, S> {
135        self.system_effect(SystemEffect::HttpGet { 
136            url: url.into(),
137            headers: HashMap::new() 
138        })
139    }
140
141    pub fn file_read(&mut self, path: impl Into<String>) -> EffectBuilder<'_, 'a, S> {
142        self.system_effect(SystemEffect::FileRead { 
143            path: path.into()
144        })
145    }
146
147    pub fn cancel(&mut self, req_id: u64) {
148        self.system_effect(SystemEffect::Cancel { req_id });
149    }
150
151    pub fn release_resource(&mut self, resource_id: u64) {
152        self.system_effect(SystemEffect::ReleaseResource { resource_id });
153    }
154}
155
156/// Fluent builder returned by [`Effects::system_effect`], [`Effects::http_get`],
157/// and [`Effects::file_read`].
158///
159/// Attach `on_ok` and `on_err` callback envelopes before the builder is dropped.
160///
161/// # Example
162///
163/// ```rust,ignore
164/// ctx.effects.http_get("https://api.example.com")
165///     .on_ok(ok_envelope)
166///     .on_err(err_envelope)
167///     .dispatch(); // optional -- dropping also finalises
168/// ```
169pub struct EffectBuilder<'a, 'b, S: AppState> {
170    effects: &'a mut Effects<'b, S>,
171    index: usize,
172}
173
174impl<'a, 'b, S: AppState> EffectBuilder<'a, 'b, S> {
175    pub fn on_ok(self, action: ActionEnvelope) -> Self {
176        self.effects.out[self.index].on_ok = Some(action);
177        self
178    }
179
180    pub fn on_err(self, action: ActionEnvelope) -> Self {
181        self.effects.out[self.index].on_err = Some(action);
182        self
183    }
184
185    pub fn dispatch(self) {
186        // Drop
187    }
188}