use crate::error::Result;
use crate::ManagerExt;
use serde::Serialize;
use serde_json::Value as Json;
use std::path::PathBuf;
use std::{fmt, io};
use tauri::{AppHandle, Emitter, EventTarget, Runtime};
#[cfg(feature = "ahash")]
use ahash::HashMap;
#[cfg(not(feature = "ahash"))]
use std::collections::HashMap;
pub type StoreState = HashMap<String, Json>;
const CHANGE_EVENT: &str = "pinia://change";
pub struct Store<R: Runtime> {
pub(crate) id: String,
pub(crate) state: StoreState,
app: AppHandle<R>,
}
impl<R: Runtime> Store<R> {
pub(crate) fn load(app: AppHandle<R>, id: impl AsRef<str>) -> Result<Self> {
let id = id.as_ref().to_owned();
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() == io::ErrorKind::NotFound => {
#[cfg(feature = "tracing")]
tracing::warn!("pinia store not found: {id}, using default state");
StoreState::default()
}
Err(e) => return Err(e.into()),
};
#[cfg(feature = "tracing")]
tracing::trace!("pinia store loaded: {id}");
Ok(Self { id, state, app })
}
#[cfg(not(feature = "async-pinia"))]
pub fn save(&self) -> Result<()> {
use std::fs::{self, File};
use std::io::Write;
let pinia = self.app.pinia();
fs::create_dir_all(pinia.path())?;
let bytes = serde_json::to_vec(&self.state)?;
let mut file = File::create(self.path())?;
file.write_all(&bytes)?;
Ok(())
}
#[cfg(feature = "async-pinia")]
pub async fn save(&self) -> Result<()> {
use tokio::fs::{self, File};
use tokio::io::AsyncWriteExt;
let pinia = self.app.pinia();
fs::create_dir_all(pinia.path()).await?;
let path = pinia.path().join(format!("{}.json", self.id));
let bytes = serde_json::to_vec(&self.state)?;
let mut file = File::create(path).await?;
file.write_all(&bytes).await?;
Ok(())
}
pub fn id(&self) -> &str {
&self.id
}
pub fn path(&self) -> PathBuf {
store_path(&self.app, &self.id)
}
pub(crate) fn patch_with_source<'a, S>(&mut self, state: StoreState, source: S) -> Result<()>
where
S: Into<Option<&'a str>>,
{
self.state.extend(state);
self.emit(source)
}
pub fn patch(&mut self, state: StoreState) -> Result<()> {
self.patch_with_source(state, None)
}
pub fn set(&mut self, key: impl AsRef<str>, value: Json) -> Result<()> {
self.state.insert(key.as_ref().to_owned(), value);
self.emit(None)
}
pub fn get(&self, key: impl AsRef<str>) -> Option<&Json> {
self.state.get(key.as_ref())
}
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()
}
fn emit<'a, S>(&self, source: S) -> Result<()>
where
S: Into<Option<&'a str>>,
{
let pinia = self.app.pinia();
if pinia.sync_denylist.contains(&self.id) {
#[cfg(feature = "tracing")]
tracing::trace!("store {} is in the denylist, skipping emit", self.id);
return Ok(());
}
let payload = Payload::from(self);
let source: Option<&str> = source.into();
if let Some(source) = source {
payload.emit_filter(&self.app, source)
} else {
payload.emit_all(&self.app)
}
}
}
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)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, Serialize)]
struct Payload<'a> {
id: &'a str,
state: &'a StoreState,
}
impl<'a> Payload<'a> {
fn emit_all<R: Runtime>(&self, app: &AppHandle<R>) -> Result<()> {
#[cfg(feature = "tracing")]
tracing::trace!(event = CHANGE_EVENT, payload = ?self);
app.emit_filter(CHANGE_EVENT, self, |target| {
matches!(target, EventTarget::WebviewWindow { .. })
})?;
Ok(())
}
fn emit_filter<R: Runtime>(&self, app: &AppHandle<R>, source: &str) -> Result<()> {
#[cfg(feature = "tracing")]
tracing::trace!(event = CHANGE_EVENT, source, payload = ?self);
app.emit_filter(CHANGE_EVENT, self, |target| match target {
EventTarget::WebviewWindow { label } => label != source,
_ => false,
})?;
Ok(())
}
}
impl<'a, R: Runtime> From<&'a Store<R>> for Payload<'a> {
fn from(store: &'a Store<R>) -> Self {
Self { id: &store.id, state: &store.state }
}
}
fn store_path<R: Runtime>(app: &AppHandle<R>, id: &str) -> PathBuf {
app.pinia().path().join(format!("{id}.json"))
}