oxide_core 0.4.0

Rust engine primitives for Oxide (store, snapshot streams, error model, optional persistence).
Documentation
#![cfg(all(target_arch = "wasm32", target_os = "unknown"))]

use std::time::Duration;

use oxide_core::{CoreResult, InitContext, Reducer, ReducerEngine, StateChange};
use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

thread_local! {
    static THREAD_POOL: flutter_rust_bridge::SimpleThreadPool =
        flutter_rust_bridge::SimpleThreadPool::default();
}

fn thread_pool() -> &'static std::thread::LocalKey<flutter_rust_bridge::SimpleThreadPool> {
    &THREAD_POOL
}

#[cfg_attr(
    feature = "state-persistence",
    derive(oxide_core::serde::Serialize, oxide_core::serde::Deserialize)
)]
#[derive(Debug, Clone, PartialEq, Eq)]
struct CounterState {
    value: u64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum CounterAction {
    Inc,
}

enum CounterSideEffect {}

#[derive(Default)]
struct CounterReducer;

impl Reducer for CounterReducer {
    type State = CounterState;
    type Action = CounterAction;
    type SideEffect = CounterSideEffect;

    async fn init(&mut self, _ctx: InitContext<Self::SideEffect>) {}

    fn reduce(
        &mut self,
        state: &mut Self::State,
        ctx: oxide_core::Context<'_, Self::Action, Self::State, ()>,
    ) -> CoreResult<StateChange> {
        match ctx.input {
            CounterAction::Inc => state.value = state.value.saturating_add(1),
        }
        Ok(StateChange::Full)
    }

    fn effect(
        &mut self,
        _state: &mut Self::State,
        _ctx: oxide_core::Context<'_, Self::SideEffect, Self::State, ()>,
    ) -> CoreResult<StateChange> {
        Ok(StateChange::None)
    }
}

#[wasm_bindgen_test(async)]
async fn reducer_engine_dispatch_works_in_web_wasm() {
    let _ = oxide_core::runtime::init(thread_pool);

    let engine =
        ReducerEngine::<CounterReducer>::new(CounterReducer::default(), CounterState { value: 0 })
            .await
            .unwrap();
    let snapshot = engine.dispatch(CounterAction::Inc).await.unwrap();
    assert_eq!(snapshot.state.value, 1);
}

#[cfg(feature = "state-persistence")]
#[wasm_bindgen_test(async)]
async fn persistence_restores_state_in_web_wasm() {
    use oxide_core::persistence::PersistenceConfig;

    let _ = oxide_core::runtime::init(thread_pool);

    let key = format!("oxide_core.web_wasm.test.{}", js_sys::Date::now());
    if let Some(window) = web_sys::window() {
        if let Ok(Some(storage)) = window.local_storage() {
            let _ = storage.remove_item(&format!("oxide/{}.bin", key));
            let _ = storage.remove_item(&format!("oxide/{}.json", key));
        }
    }

    let config = PersistenceConfig {
        key: key.clone(),
        min_interval: Duration::from_millis(0),
    };

    let engine = ReducerEngine::<CounterReducer>::new_persistent(
        CounterReducer::default(),
        CounterState { value: 0 },
        config,
    )
    .await
    .unwrap();

    let _ = engine.dispatch(CounterAction::Inc).await.unwrap();
    gloo_timers::future::TimeoutFuture::new(10).await;

    let engine2 = ReducerEngine::<CounterReducer>::new_persistent(
        CounterReducer::default(),
        CounterState { value: 0 },
        PersistenceConfig {
            key,
            min_interval: Duration::from_millis(0),
        },
    )
    .await
    .unwrap();

    let current = engine2.current().await;
    assert_eq!(current.state.value, 1);
}