rearch_effects/
lib.rs

1use rearch::{CData, SideEffect, SideEffectRegistrar};
2use std::sync::Arc;
3
4mod state_transformers;
5pub use state_transformers::*;
6
7mod multi;
8pub use multi::*;
9
10mod overridable_capsule;
11pub use overridable_capsule::{overridable_capsule, OverridableCapsule};
12
13mod effect_lifetime_fixers;
14use effect_lifetime_fixers::{EffectLifetimeFixer0, EffectLifetimeFixer1, EffectLifetimeFixer2};
15
16/// A way to re-use the same exact side effect code while providing a different [`SideEffect::Api`]
17/// based on the data you have and the data you want in return (such as a [`Clone`] or a ref).
18///
19/// See the implementors of this trait of the various state transformers you can use.
20pub trait StateTransformer: Send + 'static {
21    type Input;
22    fn from_input(input: Self::Input) -> Self;
23
24    type Inner;
25    fn as_inner(&mut self) -> &mut Self::Inner;
26
27    type Output<'a>;
28    fn as_output(&mut self) -> Self::Output<'_>;
29}
30
31type SideEffectMutation<'f, ST> = Box<dyn 'f + FnOnce(&mut <ST as StateTransformer>::Inner)>;
32
33/// A no-op side effect that specifies non-idempotence.
34///
35/// Useful so that a capsule can be treated as a listener/will not get
36/// idempotent garbage collected from a container.
37// NOTE: returns (), the no-op side effect
38#[must_use]
39pub fn as_listener() -> impl for<'a> SideEffect<Api<'a> = ()> {}
40
41/// Analogous to [`SideEffectRegistrar::raw`], but uses a [`StateTransformer`] to specify the api.
42pub fn raw<ST: StateTransformer>(
43    initial: ST::Input,
44) -> impl for<'a> SideEffect<
45    Api<'a> = (
46        ST::Output<'a>,
47        impl CData + for<'f> Fn(Box<dyn 'f + FnOnce(&mut ST::Inner)>),
48        Arc<dyn Send + Sync + for<'f> Fn(Box<dyn 'f + FnOnce()>)>,
49    ),
50> {
51    EffectLifetimeFixer2::<_, ST>::new(move |register: SideEffectRegistrar| {
52        let (transformer, run_mutation, run_txn) = register.raw(ST::from_input(initial));
53        (
54            transformer.as_output(),
55            move |mutation: SideEffectMutation<ST>| {
56                run_mutation(Box::new(move |st| mutation(st.as_inner())));
57            },
58            run_txn,
59        )
60    })
61}
62
63/// Similar to `useState` from React hooks.
64/// Provides a copy of some state and a way to set that state via a callback.
65pub fn state<ST: StateTransformer>(
66    initial: ST::Input,
67) -> impl for<'a> SideEffect<Api<'a> = (ST::Output<'a>, impl CData + Fn(ST::Inner))> {
68    EffectLifetimeFixer1::<_, ST>::new(move |register: SideEffectRegistrar| {
69        let (state, rebuild, _) = register.register(raw::<ST>(initial));
70        let set_state = move |new_state| {
71            rebuild(Box::new(|state| *state = new_state));
72        };
73        (state, set_state)
74    })
75}
76
77/// Provides the same given value across builds.
78pub fn value<ST: StateTransformer>(
79    value: ST::Input,
80) -> impl for<'a> SideEffect<Api<'a> = ST::Output<'a>> {
81    EffectLifetimeFixer0::<_, ST>::new(move |register: SideEffectRegistrar| {
82        register.register(raw::<ST>(value)).0
83    })
84}
85
86/// Provides whether or not this is the first build being called.
87#[must_use]
88pub fn is_first_build() -> impl for<'a> SideEffect<Api<'a> = bool> {
89    |register: SideEffectRegistrar| {
90        let has_built_before = register.register(value::<MutRef<_>>(false));
91        let is_first_build = !*has_built_before;
92        *has_built_before = true;
93        is_first_build
94    }
95}
96
97/// Models the state reducer pattern via side effects (similar to `useReducer` from React hooks).
98///
99/// This should normally *not* be used with [`MutRef`].
100pub fn reducer<ST: StateTransformer, Action, Reducer>(
101    initial: ST::Input,
102    reducer: Reducer,
103) -> impl for<'a> SideEffect<Api<'a> = (ST::Output<'a>, impl CData + Fn(Action))>
104where
105    Action: 'static,
106    Reducer: Clone + Send + Sync + 'static + Fn(&ST::Inner, Action) -> ST::Inner,
107{
108    EffectLifetimeFixer1::<_, ST>::new(move |register: SideEffectRegistrar| {
109        let (state, update_state, _) = register.register(raw::<ST>(initial));
110        (state, move |action| {
111            update_state(Box::new(|state| *state = reducer(state, action)));
112        })
113    })
114}
115
116// NOTE: Commented out because I think people should really be using a hydrate equivalent
117// instead of this. Probably value::<LazyMutRef<_>>() and run_on_change?
118//
119// /// A thin wrapper around the state side effect that enables easy state persistence.
120// ///
121// /// You provide a `read` function and a `write` function,
122// /// and you receive the status of the latest read/write operation,
123// /// in addition to a persist function that persists new state and triggers rebuilds.
124// ///
125// /// Note: when possible, it is highly recommended to use async persist instead of sync persist.
126// /// This effect is blocking, which will prevent other capsule updates.
127// /// However, this function is perfect for quick I/O, like when using something similar to redb.
128// pub fn persist<Read, Write, R, T>(
129//     read: Read,
130//     write: Write,
131// ) -> impl for<'a> SideEffect<Api<'a> = (&'a R, impl CData + Fn(T))>
132// where
133//     T: Send + 'static,
134//     R: Send + 'static,
135//     Read: Send + 'static + FnOnce() -> R,
136//     Write: Clone + Send + Sync + 'static + Fn(T) -> R,
137// {
138//     EffectLifetimeFixer1::new(move |register: SideEffectRegistrar| {
139//         let (state, set_state) = register.register(lazy_state(read));
140//         let persist = move |new_data| set_state(write(new_data));
141//         (&*state, persist)
142//     })
143// }
144
145// NOTE: Commented out because this currently fails to compile due to the
146// higher kinded lifetime bound on the nested opaque type (Api<'a> = impl Trait + 'a)
147/*
148/// Side effect that runs a callback whenever it changes and is dropped.
149/// Similar to `useEffect` from React.
150#[must_use]
151pub fn run_on_change<F>() -> impl for<'a> SideEffect<Api<'a> = impl FnMut(F) + 'a>
152where
153    F: FnOnce() + Send + 'static,
154{
155    move |register: SideEffectRegistrar| {
156        let state = register.register(value(FunctionalDrop(None)));
157        // The old callback, if there is one, will be called when it is dropped,
158        // via the `*state = ...` assignment below
159        |callback| *state = FunctionalDrop(Some(callback))
160    }
161}
162struct FunctionalDrop<F: FnOnce()>(Option<F>);
163impl<F: FnOnce()> Drop for FunctionalDrop<F> {
164    fn drop(&mut self) {
165        if let Some(callback) = std::mem::take(&mut self.0) {
166            callback();
167        }
168    }
169}
170#[must_use]
171pub fn run_on_change2<F>() -> RunOnChange<F>
172where
173    F: FnOnce() + Send + 'static,
174{
175    RunOnChange(std::marker::PhantomData)
176}
177pub struct RunOnChange<F>(std::marker::PhantomData<F>);
178impl<F: Send + FnOnce() + 'static> SideEffect for RunOnChange<F> {
179    type Api<'registrar> = impl FnMut(F) + 'registrar;
180
181    fn build(self, registrar: SideEffectRegistrar) -> Self::Api<'_> {
182        let state = registrar.register(value(FunctionalDrop(None)));
183        // The old callback, if there is one, will be called when it is dropped,
184        // via the `*state = ...` assignment below
185        |callback| *state = FunctionalDrop(Some(callback))
186    }
187}
188*/
189
190#[cfg(test)]
191mod tests {
192    use crate::*;
193    use rearch::{CapsuleHandle, Container};
194    use std::sync::atomic::{AtomicU8, Ordering};
195
196    // NOTE: raw side effect is effectively tested via combination of the other side effects
197
198    #[allow(clippy::needless_pass_by_value)]
199    fn assert_type<Expected>(_actual: Expected) {}
200
201    #[test]
202    fn transformer_output_types() {
203        fn dummy_capsule(CapsuleHandle { register, .. }: CapsuleHandle) {
204            let ((r, _, _), (mr, _, _), (c, _, _)) = register.register((
205                raw::<Ref<u8>>(123),
206                raw::<MutRef<u8>>(123),
207                raw::<Cloned<u8>>(123),
208            ));
209            assert_type::<&u8>(r);
210            assert_type::<&mut u8>(mr);
211            assert_type::<u8>(c);
212        }
213        Container::new().read(dummy_capsule);
214    }
215
216    #[test]
217    fn lazy_transformer_output_types() {
218        fn dummy_capsule(CapsuleHandle { register, .. }: CapsuleHandle) {
219            let ((r, _, _), (mr, _, _), (c, _, _)) = register.register((
220                raw::<LazyRef<_>>(|| 123),
221                raw::<LazyMutRef<_>>(|| 123),
222                raw::<LazyCloned<_>>(|| 123),
223            ));
224            assert_type::<&u8>(r);
225            assert_type::<&mut u8>(mr);
226            assert_type::<u8>(c);
227        }
228        Container::new().read(dummy_capsule);
229    }
230
231    #[test]
232    fn lazy_transformer_invokes_init_fn() {
233        fn lazy_transformer_capsule(CapsuleHandle { register, .. }: CapsuleHandle) -> u8 {
234            register.register(value::<LazyCloned<_>>(|| 123))
235        }
236        assert_eq!(Container::new().read(lazy_transformer_capsule), 123);
237    }
238
239    #[test]
240    fn as_listener_gets_changes() {
241        static BUILD_COUNT: AtomicU8 = AtomicU8::new(0);
242
243        fn rebuildable_capsule(CapsuleHandle { register, .. }: CapsuleHandle) -> impl CData + Fn() {
244            let ((), rebuild, _) = register.raw(());
245            move || rebuild(Box::new(|()| {}))
246        }
247
248        fn listener_capsule(CapsuleHandle { mut get, register }: CapsuleHandle) {
249            register.register(as_listener());
250            BUILD_COUNT.fetch_add(1, Ordering::SeqCst);
251            get.as_ref(rebuildable_capsule);
252        }
253
254        let container = Container::new();
255        container.read(listener_capsule);
256        container.read(rebuildable_capsule)();
257        assert_eq!(BUILD_COUNT.fetch_add(1, Ordering::SeqCst), 2);
258    }
259
260    #[test]
261    fn state_can_change() {
262        fn stateful_capsule(
263            CapsuleHandle { register, .. }: CapsuleHandle,
264        ) -> (u8, impl CData + Fn(u8)) {
265            register.register(state::<Cloned<_>>(0))
266        }
267
268        let container = Container::new();
269        assert_eq!(container.read(stateful_capsule).0, 0);
270        container.read(stateful_capsule).1(1);
271        assert_eq!(container.read(stateful_capsule).0, 1);
272    }
273
274    #[test]
275    fn value_can_change() {
276        fn rebuildable_capsule(CapsuleHandle { register, .. }: CapsuleHandle) -> impl CData + Fn() {
277            let ((), rebuild, _) = register.raw(());
278            move || rebuild(Box::new(|()| {}))
279        }
280
281        fn build_count_capsule(CapsuleHandle { mut get, register }: CapsuleHandle) -> u8 {
282            get.as_ref(rebuildable_capsule);
283            let build_count = register.register(value::<MutRef<_>>(0));
284            *build_count += 1;
285            *build_count
286        }
287
288        let container = Container::new();
289        assert_eq!(container.read(build_count_capsule), 1);
290        container.read(rebuildable_capsule)();
291        assert_eq!(container.read(build_count_capsule), 2);
292        container.read(rebuildable_capsule)();
293        assert_eq!(container.read(build_count_capsule), 3);
294    }
295
296    #[test]
297    fn is_first_build_changes_state() {
298        fn is_first_build_capsule(
299            CapsuleHandle { register, .. }: CapsuleHandle,
300        ) -> (bool, impl CData + Fn()) {
301            let (is_first_build, ((), rebuild, _)) =
302                register.register((is_first_build(), raw::<MutRef<_>>(())));
303            (is_first_build, move || rebuild(Box::new(|()| {})))
304        }
305
306        let container = Container::new();
307        assert!(container.read(is_first_build_capsule).0);
308        container.read(is_first_build_capsule).1();
309        assert!(!container.read(is_first_build_capsule).0);
310        container.read(is_first_build_capsule).1();
311        assert!(!container.read(is_first_build_capsule).0);
312    }
313
314    #[test]
315    fn reducer_can_change() {
316        enum CountAction {
317            Increment,
318            Decrement,
319        }
320
321        fn count_manager(
322            CapsuleHandle { register, .. }: CapsuleHandle,
323        ) -> (u8, impl CData + Fn(CountAction)) {
324            register.register(reducer::<Cloned<_>, _, _>(
325                0,
326                |state, action| match action {
327                    CountAction::Increment => state + 1,
328                    CountAction::Decrement => state - 1,
329                },
330            ))
331        }
332
333        let container = Container::new();
334        assert_eq!(container.read(count_manager).0, 0);
335        container.read(count_manager).1(CountAction::Increment);
336        assert_eq!(container.read(count_manager).0, 1);
337        container.read(count_manager).1(CountAction::Decrement);
338        assert_eq!(container.read(count_manager).0, 0);
339    }
340}