use std::sync::Arc;
use std::sync::Mutex;
use std::sync::PoisonError;
use leptoskit_query::BoxFuture;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
#[allow(dead_code, reason = "only the browser and native stores name it")]
pub(crate) const STORAGE_KEY: &str = "leptoskit.rpc.offline_queue";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct QueuedCall {
pub kind: QueuedKind,
pub name: String,
#[serde(default)]
pub args: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QueuedKind {
Mutation,
Action,
}
pub trait QueueStore: Send + Sync + 'static {
fn load(&self) -> BoxFuture<'static, Vec<QueuedCall>>;
fn save(&self, calls: Vec<QueuedCall>) -> BoxFuture<'static, ()>;
}
#[derive(Clone, Debug, Default)]
pub struct MemoryQueue(Arc<Mutex<Vec<QueuedCall>>>);
impl MemoryQueue {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn calls(&self) -> Vec<QueuedCall> {
self.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
}
impl QueueStore for MemoryQueue {
fn load(&self) -> BoxFuture<'static, Vec<QueuedCall>> {
let calls = self.calls();
Box::pin(std::future::ready(calls))
}
fn save(&self, calls: Vec<QueuedCall>) -> BoxFuture<'static, ()> {
*self.0.lock().unwrap_or_else(PoisonError::into_inner) = calls;
Box::pin(std::future::ready(()))
}
}
pub(crate) fn platform_store() -> Option<Arc<dyn QueueStore>> {
#[cfg(all(
feature = "web",
not(feature = "guest"),
target_arch = "wasm32",
target_os = "unknown"
))]
return Some(Arc::new(browser::LocalStorage));
#[cfg(all(feature = "guest", target_arch = "wasm32"))]
return Some(Arc::new(native::Settings));
#[allow(unreachable_code, reason = "a platform with a store returned it above")]
None
}
#[cfg(all(
feature = "web",
not(feature = "guest"),
target_arch = "wasm32",
target_os = "unknown"
))]
mod browser {
use leptoskit_query::BoxFuture;
use super::QueueStore;
use super::QueuedCall;
use super::STORAGE_KEY;
pub(super) struct LocalStorage;
fn storage() -> Option<web_sys::Storage> {
web_sys::window()?.local_storage().ok().flatten()
}
impl QueueStore for LocalStorage {
fn load(&self) -> BoxFuture<'static, Vec<QueuedCall>> {
let calls = storage()
.and_then(|storage| storage.get_item(STORAGE_KEY).ok().flatten())
.and_then(|text| serde_json::from_str(&text).ok())
.unwrap_or_default();
Box::pin(std::future::ready(calls))
}
fn save(&self, calls: Vec<QueuedCall>) -> BoxFuture<'static, ()> {
if let Some(storage) = storage() {
if calls.is_empty() {
let _ = storage.remove_item(STORAGE_KEY);
} else if let Ok(text) = serde_json::to_string(&calls) {
let _ = storage.set_item(STORAGE_KEY, &text);
}
}
Box::pin(std::future::ready(()))
}
}
}
#[cfg(all(feature = "guest", target_arch = "wasm32"))]
mod native {
use leptoskit_plugin_store::Change;
use leptoskit_plugin_store::Save;
use leptoskit_plugin_store::guest::Store;
use leptoskit_query::BoxFuture;
use super::QueueStore;
use super::QueuedCall;
use super::STORAGE_KEY;
pub(super) struct Settings;
impl QueueStore for Settings {
fn load(&self) -> BoxFuture<'static, Vec<QueuedCall>> {
Box::pin(send_wrapper::SendWrapper::new(async {
let Ok(store) = Store::new() else {
return Vec::new();
};
match store.get(STORAGE_KEY).await {
Ok(Some(value)) => serde_json::from_value(value).unwrap_or_default(),
_ => Vec::new(),
}
}))
}
fn save(&self, calls: Vec<QueuedCall>) -> BoxFuture<'static, ()> {
Box::pin(send_wrapper::SendWrapper::new(async move {
let Ok(store) = Store::new() else {
return;
};
let change = if calls.is_empty() {
Change::Remove(STORAGE_KEY.to_owned())
} else {
match serde_json::to_value(&calls) {
Ok(value) => Change::Set(STORAGE_KEY.to_owned(), value),
Err(_) => return,
}
};
let _ = store.apply(&[change], Save::Commit).await;
}))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_queued_call_round_trips() {
let call = QueuedCall {
kind: QueuedKind::Mutation,
name: "send".to_owned(),
args: serde_json::json!({ "body": "hi" }),
key: Some("k".to_owned()),
};
let text = serde_json::to_string(&call).unwrap();
assert_eq!(serde_json::from_str::<QueuedCall>(&text).unwrap(), call);
assert!(platform_store().is_none());
}
#[test]
fn a_memory_queue_is_shared_by_its_clones() {
let queue = MemoryQueue::new();
let other = queue.clone();
futures::executor::block_on(queue.save(vec![QueuedCall {
kind: QueuedKind::Action,
name: "charge".to_owned(),
args: Value::Null,
key: None,
}]));
assert_eq!(other.calls().len(), 1);
let loaded = futures::executor::block_on(other.load());
assert_eq!(loaded, queue.calls());
}
}