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}