Expand description
§oxide_core
oxide_core is the Rust-side engine for Oxide. It provides the primitives for:
- Defining reducers (
Reducer) - Owning state in an engine (
ReducerEngine) - Streaming revisioned snapshots (
StateSnapshot<T>) - Optional persistence helpers (feature-gated)
This crate is intentionally usage-agnostic. For end-to-end Rust ↔ Flutter wiring, see the repository examples and the root README.
§Add It To Your Crate
In your Cargo.toml:
[dependencies]
oxide_core = "0.2.0"When working inside this repository, use a combined version + path dependency (Cargo prefers path locally, while published crates resolve by version):
oxide_core = { version = "0.2.0", path = "../rust/oxide_core" }§Core Concepts
§Reducer
Reducers are stateful controllers that mutate state in response to actions and side-effects:
use oxide_core::{CoreResult, Reducer, StateChange};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CounterState {
pub value: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CounterAction {
Inc,
}
pub enum CounterSideEffect {}
#[derive(Default)]
pub struct CounterReducer;
impl Reducer for CounterReducer {
type State = CounterState;
type Action = CounterAction;
type SideEffect = CounterSideEffect;
async fn init(&mut self, _ctx: oxide_core::InitContext<Self::SideEffect>) {}
fn reduce(&mut self, state: &mut Self::State, action: Self::Action) -> CoreResult<StateChange> {
match action {
CounterAction::Inc => state.value = state.value.saturating_add(1),
}
Ok(StateChange::Full)
}
fn effect(
&mut self,
_state: &mut Self::State,
_effect: Self::SideEffect,
) -> CoreResult<StateChange> {
Ok(StateChange::None)
}
}§ReducerEngine
ReducerEngine<R> is the async-safe facade for dispatching actions and observing snapshots:
use oxide_core::ReducerEngine;
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
fn thread_pool() -> &'static flutter_rust_bridge::SimpleThreadPool {
static POOL: std::sync::OnceLock<flutter_rust_bridge::SimpleThreadPool> = std::sync::OnceLock::new();
POOL.get_or_init(flutter_rust_bridge::SimpleThreadPool::default)
}
let _ = oxide_core::runtime::init(thread_pool);
let engine = ReducerEngine::<CounterReducer>::new(CounterReducer::default(), CounterState { value: 0 })
.await
.unwrap();
let snap = engine.dispatch(CounterAction::Inc).await.unwrap();
let current = engine.current().await;
assert_eq!(snap.revision, current.revision);
});§Async Runtime Behavior
ReducerEngine starts an internal async loop to process side-effects. Engine creation is safe even when there is no ambient Tokio runtime (for example, when entered from a synchronous FFI boundary):
- If a Tokio runtime is currently available, Oxide spawns tasks onto it.
- On native targets, if no runtime is available and
internal-runtimeis enabled (default), Oxide falls back to an internal global Tokio runtime for background work. - On web WASM (
wasm32-unknown-unknown), Oxide useswasm-bindgen-futuresto spawn background work without requiring a Tokio runtime.
In a normal Rust binary, the simplest approach is to run inside a Tokio runtime:
use oxide_core::ReducerEngine;
#[tokio::main(flavor = "multi_thread")]
async fn main() {
let engine = ReducerEngine::<CounterReducer>::new(CounterReducer::default(), CounterState { value: 0 });
let _ = engine.dispatch(CounterAction::Inc).await.unwrap();
}§Invariant: No Partial Mutation On Error
Dispatch uses clone-first semantics:
- The current state is cloned.
- The reducer runs against the clone.
- If the reducer returns
StateChange::None, the clone is discarded and no snapshot is emitted. - If the reducer returns an error, the live state is not updated and no snapshot is emitted.
This makes it safe to write reducers as normal &mut State code while preserving strong error semantics.
§Sliced Updates (Optional)
Sliced updates allow snapshots to carry lightweight metadata describing which top-level parts of state changed, so binding layers (like Flutter) can avoid rebuilding when irrelevant fields update.
- Opt-in: slicing is only meaningful when the state type implements
SlicedState(typically generated byoxide_generator_rsvia#[state(sliced = true)]). - Reducer signaling:
- Return
StateChange::Inferto ask the engine to callReducer::infer_slices(before, after). - Return
StateChange::Slices(&[...])to explicitly declare which slices changed. - Return
StateChange::Fullfor a full update.
- Return
- Snapshot semantics:
snapshot.stateis always the full state.snapshot.slices.is_empty()is treated as a full update.
If you are not using sliced updates, you can ignore snapshot.slices (it will remain empty).
§Feature Flags
frb-spawn(default): enables FRB’sspawnhelper for cross-platform task spawningstate-persistence: enables bincode encode/decode helpers inoxide_core::persistencepersistence-json: adds JSON encode/decode helpers (requiresstate-persistence)full: enables all persistence featuresinternal-runtime(default): enables a global Tokio runtime fallback on native targets
§Web / WASM Support
oxide_core supports compiling for WebAssembly. The supported targets differ in what persistence backend is available:
wasm32-unknown-unknown(web): background tasks are spawned withwasm-bindgen-futures. Whenstate-persistenceis enabled, persisted snapshots are stored inlocalStorageusing a stable key derived fromPersistenceConfig.key.wasm32-wasip1(WASI): compilation is supported. Whenstate-persistenceis enabled, snapshots are persisted to the filesystem.
§Build Commands
From rust/:
rustup target add wasm32-unknown-unknown wasm32-wasip1
cargo check -p oxide_core --target wasm32-unknown-unknown --all-features
cargo check -p oxide_core --target wasm32-wasip1 --all-features§WASM Tests
oxide_core includes WASM-focused regression tests:
wasm_web_compat(web): validatesReducerEnginedispatch in browser WASM and (when enabled) persistence restore usinglocalStorage.wasm_wasi_compat(WASI): compile-level compatibility check forwasm32-wasip1.
To compile the web test crate:
cargo test -p oxide_core --target wasm32-unknown-unknown --all-features --no-run --test wasm_web_compat§Troubleshooting
#[tokio::test]does not work onwasm32-unknown-unknown. Usewasm-bindgen-testfor web WASM tests and keep Tokio-based tests for native targets.- Web persistence uses
localStorageand therefore requires a browser environment. If you run WASM tests under Node.js,window/localStoragemay not be available. - Persistence is debounced: writes are throttled to at most once per
PersistenceConfig.min_interval, always persisting the latest snapshot payload observed during the interval.
§Commands
From rust/:
cargo testTo run benches (if enabled for your environment):
cargo bench§License
Dual-licensed under MIT OR Apache-2.0. See LICENSE.
Re-exports§
pub use ffi::watch_receiver_to_stream;pub use tokio;
Modules§
- ffi
- FFI-oriented utilities. FFI-facing helpers.
- runtime
- Async runtime utilities backed by Flutter Rust Bridge. Async runtime utilities backed by Flutter Rust Bridge (FRB).
Structs§
- Init
Context - Initialization context passed to
Reducer::init. - Reducer
Engine - A reducer-driven engine that owns state, broadcasts snapshots, and runs side-effects.
- State
Snapshot - Versioned snapshot of store state.
Enums§
- Oxide
Error - Error type for core operations.
- State
Change - Indicates whether applying an action/effect produced a new state snapshot.
Traits§
- Reducer
- A reducer defines how actions mutate state and how side-effects are applied.
- Sliced
State - Marker trait implemented by state types that opt into sliced updates.
Type Aliases§
- Core
Result - Convenience alias used throughout the crate.