tauri-plugin-sync-state 0.1.0

Bidirectional, type-keyed state sync between a Tauri backend and a JS/React frontend.
Documentation
use crate::error::{Error, Result};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use std::any::Any;
use std::collections::HashMap;
use std::sync::Mutex;

pub trait SyncState: Serialize + DeserializeOwned + Clone + Send + Sync + 'static {
    const NAME: &'static str;
}

trait StateSlice: Send + Sync {
    fn as_any(&self) -> &dyn Any;
    fn as_any_mut(&mut self) -> &mut dyn Any;
    fn to_json(&self) -> Result<Value>;
    fn replace_from_json(&mut self, value: Value) -> Result<()>;
}

impl<S: SyncState> StateSlice for S {
    fn as_any(&self) -> &dyn Any {
        self
    }
    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
    fn to_json(&self) -> Result<Value> {
        serde_json::to_value(self).map_err(|e| Error::Serialize(e.to_string()))
    }
    fn replace_from_json(&mut self, value: Value) -> Result<()> {
        *self = serde_json::from_value(value).map_err(|e| Error::Deserialize(e.to_string()))?;
        Ok(())
    }
}

pub struct StateRegistry {
    slices: Mutex<HashMap<&'static str, Box<dyn StateSlice>>>,
}

impl StateRegistry {
    pub fn new() -> Self {
        Self {
            slices: Mutex::new(HashMap::new()),
        }
    }

    pub fn insert<S: SyncState>(&self, value: S) -> Result<()> {
        let mut map = self.slices.lock().map_err(|_| Error::LockPoisoned)?;
        if map.contains_key(S::NAME) {
            return Err(Error::DuplicateRegistration(S::NAME.to_owned()));
        }
        map.insert(S::NAME, Box::new(value));
        Ok(())
    }

    pub fn read<S: SyncState>(&self) -> Result<S> {
        let map = self.slices.lock().map_err(|_| Error::LockPoisoned)?;
        let slice = map
            .get(S::NAME)
            .ok_or_else(|| Error::NotFound(S::NAME.to_owned()))?;
        slice
            .as_any()
            .downcast_ref::<S>()
            .cloned()
            .ok_or_else(|| Error::TypeMismatch(S::NAME.to_owned()))
    }

    pub fn mutate<S: SyncState>(&self, f: impl FnOnce(&mut S)) -> Result<S> {
        let mut map = self.slices.lock().map_err(|_| Error::LockPoisoned)?;
        let slice = map
            .get_mut(S::NAME)
            .ok_or_else(|| Error::NotFound(S::NAME.to_owned()))?;
        let value = slice
            .as_any_mut()
            .downcast_mut::<S>()
            .ok_or_else(|| Error::TypeMismatch(S::NAME.to_owned()))?;
        f(value);
        Ok(value.clone())
    }

    pub fn get_json(&self, name: &str) -> Result<Value> {
        let map = self.slices.lock().map_err(|_| Error::LockPoisoned)?;
        map.get(name)
            .ok_or_else(|| Error::NotFound(name.to_owned()))?
            .to_json()
    }

    pub fn set_json(&self, name: &str, value: Value) -> Result<()> {
        let mut map = self.slices.lock().map_err(|_| Error::LockPoisoned)?;
        map.get_mut(name)
            .ok_or_else(|| Error::NotFound(name.to_owned()))?
            .replace_from_json(value)
    }

    pub fn contains(&self, name: &str) -> bool {
        self.slices
            .lock()
            .map(|m| m.contains_key(name))
            .unwrap_or(false)
    }
}

impl Default for StateRegistry {
    fn default() -> Self {
        Self::new()
    }
}