mod id;
mod marshaler;
mod options;
mod resource;
mod save;
mod state;
mod watch;
use crate::collection::CollectionMarker;
use crate::error::{Error, Result};
use crate::event::{
emit, ConfigPayload, EventSource, StatePayload, STORE_CONFIG_CHANGE_EVENT,
STORE_STATE_CHANGE_EVENT,
};
use crate::manager::ManagerExt;
use crate::StoreCollection;
use options::set_options;
use save::{debounce, throttle, SaveHandle};
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::collections::HashMap;
use std::fs::File;
use std::io::ErrorKind;
use std::io::Write;
use std::marker::PhantomData;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use std::{fmt, fs};
use tauri::async_runtime::spawn_blocking;
use tauri::{AppHandle, ResourceId, Runtime};
use watch::Watcher;
pub use id::StoreId;
pub use marshaler::{JsonMarshaler, Marshaler, MarshalingError, PrettyJsonMarshaler};
pub use options::StoreOptions;
pub(crate) use resource::StoreResource;
pub use save::SaveStrategy;
pub use state::StoreState;
pub use watch::WatcherId;
#[cfg(feature = "marshaler-cbor")]
pub use marshaler::CborMarshaler;
#[cfg(feature = "marshaler-ron")]
pub use marshaler::{PrettyRonMarshaler, RonMarshaler};
#[cfg(feature = "marshaler-toml")]
pub use marshaler::{PrettyTomlMarshaler, TomlMarshaler};
type ResourceTuple<R, C> = (ResourceId, Arc<StoreResource<R, C>>);
pub struct Store<R, C>
where
R: Runtime,
C: CollectionMarker,
{
app: AppHandle<R>,
pub(crate) id: StoreId,
state: StoreState,
pub(crate) save_on_exit: bool,
save_on_change: bool,
save_strategy: Option<SaveStrategy>,
debounce_save_handle: OnceLock<SaveHandle<R>>,
throttle_save_handle: OnceLock<SaveHandle<R>>,
watchers: HashMap<WatcherId, Watcher<R>>,
phantom: PhantomData<C>,
}
impl<R, C> Store<R, C>
where
R: Runtime,
C: CollectionMarker,
{
pub(crate) fn load(app: &AppHandle<R>, id: impl AsRef<str>) -> Result<ResourceTuple<R, C>> {
let id = StoreId::from(id.as_ref());
let collection = app.store_collection_with_marker::<C>();
let marshaler = collection.marshaler_table.get(&id);
let path = make_path::<R, C>(&collection, &id, marshaler.extension());
let state = match fs::read(&path) {
Ok(bytes) => marshaler
.deserialize(&bytes)
.map_err(Error::FailedToDeserialize)?,
Err(err) if err.kind() == ErrorKind::NotFound => StoreState::default(),
Err(err) => return Err(Error::Io(err)),
};
let mut store = Self {
app: app.clone(),
id,
state,
save_on_change: false,
save_on_exit: true,
save_strategy: None,
debounce_save_handle: OnceLock::new(),
throttle_save_handle: OnceLock::new(),
watchers: HashMap::new(),
phantom: PhantomData,
};
store.run_pending_migrations()?;
Ok(StoreResource::create(app, store))
}
fn run_pending_migrations(&mut self) -> Result<()> {
self
.app
.store_collection_with_marker::<C>()
.migrator
.lock()
.expect("migrator is poisoned")
.migrate::<R, C>(&self.app, &self.id, &mut self.state)
}
#[inline]
pub fn id(&self) -> StoreId {
self.id.clone()
}
pub fn path(&self) -> PathBuf {
let collection = self.app.store_collection_with_marker::<C>();
let marshaler = collection.marshaler_table.get(&self.id);
make_path::<R, C>(&collection, &self.id, marshaler.extension())
}
pub fn app_handle(&self) -> &AppHandle<R> {
&self.app
}
#[inline]
pub fn raw_state(&self) -> &StoreState {
&self.state
}
pub fn state<T>(&self) -> Result<T>
where
T: DeserializeOwned,
{
let value = Value::from(&self.state);
Ok(serde_json::from_value(value)?)
}
pub fn state_or<T>(&self, default: T) -> T
where
T: DeserializeOwned,
{
self.state().unwrap_or(default)
}
pub fn state_or_default<T>(&self) -> T
where
T: DeserializeOwned + Default,
{
self.state().unwrap_or_default()
}
pub fn state_or_else<T>(&self, f: impl FnOnce() -> T) -> T
where
T: DeserializeOwned,
{
self.state().unwrap_or_else(|_| f())
}
pub fn get_raw(&self, key: impl AsRef<str>) -> Option<&Value> {
self.state.get_raw(key)
}
pub unsafe fn get_raw_unchecked(&self, key: impl AsRef<str>) -> &Value {
unsafe { self.state.get_raw_unchecked(key) }
}
pub fn get<T>(&self, key: impl AsRef<str>) -> Result<T>
where
T: DeserializeOwned,
{
self.state.get(key)
}
pub fn get_or<T>(&self, key: impl AsRef<str>, default: T) -> T
where
T: DeserializeOwned,
{
self.state.get_or(key, default)
}
pub fn get_or_default<T>(&self, key: impl AsRef<str>) -> T
where
T: DeserializeOwned + Default,
{
self.state.get_or_default(key)
}
pub fn get_or_else<T>(&self, key: impl AsRef<str>, f: impl FnOnce() -> T) -> T
where
T: DeserializeOwned,
{
self.state.get_or_else(key, f)
}
pub unsafe fn get_unchecked<T>(&self, key: impl AsRef<str>) -> T
where
T: DeserializeOwned,
{
unsafe { self.state.get_unchecked(key) }
}
pub fn set(&mut self, key: impl AsRef<str>, value: impl Into<Value>) -> Result<()> {
self.state.set(key, value);
self.on_state_change(None::<&str>)
}
#[doc(hidden)]
pub fn patch_with_source<S, E>(&mut self, state: S, source: E) -> Result<()>
where
S: Into<StoreState>,
E: Into<EventSource>,
{
self.state.patch(state);
self.on_state_change(source)
}
pub fn patch<S>(&mut self, state: S) -> Result<()>
where
S: Into<StoreState>,
{
self.patch_with_source(state, None::<&str>)
}
pub fn has(&self, key: impl AsRef<str>) -> bool {
self.state.has(key)
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.state.keys()
}
pub fn values(&self) -> impl Iterator<Item = &Value> {
self.state.values()
}
pub fn entries(&self) -> impl Iterator<Item = (&String, &Value)> {
self.state.entries()
}
#[inline]
pub fn len(&self) -> usize {
self.state.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.state.is_empty()
}
pub fn save(&self) -> Result<()> {
match self.save_strategy() {
SaveStrategy::Immediate => self.save_now()?,
SaveStrategy::Debounce(duration) => {
self
.debounce_save_handle
.get_or_init(|| debounce::<R, C>(self.id.clone(), duration))
.call(&self.app);
}
SaveStrategy::Throttle(duration) => {
self
.throttle_save_handle
.get_or_init(|| throttle::<R, C>(self.id.clone(), duration))
.call(&self.app);
}
}
Ok(())
}
pub fn save_now(&self) -> Result<()> {
let collection = self.app.store_collection_with_marker::<C>();
if collection.save_denylist.contains(&self.id) {
return Ok(());
}
let marshaler = collection.marshaler_table.get(&self.id);
let bytes = marshaler
.serialize(&self.state)
.map_err(Error::FailedToSerialize)?;
let path = self.path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = File::create(path)?;
file.write_all(&bytes)?;
file.flush()?;
if cfg!(feature = "file-sync-all") {
file.sync_all()?;
}
Ok(())
}
#[inline]
pub fn save_on_exit(&mut self, enabled: bool) {
self.save_on_exit = enabled;
}
#[inline]
pub fn save_on_change(&mut self, enabled: bool) {
self.save_on_change = enabled;
}
pub fn save_strategy(&self) -> SaveStrategy {
self.save_strategy.unwrap_or_else(|| {
self
.app
.store_collection_with_marker::<C>()
.default_save_strategy
})
}
pub fn set_save_strategy(&mut self, strategy: SaveStrategy) {
if strategy.is_debounce() {
self
.debounce_save_handle
.take()
.inspect(SaveHandle::abort);
} else if strategy.is_throttle() {
self
.throttle_save_handle
.take()
.inspect(SaveHandle::abort);
}
self.save_strategy = Some(strategy);
}
pub fn watch<F>(&mut self, f: F) -> WatcherId
where
F: Fn(AppHandle<R>) -> Result<()> + Send + Sync + 'static,
{
let (id, listener) = Watcher::new(f);
self.watchers.insert(id, listener);
id
}
pub fn unwatch(&mut self, id: impl Into<WatcherId>) -> bool {
self.watchers.remove(&id.into()).is_some()
}
pub fn set_options(&mut self, options: StoreOptions) -> Result<()> {
self.set_options_with_source(options, None::<&str>)
}
#[doc(hidden)]
pub fn set_options_with_source<E>(&mut self, options: StoreOptions, source: E) -> Result<()>
where
E: Into<EventSource>,
{
set_options(self, options);
self.on_config_change(source)
}
fn on_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
self.emit_state_change(source)?;
self.call_watchers();
if self.save_on_change {
self.save()?;
}
Ok(())
}
fn emit_state_change(&self, source: impl Into<EventSource>) -> Result<()> {
let source: EventSource = source.into();
if !source.is_backend()
&& self
.app
.store_collection_with_marker::<C>()
.sync_denylist
.contains(&self.id)
{
return Ok(());
}
emit(
&self.app,
STORE_STATE_CHANGE_EVENT,
&StatePayload::from(self),
source,
)
}
fn on_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
self.emit_config_change(source)
}
fn emit_config_change(&self, source: impl Into<EventSource>) -> Result<()> {
emit(
&self.app,
STORE_CONFIG_CHANGE_EVENT,
&ConfigPayload::from(self),
source,
)
}
fn call_watchers(&self) {
if self.watchers.is_empty() {
return;
}
for watcher in self.watchers.values() {
let app = self.app.clone();
let watcher = watcher.clone();
spawn_blocking(move || watcher.call(app));
}
}
pub(crate) fn abort_pending_save(&self) {
self
.debounce_save_handle
.get()
.map(SaveHandle::abort);
self
.throttle_save_handle
.get()
.map(SaveHandle::abort);
}
pub(crate) fn destroy(&mut self) -> Result<()> {
self.abort_pending_save();
self.state.clear();
fs::remove_file(self.path())?;
Ok(())
}
}
impl<R, C> fmt::Debug for Store<R, C>
where
R: Runtime,
C: CollectionMarker,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Store")
.field("id", &self.id)
.field("state", &self.state)
.field("watchers", &self.watchers.len())
.field("save_on_exit", &self.save_on_exit)
.field("save_on_change", &self.save_on_change)
.field("save_strategy", &self.save_strategy)
.finish_non_exhaustive()
}
}
fn make_path<R, C>(collection: &StoreCollection<R, C>, id: &StoreId, extension: &str) -> PathBuf
where
R: Runtime,
C: CollectionMarker,
{
debug_assert!(
!extension.eq_ignore_ascii_case("tauristore"),
"illegal store extension: {extension}"
);
let filename = if cfg!(debug_assertions) && collection.debug_stores {
format!("{id}.dev.{extension}")
} else {
format!("{id}.{extension}")
};
collection.path_of(id).join(filename)
}