mod hook;
mod store;
pub use hook::*;
use crate::{CacheKey, CacheableFn};
use dioxus::prelude::{GlobalSignal, Signal, document, spawn, use_hook};
use serde::Serialize;
use std::{
any::Any,
borrow::Cow,
cell::{Cell, RefCell},
collections::BTreeMap,
rc::Rc,
sync::OnceLock,
time::Duration,
};
pub(crate) const ENABLED: bool = !cfg!(feature = "server");
pub(crate) const STALE: i64 = i64::MIN;
const OWNER_KEY: &str = "\u{1f}owner";
#[derive(Clone, Debug)]
pub struct CacheConfig {
pub(crate) name: Cow<'static, str>,
pub(crate) share_for: Duration,
pub(crate) keep_for: Duration,
pub(crate) revalidate_on_focus: bool,
pub(crate) focus_gap: Duration,
}
impl CacheConfig {
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self {
name: name.into(),
share_for: Duration::from_secs(2),
keep_for: Duration::from_secs(7 * 24 * 60 * 60),
revalidate_on_focus: true,
focus_gap: Duration::from_secs(30),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn share_for(mut self, duration: Duration) -> Self {
self.share_for = duration;
self
}
pub fn keep_for(mut self, duration: Duration) -> Self {
self.keep_for = duration;
self
}
pub fn revalidate_on_focus(mut self, on: bool) -> Self {
self.revalidate_on_focus = on;
self
}
pub fn focus_gap(mut self, duration: Duration) -> Self {
self.focus_gap = duration;
self
}
}
static CONFIG: OnceLock<CacheConfig> = OnceLock::new();
pub(crate) fn config() -> &'static CacheConfig {
CONFIG.get_or_init(|| CacheConfig::new("g3-kit"))
}
pub fn use_client_cache(config: CacheConfig) {
use_hook(move || {
if !ENABLED {
return;
}
if CONFIG.set(config).is_err() {
tracing::warn!(
"g3-kit: `use_client_cache` ran more than once, or after the first cached \
read; the later settings are ignored. Call it once, first thing in the root \
component."
);
}
if self::config().revalidate_on_focus {
spawn(revalidate_on_focus());
}
});
}
async fn revalidate_on_focus() {
const SCRIPT: &str = r#"
const send = () => dioxus.send(null);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") send();
});
window.addEventListener("focus", send);
window.addEventListener("online", send);
await new Promise(() => {});
"#;
let mut events = document::eval(SCRIPT);
let mut last = now_ms();
while events.recv::<()>().await.is_ok() {
let gap = i64::try_from(config().focus_gap.as_millis()).unwrap_or(i64::MAX);
if now_ms().saturating_sub(last) >= gap {
last = now_ms();
invalidate_all_cached();
}
}
}
struct MemoryEntry {
value: Rc<dyn Any>,
fetched_at: i64,
}
thread_local! {
static MEMORY: RefCell<BTreeMap<CacheKey, MemoryEntry>> = const { RefCell::new(BTreeMap::new()) };
static OWNER_SET: Cell<bool> = const { Cell::new(false) };
static STORE_OWNED: Cell<Option<bool>> = const { Cell::new(None) };
}
pub(crate) static EPOCH: GlobalSignal<u64> = Signal::global(|| 0);
pub(crate) fn now_ms() -> i64 {
chrono::Utc::now().timestamp_millis()
}
pub(crate) fn remembered<T: Clone + 'static>(key: &CacheKey) -> Option<(T, i64)> {
if !ENABLED {
return None;
}
MEMORY.with_borrow(|memory| {
let entry = memory.get(key)?;
let value = entry.value.downcast_ref::<T>()?.clone();
Some((value, entry.fetched_at))
})
}
pub(crate) fn remember<T: 'static>(key: &CacheKey, value: T, fetched_at: i64) {
if ENABLED {
let entry = MemoryEntry {
value: Rc::new(value),
fetched_at,
};
MEMORY.with_borrow_mut(|memory| memory.insert(key.clone(), entry));
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Stored<T> {
saved_at: i64,
value: T,
}
pub(crate) async fn restore<T: serde::de::DeserializeOwned>(key: &CacheKey) -> Option<T> {
if !ENABLED || !store_owned().await {
return None;
}
let stored: Stored<T> = serde_json::from_str(&store::load(&key.storage_key()).await?).ok()?;
let keep_for = i64::try_from(config().keep_for.as_millis()).unwrap_or(i64::MAX);
(now_ms().saturating_sub(stored.saved_at) < keep_for).then_some(stored.value)
}
pub(crate) async fn persist<T: Serialize>(key: &CacheKey, value: &T) {
if !ENABLED || !OWNER_SET.get() {
return;
}
let stored = Stored {
saved_at: now_ms(),
value,
};
if let Ok(json) = serde_json::to_string(&stored) {
store::save(&key.storage_key(), &json).await;
}
}
async fn store_owned() -> bool {
if let Some(owned) = STORE_OWNED.get() {
return owned;
}
let owned = store::load(OWNER_KEY)
.await
.is_some_and(|owner| !owner.is_empty());
STORE_OWNED.set(Some(owned));
owned
}
pub async fn set_cache_owner(owner: Option<String>) {
if !ENABLED {
return;
}
OWNER_SET.set(true);
let owner = owner.unwrap_or_default();
if !owner.is_empty() && store::load(OWNER_KEY).await.as_deref() == Some(owner.as_str()) {
STORE_OWNED.set(Some(true));
return;
}
MEMORY.with_borrow_mut(BTreeMap::clear);
store::clear().await;
store::save(OWNER_KEY, &owner).await;
STORE_OWNED.set(Some(!owner.is_empty()));
*EPOCH.write() += 1;
}
fn mark_stale(matches: impl Fn(&CacheKey) -> bool) {
if !ENABLED {
return;
}
MEMORY.with_borrow_mut(|memory| {
for (key, entry) in memory.iter_mut() {
if matches(key) {
entry.fetched_at = STALE;
}
}
});
*EPOCH.write() += 1;
}
pub fn invalidate_cached<F, Args>(server_fn: F)
where
F: CacheableFn<Args>,
{
let _ = server_fn;
let name = crate::cache::key::fn_name::<F>();
mark_stale(|key| key.name() == name);
}
pub fn invalidate_cached_call<F, Args>(server_fn: F, args: Args)
where
F: CacheableFn<Args>,
Args: Serialize,
{
let target = CacheKey::of(&server_fn, &args);
mark_stale(|key| *key == target);
}
pub fn invalidate_cached_key(key: &CacheKey) {
mark_stale(|candidate| candidate == key);
}
pub fn invalidate_cached_name(name: &str) {
mark_stale(|key| key.name() == name);
}
pub fn invalidate_all_cached() {
mark_stale(|_| true);
}