mod id;
mod options;
mod resource;
mod save;
mod state;
mod watch;
use crate::error::Result;
use crate::manager::ManagerExt;
use options::set_options;
use save::{debounce, throttle, SaveHandle};
use serde::de::DeserializeOwned;
use serde_json::{json, Value as Json};
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use tauri::async_runtime::spawn_blocking;
use tauri::{AppHandle, ResourceId, Runtime};
use tauri_store_utils::{read_file, write_file};
use watch::Watcher;
use crate::event::{
emit, ConfigPayload, EventSource, StatePayload, STORE_CONFIG_CHANGE_EVENT,
STORE_STATE_CHANGE_EVENT,
};
pub use id::StoreId;
pub use options::StoreOptions;
pub(crate) use resource::StoreResource;
pub use save::SaveStrategy;
pub use state::StoreState;
pub use watch::WatcherId;
#[cfg(tauri_store_tracing)]
use tracing::debug;
#[cfg(debug_assertions)]
const FILE_EXTENSION: &str = "dev.json";
#[cfg(not(debug_assertions))]
const FILE_EXTENSION: &str = "json";
type ResourceTuple<R> = (ResourceId, Arc<StoreResource<R>>);
pub struct Store<R: Runtime> {
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>>,
}
impl<R: Runtime> Store<R> {
pub(crate) fn load(app: &AppHandle<R>, id: impl AsRef<str>) -> Result<ResourceTuple<R>> {
let id = StoreId::from(id.as_ref());
let path = store_path(app, &id);
let state = read_file(&path).call()?;
#[cfg(tauri_store_tracing)]
debug!("store loaded: {id}");
#[allow(unused_mut)]
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(),
};
#[cfg(feature = "unstable-migration")]
store.run_pending_migrations(app)?;
Ok(StoreResource::create(app, store))
}
#[cfg(feature = "unstable-migration")]
fn run_pending_migrations(&mut self, app: &AppHandle<R>) -> Result<()> {
use crate::meta::Meta;
let collection = app.store_collection();
let result = collection
.migrator
.lock()
.expect("migrator is poisoned")
.migrate(&self.id, &mut self.state);
if result.done > 0 {
Meta::write(&collection)?;
}
if let Some(err) = result.error {
Err(err)
} else {
Ok(())
}
}
#[inline]
pub fn id(&self) -> StoreId {
self.id.clone()
}
pub fn path(&self) -> PathBuf {
store_path(&self.app, &self.id)
}
pub fn app_handle(&self) -> &AppHandle<R> {
&self.app
}
#[inline]
pub fn state(&self) -> &StoreState {
&self.state
}
pub fn try_state<T>(&self) -> Result<T>
where
T: DeserializeOwned,
{
Ok(serde_json::from_value(json!(self.state))?)
}
pub fn try_state_or<T>(&self, default: T) -> T
where
T: DeserializeOwned,
{
self.try_state().unwrap_or(default)
}
pub fn try_state_or_default<T>(&self) -> T
where
T: DeserializeOwned + Default,
{
self.try_state().unwrap_or_default()
}
pub fn try_state_or_else<T>(&self, f: impl FnOnce() -> T) -> T
where
T: DeserializeOwned,
{
self.try_state().unwrap_or_else(|_| f())
}
pub fn get(&self, key: impl AsRef<str>) -> Option<&Json> {
self.state.get(key)
}
pub fn try_get<T>(&self, key: impl AsRef<str>) -> Result<T>
where
T: DeserializeOwned,
{
self.state.try_get(key)
}
pub fn try_get_or<T>(&self, key: impl AsRef<str>, default: T) -> T
where
T: DeserializeOwned,
{
self.state.try_get_or(key, default)
}
pub fn try_get_or_default<T>(&self, key: impl AsRef<str>) -> T
where
T: DeserializeOwned + Default,
{
self.state.try_get_or_default(key)
}
pub fn try_get_or_else<T>(&self, key: impl AsRef<str>, f: impl FnOnce() -> T) -> T
where
T: DeserializeOwned,
{
self.state.try_get_or_else(key, f)
}
pub fn set(&mut self, key: impl AsRef<str>, value: impl Into<Json>) -> 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 = &Json> {
self.state.values()
}
pub fn entries(&self) -> impl Iterator<Item = (&String, &Json)> {
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(self.id.clone(), duration))
.call(&self.app);
}
SaveStrategy::Throttle(duration) => {
self
.throttle_save_handle
.get_or_init(|| throttle(self.id.clone(), duration))
.call(&self.app);
}
}
Ok(())
}
pub fn save_now(&self) -> Result<()> {
let collection = self.app.store_collection();
if collection
.save_denylist
.as_ref()
.is_some_and(|it| it.contains(&self.id))
{
return Ok(());
}
write_file(self.path(), &self.state)
.sync(cfg!(feature = "file-sync-all"))
.pretty(collection.pretty)
.call()?;
#[cfg(tauri_store_tracing)]
debug!("store saved: {}", self.id);
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().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()
}
#[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)
}
pub fn set_options(&mut self, options: StoreOptions) -> Result<()> {
self.set_options_with_source(options, None::<&str>)
}
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()
.sync_denylist
.as_ref()
.is_some_and(|it| it.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);
}
}
impl<R: Runtime> fmt::Debug for Store<R> {
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 store_path<R>(app: &AppHandle<R>, id: &StoreId) -> PathBuf
where
R: Runtime,
{
append_filename(&app.store_collection().path(), id)
}
pub(crate) fn append_filename(path: &Path, id: &StoreId) -> PathBuf {
path.join(format!("{id}.{FILE_EXTENSION}"))
}