mod resource;
mod save;
mod state;
mod watch;
#[cfg(feature = "unstable-async")]
mod unstable_async;
use crate::error::Result;
use crate::event::{
emit, ConfigPayload, EventSource, StatePayload, STORE_CONFIG_CHANGE_EVENT,
STORE_STATE_CHANGE_EVENT,
};
use crate::manager::ManagerExt;
use save::SaveHandle;
pub use save::SaveStrategy;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value as Json;
use std::collections::HashMap;
use std::fmt;
use std::io::ErrorKind::NotFound;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use tauri::{AppHandle, ResourceId, Runtime};
use watch::Watcher;
pub(crate) use resource::StoreResource;
pub use state::{StoreState, StoreStateExt};
pub use watch::WatcherResult;
#[cfg(not(feature = "unstable-async"))]
use {
save::{debounce, save_now, throttle},
tauri::async_runtime::spawn_blocking,
};
#[cfg(feature = "unstable-async")]
use tauri::async_runtime::spawn;
#[cfg(tauri_store_tracing)]
use tracing::{debug, warn};
type ResourceTuple<R> = (ResourceId, Arc<StoreResource<R>>);
pub struct Store<R: Runtime> {
app: AppHandle<R>,
pub(crate) id: String,
pub(crate) state: StoreState,
pub(crate) watchers: HashMap<u32, Watcher<R>>,
save_on_change: bool,
save_strategy: Option<SaveStrategy>,
debounce_save_handle: OnceLock<SaveHandle<R>>,
throttle_save_handle: OnceLock<SaveHandle<R>>,
}
impl<R: Runtime> Store<R> {
fn blocking_load(app: &AppHandle<R>, id: String) -> Result<ResourceTuple<R>> {
let path = store_path(app, &id);
let state = match std::fs::read(path) {
Ok(bytes) => serde_json::from_slice(&bytes)?,
Err(e) if e.kind() == NotFound => {
#[cfg(tauri_store_tracing)]
warn!("store not found: {id}, using default state");
StoreState::default()
}
Err(e) => return Err(e.into()),
};
#[cfg(tauri_store_tracing)]
debug!("store loaded: {id}");
let store = Self {
app: app.clone(),
id,
state,
watchers: HashMap::new(),
save_on_change: false,
save_strategy: None,
debounce_save_handle: OnceLock::new(),
throttle_save_handle: OnceLock::new(),
};
Ok(StoreResource::create(app, store))
}
pub fn id(&self) -> &str {
&self.id
}
pub fn path(&self) -> PathBuf {
store_path(&self.app, &self.id)
}
pub fn app_handle(&self) -> &AppHandle<R> {
&self.app
}
pub fn state(&self) -> &StoreState {
&self.state
}
pub fn try_state<T: DeserializeOwned>(&self) -> Result<T> {
self.state.parse()
}
pub fn get(&self, key: impl AsRef<str>) -> Option<&Json> {
self.state.get(key.as_ref())
}
pub fn try_get<T: DeserializeOwned>(&self, key: impl AsRef<str>) -> Result<T> {
self.state.try_get(key)
}
pub fn has(&self, key: impl AsRef<str>) -> bool {
self.state.contains_key(key.as_ref())
}
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.iter()
}
pub fn len(&self) -> usize {
self.state.len()
}
pub fn is_empty(&self) -> bool {
self.state.is_empty()
}
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);
}
#[expect(
clippy::needless_pass_by_value,
reason = "We are just anticipating the need for it."
)]
pub fn set_options_with_source<S>(&mut self, options: StoreOptions, source: S) -> Result<()>
where
S: Into<EventSource>,
{
if let Some(strategy) = options.save_strategy {
self.set_save_strategy(strategy);
}
if let Some(enabled) = options.save_on_change {
self.save_on_change = enabled;
}
self.on_config_change(source)
}
pub fn set_options(&mut self, options: StoreOptions) -> Result<()> {
self.set_options_with_source(options, None::<&str>)
}
pub fn watch<F>(&mut self, f: F) -> u32
where
F: Fn(AppHandle<R>) -> WatcherResult + Send + Sync + 'static,
{
let listener = Watcher::new(f);
let id = listener.id;
self.watchers.insert(id, listener);
id
}
pub fn unwatch(&mut self, id: u32) -> bool {
self.watchers.remove(&id).is_some()
}
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 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 call_watchers(&self) {
if self.watchers.is_empty() {
return;
}
let watchers = self
.watchers
.values()
.cloned()
.collect::<Vec<_>>();
for watcher in watchers {
let app = self.app.clone();
#[cfg(feature = "unstable-async")]
spawn(async move { watcher.call(app).await });
#[cfg(not(feature = "unstable-async"))]
spawn_blocking(move || watcher.call(app));
}
}
pub(crate) fn abort_pending_save(&self) {
if let Some(debounce_save_handle) = self.debounce_save_handle.get() {
debounce_save_handle.abort();
}
if let Some(throttle_save_handle) = self.throttle_save_handle.get() {
throttle_save_handle.abort();
}
}
}
#[cfg(not(feature = "unstable-async"))]
impl<R: Runtime> Store<R> {
pub(crate) fn load(app: &AppHandle<R>, id: impl AsRef<str>) -> Result<ResourceTuple<R>> {
Self::blocking_load(app, id.as_ref().to_owned())
}
pub fn set(&mut self, key: impl AsRef<str>, value: Json) -> Result<()> {
self.state.insert(key.as_ref().to_owned(), value);
self.on_state_change(None)
}
pub fn patch_with_source<S>(&mut self, state: StoreState, source: S) -> Result<()>
where
S: Into<EventSource>,
{
self.state.extend(state);
self.on_state_change(source)
}
pub fn patch(&mut self, state: StoreState) -> Result<()> {
self.patch_with_source(state, None)
}
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(())
}
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(duration, Arc::from(self.id.as_str())))
.call(&self.app);
}
SaveStrategy::Throttle(duration) => {
self
.throttle_save_handle
.get_or_init(|| throttle(duration, Arc::from(self.id.as_str())))
.call(&self.app);
}
}
Ok(())
}
pub fn save_now(&self) -> Result<()> {
save_now(self)
}
}
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("save_on_change", &self.save_on_change)
.field("save_strategy", &self.save_strategy)
.finish_non_exhaustive()
}
}
#[non_exhaustive]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreOptions {
pub save_strategy: Option<SaveStrategy>,
pub save_on_change: Option<bool>,
}
impl<R: Runtime> From<&Store<R>> for StoreOptions {
fn from(store: &Store<R>) -> Self {
Self {
save_strategy: store.save_strategy,
save_on_change: Some(store.save_on_change),
}
}
}
fn store_path<R: Runtime>(app: &AppHandle<R>, id: &str) -> PathBuf {
app
.store_collection()
.path()
.join(format!("{id}.json"))
}