mod autosave;
mod builder;
mod path;
use crate::error::Result;
use crate::event::{emit, STORE_UNLOAD_EVENT};
use crate::store::{SaveStrategy, Store, StoreResource, StoreState};
use autosave::Autosave;
use dashmap::DashMap;
use path::set_path;
use serde::de::DeserializeOwned;
use serde_json::Value as Json;
use std::collections::HashSet;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use tauri::{AppHandle, Resource, ResourceId, Runtime};
pub use builder::StoreCollectionBuilder;
#[cfg(tauri_store_tracing)]
use tracing::error;
pub(crate) static RESOURCE_ID: OnceLock<ResourceId> = OnceLock::new();
pub type OnLoadFn<R> = dyn Fn(&Store<R>) -> Result<()> + Send + Sync;
pub struct StoreCollection<R: Runtime> {
pub(crate) app: AppHandle<R>,
pub(crate) path: Mutex<PathBuf>,
pub(crate) stores: DashMap<String, ResourceId>,
pub(crate) on_load: Option<Box<OnLoadFn<R>>>,
pub(crate) autosave: Mutex<Autosave>,
pub(crate) default_save_strategy: SaveStrategy,
pub(crate) save_denylist: Option<HashSet<String>>,
pub(crate) sync_denylist: Option<HashSet<String>>,
pub(crate) pretty: bool,
}
impl<R: Runtime> StoreCollection<R> {
pub fn builder() -> StoreCollectionBuilder<R> {
StoreCollectionBuilder::new()
}
pub(crate) fn get_resource(&self, id: impl AsRef<str>) -> Result<Arc<StoreResource<R>>> {
let id = id.as_ref();
let rid = match self.rid(id) {
Some(rid) => rid,
None => {
let (rid, resource) = Store::load(&self.app, id)?;
if let Some(on_load) = &self.on_load {
resource.locked(|store| on_load(store))?;
}
self.stores.insert(id.to_owned(), rid);
rid
}
};
StoreResource::get(&self.app, rid)
}
fn rid(&self, store_id: &str) -> Option<ResourceId> {
self.stores.get(store_id).map(|it| *it.value())
}
fn rids(&self) -> Vec<ResourceId> {
self.stores.iter().map(|it| *it.value()).collect()
}
pub fn ids(&self) -> Vec<String> {
self
.stores
.iter()
.map(|it| it.key().clone())
.collect()
}
pub fn path(&self) -> PathBuf {
self.path.lock().unwrap().clone()
}
pub fn set_path(&self, path: impl AsRef<Path>) -> Result<()> {
set_path(self, path)
}
pub fn with_store<F, T>(&self, store_id: impl AsRef<str>, f: F) -> Result<T>
where
F: FnOnce(&mut Store<R>) -> T,
{
Ok(self.get_resource(store_id)?.locked(f))
}
pub fn state(&self, store_id: impl AsRef<str>) -> Result<StoreState> {
self
.get_resource(store_id)?
.locked(|store| Ok(store.state().clone()))
}
pub fn try_state<T>(&self, store_id: impl AsRef<str>) -> Result<T>
where
T: DeserializeOwned,
{
self
.get_resource(store_id)?
.locked(|store| store.try_state())
}
pub fn get(&self, store_id: impl AsRef<str>, key: impl AsRef<str>) -> Option<Json> {
self
.get_resource(store_id)
.ok()?
.locked(|store| store.get(key).cloned())
}
pub fn try_get<T>(&self, store_id: impl AsRef<str>, key: impl AsRef<str>) -> Result<T>
where
T: DeserializeOwned,
{
self
.get_resource(store_id)?
.locked(|store| store.try_get(key))
}
pub fn try_get_or<T>(&self, store_id: impl AsRef<str>, key: impl AsRef<str>, default: T) -> T
where
T: DeserializeOwned,
{
self.try_get(store_id, key).unwrap_or(default)
}
pub fn try_get_or_default<T>(&self, store_id: impl AsRef<str>, key: impl AsRef<str>) -> T
where
T: Default + DeserializeOwned,
{
self.try_get(store_id, key).unwrap_or_default()
}
pub fn try_get_or_else<T>(
&self,
store_id: impl AsRef<str>,
key: impl AsRef<str>,
f: impl FnOnce() -> T,
) -> T
where
T: DeserializeOwned,
{
self
.try_get(store_id, key)
.unwrap_or_else(|_| f())
}
pub fn set<K, V>(&self, store_id: impl AsRef<str>, key: K, value: V) -> Result<()>
where
K: AsRef<str>,
V: Into<Json>,
{
self
.get_resource(store_id)?
.locked(|store| store.set(key, value))
}
pub fn patch<S>(&self, store_id: impl AsRef<str>, state: S) -> Result<()>
where
S: Into<StoreState>,
{
self
.get_resource(store_id)?
.locked(|store| store.patch(state))
}
pub fn save(&self, store_id: impl AsRef<str>) -> Result<()> {
self
.get_resource(store_id)?
.locked(|store| store.save())
}
pub fn save_now(&self, store_id: impl AsRef<str>) -> Result<()> {
self.get_resource(store_id)?.locked(|store| {
store.abort_pending_save();
store.save_now()
})
}
pub fn save_some(&self, ids: &[impl AsRef<str>]) -> Result<()> {
ids.iter().try_for_each(|id| self.save(id))
}
pub fn save_some_now(&self, ids: &[impl AsRef<str>]) -> Result<()> {
ids.iter().try_for_each(|id| self.save_now(id))
}
pub fn save_all(&self) -> Result<()> {
self
.rids()
.into_iter()
.try_for_each(|rid| StoreResource::save(&self.app, rid))
}
pub fn save_all_now(&self) -> Result<()> {
self
.rids()
.into_iter()
.try_for_each(|rid| StoreResource::save_now(&self.app, rid))
}
pub fn default_save_strategy(&self) -> SaveStrategy {
self.default_save_strategy
}
pub fn set_autosave(&self, duration: Duration) {
if let Ok(mut autosave) = self.autosave.lock() {
autosave.set_duration(duration);
autosave.start(&self.app);
}
}
pub fn clear_autosave(&self) {
if let Ok(mut autosave) = self.autosave.lock() {
autosave.stop();
}
}
pub fn watch<F>(&self, store_id: impl AsRef<str>, f: F) -> Result<u32>
where
F: Fn(AppHandle<R>) -> Result<()> + Send + Sync + 'static,
{
self
.get_resource(store_id)?
.locked(|store| Ok(store.watch(f)))
}
pub fn unwatch(&self, store_id: impl AsRef<str>, watcher_id: u32) -> Result<bool> {
self
.get_resource(store_id)?
.locked(|store| Ok(store.unwatch(watcher_id)))
}
pub fn unload_store(&self, id: &str) -> Result<()> {
if let Some((_, rid)) = self.stores.remove(id) {
let resource = StoreResource::take(&self.app, rid)?;
resource.locked(|store| store.save_now())?;
emit(&self.app, STORE_UNLOAD_EVENT, id, None::<&str>)?;
}
Ok(())
}
pub fn on_exit(&self) -> Result<()> {
self.clear_autosave();
for rid in self.rids() {
match StoreResource::take(&self.app, rid) {
Ok(resource) => {
resource.locked(|store| {
if store.save_on_exit {
let _ = store.save_now();
}
});
}
#[cfg_attr(not(tauri_store_tracing), allow(unused_variables))]
Err(err) => {
#[cfg(tauri_store_tracing)]
error!("failed to take store resource: {err}");
}
}
}
Ok(())
}
}
impl<R: Runtime> Resource for StoreCollection<R> {}
impl<R: Runtime> fmt::Debug for StoreCollection<R> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoreCollection")
.field("pretty", &self.pretty)
.field("default_save_strategy", &self.default_save_strategy)
.field("on_load", &self.on_load.is_some())
.field(
"save_denylist",
&self
.save_denylist
.as_ref()
.map(HashSet::len)
.unwrap_or(0),
)
.field(
"sync_denylist",
&self
.sync_denylist
.as_ref()
.map(HashSet::len)
.unwrap_or(0),
)
.finish_non_exhaustive()
}
}