use crate::persist::backend::{
LocalStorageBackend, PersistenceBackend, QueryBackend, SessionStorageBackend,
};
use crate::persist::codec::{
OptionCodec, ParseCodec, PersistCodec, StringCodec, map_decode_error, map_encode_error,
};
use crate::persist::state::{
PersistenceController, PersistenceState, Persistent, apply_backend_event,
flush_persistent_value,
};
use crate::persist::{
DecodePolicy, PersistMode, PersistenceError, RemovePolicy, SyncStrategy, WriteDefault,
};
use silex_core::reactivity::{Effect, RwSignal, StoredValue, on_cleanup};
use silex_core::traits::{RxData, RxGet, RxRead, RxWrite};
use std::marker::PhantomData;
use std::rc::Rc;
pub struct NoBackend;
pub struct NoCodec;
pub struct NoDefault;
pub struct HasDefault;
struct PersistConfig<T> {
default: Option<Rc<dyn Fn() -> T>>,
write_default: WriteDefault,
decode_policy: DecodePolicy,
remove_policy: RemovePolicy,
mode: PersistMode,
sync: SyncStrategy,
}
impl<T> PersistConfig<T> {
fn new() -> Self {
Self {
default: None,
write_default: WriteDefault::IfMissing,
decode_policy: DecodePolicy::RemoveAndUseDefault,
remove_policy: RemovePolicy::UseDefault,
mode: PersistMode::Immediate,
sync: SyncStrategy::CrossContext,
}
}
}
pub struct PersistentBuilder<B, C, T = (), D = NoDefault> {
key: String,
backend: B,
codec: C,
config: PersistConfig<T>,
_marker: PhantomData<D>,
}
impl PersistentBuilder<NoBackend, NoCodec, (), NoDefault> {
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
backend: NoBackend,
codec: NoCodec,
config: PersistConfig::new(),
_marker: PhantomData,
}
}
}
impl<C, T, D> PersistentBuilder<NoBackend, C, T, D> {
pub fn local(self) -> PersistentBuilder<LocalStorageBackend, C, T, D> {
PersistentBuilder {
key: self.key,
backend: LocalStorageBackend::default(),
codec: self.codec,
config: self.config,
_marker: PhantomData,
}
}
pub fn session(self) -> PersistentBuilder<SessionStorageBackend, C, T, D> {
PersistentBuilder {
key: self.key,
backend: SessionStorageBackend::default(),
codec: self.codec,
config: self.config,
_marker: PhantomData,
}
}
pub fn query(self) -> PersistentBuilder<QueryBackend, C, T, D> {
PersistentBuilder {
key: self.key,
backend: QueryBackend::new().unwrap_or_else(|_| QueryBackend::unavailable()),
codec: self.codec,
config: self.config,
_marker: PhantomData,
}
}
}
impl<B, T, D> PersistentBuilder<B, NoCodec, T, D> {
pub fn string(self) -> PersistentBuilder<B, StringCodec, String, D> {
PersistentBuilder {
key: self.key,
backend: self.backend,
codec: StringCodec,
config: PersistConfig {
default: None,
write_default: self.config.write_default,
decode_policy: self.config.decode_policy,
remove_policy: self.config.remove_policy,
mode: self.config.mode,
sync: self.config.sync,
},
_marker: PhantomData,
}
}
pub fn parse<U>(self) -> PersistentBuilder<B, ParseCodec<U>, U, D>
where
U: std::fmt::Display + std::str::FromStr + Clone + 'static,
<U as std::str::FromStr>::Err: std::fmt::Display,
{
PersistentBuilder {
key: self.key,
backend: self.backend,
codec: ParseCodec::new(),
config: PersistConfig {
default: None,
write_default: self.config.write_default,
decode_policy: self.config.decode_policy,
remove_policy: self.config.remove_policy,
mode: self.config.mode,
sync: self.config.sync,
},
_marker: PhantomData,
}
}
#[cfg(feature = "json")]
pub fn json<U>(self) -> PersistentBuilder<B, crate::persist::PersistJsonCodec<U>, U, D>
where
U: serde::Serialize + serde::de::DeserializeOwned + Clone + 'static,
{
PersistentBuilder {
key: self.key,
backend: self.backend,
codec: crate::persist::PersistJsonCodec::new(),
config: PersistConfig {
default: None,
write_default: self.config.write_default,
decode_policy: self.config.decode_policy,
remove_policy: self.config.remove_policy,
mode: self.config.mode,
sync: self.config.sync,
},
_marker: PhantomData,
}
}
}
impl<B, C, T, D> PersistentBuilder<B, C, T, D> {
pub fn write_default(mut self, policy: WriteDefault) -> Self {
self.config.write_default = policy;
self
}
pub fn on_decode_error(mut self, policy: DecodePolicy) -> Self {
self.config.decode_policy = policy;
self
}
pub fn on_remove(mut self, policy: RemovePolicy) -> Self {
self.config.remove_policy = policy;
self
}
pub fn mode(mut self, mode: PersistMode) -> Self {
self.config.mode = mode;
self
}
pub fn sync(mut self, sync: SyncStrategy) -> Self {
self.config.sync = sync;
self
}
}
impl<B, C, T, D> PersistentBuilder<B, C, T, D>
where
T: Clone + 'static,
{
pub fn default(self, value: T) -> PersistentBuilder<B, C, T, HasDefault> {
let value = Rc::new(value);
PersistentBuilder {
key: self.key,
backend: self.backend,
codec: self.codec,
config: PersistConfig {
default: Some({
let value = value.clone();
Rc::new(move || (*value).clone())
}),
write_default: self.config.write_default,
decode_policy: self.config.decode_policy,
remove_policy: self.config.remove_policy,
mode: self.config.mode,
sync: self.config.sync,
},
_marker: PhantomData,
}
}
pub fn default_with(
self,
f: impl Fn() -> T + 'static,
) -> PersistentBuilder<B, C, T, HasDefault> {
PersistentBuilder {
key: self.key,
backend: self.backend,
codec: self.codec,
config: PersistConfig {
default: Some(Rc::new(f)),
write_default: self.config.write_default,
decode_policy: self.config.decode_policy,
remove_policy: self.config.remove_policy,
mode: self.config.mode,
sync: self.config.sync,
},
_marker: PhantomData,
}
}
}
impl<B, C, T, D> PersistentBuilder<B, C, T, D>
where
C: PersistCodec<T>,
T: Clone + 'static,
{
pub fn optional(self) -> PersistentBuilder<B, OptionCodec<C, T>, Option<T>, HasDefault> {
PersistentBuilder {
key: self.key,
backend: self.backend,
codec: OptionCodec::new(self.codec),
config: PersistConfig {
default: Some(Rc::new(|| None)),
write_default: self.config.write_default,
decode_policy: self.config.decode_policy,
remove_policy: self.config.remove_policy,
mode: self.config.mode,
sync: self.config.sync,
},
_marker: PhantomData,
}
}
}
impl<B, C, T> PersistentBuilder<B, C, T, HasDefault>
where
B: PersistenceBackend,
C: PersistCodec<T>,
T: RxData + Clone + PartialEq + 'static,
{
pub fn build(self) -> Persistent<T> {
let default = self
.config
.default
.expect("Default status verified by typestate");
let value = RwSignal::new(default());
let state = RwSignal::new(PersistenceState::Ready(String::new()));
let backend = self.backend.clone();
let codec = self.codec.clone();
let key = self.key.clone();
let controller = StoredValue::new(PersistenceController {
key: key.clone(),
default: default.clone(),
decode_policy: self.config.decode_policy,
remove_policy: self.config.remove_policy,
last_flushed_raw: None,
skip_next_auto_flush: false,
backend_get: Rc::new({
let backend = backend.clone();
move |key| backend.get(key)
}),
backend_set: Rc::new({
let backend = backend.clone();
move |key, raw| backend.set(key, raw)
}),
backend_remove: Rc::new({
let backend = backend.clone();
move |key| backend.remove(key)
}),
encode: Rc::new({
let codec = codec.clone();
move |value| codec.encode(value).map_err(map_encode_error)
}),
decode: Rc::new(move |raw| codec.decode(raw).map_err(|err| map_decode_error(raw, err))),
should_remove: Rc::new({
let codec = self.codec.clone();
move |value| codec.should_remove(value)
}),
subscription: None,
});
let mut had_missing_value = false;
match backend.get(&key) {
Ok(Some(raw)) => match self.codec.decode(&raw) {
Ok(decoded) => {
value.set_untracked(decoded);
let _ = controller.try_update_untracked(|controller| {
controller.last_flushed_raw = Some(raw.clone());
});
state.set_untracked(PersistenceState::Ready(raw));
}
Err(message) => {
state.set_untracked(PersistenceState::DecodeError(
crate::persist::DecodeErrorInfo {
raw: raw.clone(),
message: message.clone(),
},
));
value.set_untracked(default());
let _ = controller.try_update_untracked(|controller| {
controller.last_flushed_raw = None;
});
if matches!(self.config.decode_policy, DecodePolicy::RemoveAndUseDefault) {
let _ = backend.remove(&key);
}
}
},
Ok(None) => {
had_missing_value = true;
value.set_untracked(default());
state.set_untracked(PersistenceState::Ready(String::new()));
}
Err(PersistenceError::BackendUnavailable) => {
value.set_untracked(default());
state.set_untracked(PersistenceState::Unavailable);
let _ = controller.try_update_untracked(|controller| {
controller.skip_next_auto_flush = true;
});
}
Err(err) => {
value.set_untracked(default());
state.set_untracked(PersistenceState::ReadError(err.message()));
let _ = controller.try_update_untracked(|controller| {
controller.skip_next_auto_flush = true;
});
}
}
if had_missing_value {
match self.config.write_default {
WriteDefault::Always | WriteDefault::IfMissing => {
let _ = flush_persistent_value(controller, value, state);
}
WriteDefault::Never => {
let _ = controller.try_update_untracked(|controller| {
controller.skip_next_auto_flush = true;
});
}
}
}
if matches!(self.config.sync, SyncStrategy::CrossContext) {
let callback =
Rc::new(move |event| apply_backend_event(controller, value, state, event));
if let Ok(subscription) = backend.subscribe(key.clone(), callback) {
let _ = controller.try_update_untracked(|controller| {
controller.subscription = Some(subscription);
});
}
}
if matches!(self.config.mode, PersistMode::Immediate) {
let debounce_duration = match self.config.sync {
SyncStrategy::Debounce(d) => Some(d),
_ => None,
};
if let Some(duration) = debounce_duration {
let timer = StoredValue::new(None::<silex_dom::helpers::TimeoutHandle>);
Effect::new(move |_| {
let current = value.get();
let should_skip =
controller.with_untracked(|controller| controller.skip_next_auto_flush);
if should_skip {
let _ = controller.try_update_untracked(|controller| {
controller.skip_next_auto_flush = false;
});
return;
}
let raw = controller.with_untracked(|c| (c.encode)(¤t));
if let Ok(raw) = raw {
state.set(PersistenceState::Syncing(raw));
}
if let Some(handle) = timer.get_untracked() {
handle.clear();
}
let handle = silex_dom::helpers::set_timeout_with_handle(
move || {
let _ = flush_persistent_value(controller, value, state);
timer.set_untracked(None);
},
duration,
);
if let Ok(h) = handle {
timer.set_untracked(Some(h));
}
});
on_cleanup(move || {
if let Some(handle) = timer.get_untracked() {
handle.clear();
}
});
} else {
Effect::new(move |_| {
value.get();
let should_skip =
controller.with_untracked(|controller| controller.skip_next_auto_flush);
if should_skip {
let _ = controller.try_update_untracked(|controller| {
controller.skip_next_auto_flush = false;
});
return;
}
let _ = flush_persistent_value(controller, value, state);
});
}
}
on_cleanup(move || {
let _ = controller.try_update_untracked(|controller| {
controller.subscription.take();
});
});
Persistent {
value,
state,
controller,
}
}
}
impl<B, C, T, D> PersistentBuilder<B, C, T, D> {
fn _marker(&self) -> PhantomData<(T, D)> {
PhantomData
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::persist::backend::{BackendEvent, BackendSubscription};
use std::cell::RefCell;
use std::collections::HashMap;
type SubscriptionMap = Rc<RefCell<HashMap<String, Vec<Rc<dyn Fn(BackendEvent)>>>>>;
#[derive(Clone, Default)]
struct MockBackend {
state: Rc<RefCell<HashMap<String, String>>>,
removed: Rc<RefCell<Vec<String>>>,
subscriptions: SubscriptionMap,
fail_writes: Rc<RefCell<bool>>,
}
impl MockBackend {
fn with_value(key: &str, value: &str) -> Self {
let mut state = HashMap::new();
state.insert(key.to_string(), value.to_string());
Self {
state: Rc::new(RefCell::new(state)),
removed: Rc::new(RefCell::new(Vec::new())),
subscriptions: Rc::new(RefCell::new(HashMap::new())),
fail_writes: Rc::new(RefCell::new(false)),
}
}
fn failing_writes() -> Self {
Self {
state: Rc::new(RefCell::new(HashMap::new())),
removed: Rc::new(RefCell::new(Vec::new())),
subscriptions: Rc::new(RefCell::new(HashMap::new())),
fail_writes: Rc::new(RefCell::new(true)),
}
}
fn emit(&self, key: &str, event: BackendEvent) {
let callbacks = self
.subscriptions
.borrow()
.get(key)
.cloned()
.unwrap_or_default();
for callback in callbacks {
callback(event.clone());
}
}
}
impl PersistenceBackend for MockBackend {
fn get(&self, key: &str) -> Result<Option<String>, PersistenceError> {
Ok(self.state.borrow().get(key).cloned())
}
fn set(&self, key: &str, value: &str) -> Result<(), PersistenceError> {
if *self.fail_writes.borrow() {
return Err(PersistenceError::WriteFailed(
"mock backend write failure".to_string(),
));
}
self.state
.borrow_mut()
.insert(key.to_string(), value.to_string());
Ok(())
}
fn remove(&self, key: &str) -> Result<(), PersistenceError> {
self.state.borrow_mut().remove(key);
self.removed.borrow_mut().push(key.to_string());
Ok(())
}
fn subscribe(
&self,
key: String,
callback: Rc<dyn Fn(BackendEvent)>,
) -> Result<BackendSubscription, PersistenceError> {
self.subscriptions
.borrow_mut()
.entry(key)
.or_default()
.push(callback);
Ok(BackendSubscription::new(|| {}))
}
}
fn parse_builder(
backend: MockBackend,
key: &str,
) -> PersistentBuilder<MockBackend, ParseCodec<i32>, i32, NoDefault> {
PersistentBuilder {
key: key.to_string(),
backend,
codec: ParseCodec::new(),
config: PersistConfig::new(),
_marker: PhantomData,
}
}
#[test]
fn write_default_if_missing_persists_default() {
let backend = MockBackend::default();
let value = parse_builder(backend.clone(), "counter").default(7).build();
assert_eq!(value.get_untracked(), 7);
assert_eq!(backend.get("counter").unwrap(), Some("7".to_string()));
assert_eq!(
value.state().get_untracked(),
PersistenceState::Ready("7".to_string())
);
}
#[test]
fn decode_error_remove_and_use_default_removes_invalid_backend_value() {
let backend = MockBackend::with_value("counter", "bad");
let value = parse_builder(backend.clone(), "counter")
.on_decode_error(DecodePolicy::RemoveAndUseDefault)
.default(5)
.build();
assert_eq!(value.get_untracked(), 5);
assert_eq!(backend.get("counter").unwrap(), Some("5".to_string()));
assert_eq!(
backend.removed.borrow().as_slice(),
&["counter".to_string()]
);
assert_eq!(
value.state().get_untracked(),
PersistenceState::Ready("5".to_string())
);
}
#[test]
fn decode_error_use_default_keeps_invalid_backend_value() {
let backend = MockBackend::with_value("counter", "bad");
let value = parse_builder(backend.clone(), "counter")
.on_decode_error(DecodePolicy::UseDefault)
.default(11)
.build();
assert_eq!(value.get_untracked(), 11);
assert_eq!(backend.get("counter").unwrap(), Some("11".to_string()));
assert!(backend.removed.borrow().is_empty());
assert_eq!(
value.state().get_untracked(),
PersistenceState::Ready("11".to_string())
);
}
#[test]
fn codec_selection_preserves_builder_configuration() {
let backend = MockBackend::default();
let value = PersistentBuilder {
key: "counter".to_string(),
backend,
codec: NoCodec,
config: PersistConfig::<()>::new(),
_marker: PhantomData::<NoDefault>,
}
.mode(PersistMode::Manual)
.sync(SyncStrategy::None)
.write_default(WriteDefault::Never)
.parse::<i32>()
.default(9)
.build();
assert_eq!(value.get_untracked(), 9);
assert_eq!(
value.state().get_untracked(),
PersistenceState::Ready(String::new())
);
}
#[test]
fn flush_write_failure_sets_write_error_state() {
let backend = MockBackend::failing_writes();
let value = parse_builder(backend, "counter").default(3).build();
assert!(matches!(
value.flush(),
Err(PersistenceError::WriteFailed(message)) if message == "mock backend write failure"
));
assert_eq!(
value.state().get_untracked(),
PersistenceState::WriteError("mock backend write failure".to_string())
);
}
#[test]
fn initial_default_write_failure_sets_write_error_state() {
let backend = MockBackend::failing_writes();
let value = parse_builder(backend, "counter").default(3).build();
assert_eq!(value.get_untracked(), 3);
assert_eq!(
value.state().get_untracked(),
PersistenceState::WriteError("mock backend write failure".to_string())
);
}
#[test]
fn optional_none_flush_removes_backend_key() {
let backend = MockBackend::with_value("name", "alice");
let value = PersistentBuilder {
key: "name".to_string(),
backend: backend.clone(),
codec: StringCodec,
config: PersistConfig::<String>::new(),
_marker: PhantomData::<NoDefault>,
}
.optional()
.build();
assert_eq!(value.get_untracked(), Some("alice".to_string()));
value.set(None);
value.flush().unwrap();
assert_eq!(backend.get("name").unwrap(), None);
assert_eq!(backend.removed.borrow().as_slice(), &["name".to_string()]);
assert_eq!(
value.state().get_untracked(),
PersistenceState::Ready(String::new())
);
}
#[test]
fn external_remove_uses_default_without_rewriting_backend() {
let backend = MockBackend::with_value("counter", "7");
let value = parse_builder(backend.clone(), "counter").default(5).build();
assert_eq!(value.get_untracked(), 7);
backend.state.borrow_mut().remove("counter");
backend.emit(
"counter",
BackendEvent::Removed {
key: "counter".to_string(),
},
);
assert_eq!(value.get_untracked(), 5);
assert_eq!(backend.get("counter").unwrap(), None);
assert!(backend.removed.borrow().is_empty());
assert_eq!(
value.state().get_untracked(),
PersistenceState::Ready(String::new())
);
}
}