use nostr_sdk::prelude::Event;
use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Default)]
struct CommunityCache {
inflight: HashSet<String>,
history_start: HashSet<String>,
oldest_cursor: HashMap<String, u64>,
newest_cursor: HashMap<String, u64>,
}
struct CacheKey;
fn with_cache<R>(f: impl FnOnce(&mut CommunityCache) -> R) -> R {
let cache = crate::db::current_session().scoped::<CacheKey, Mutex<CommunityCache>>();
let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
f(&mut guard)
}
pub fn try_begin_page_fetch(key: &str) -> bool {
with_cache(|c| c.inflight.insert(key.to_string()))
}
pub fn end_page_fetch(key: &str) {
with_cache(|c| c.inflight.remove(key));
}
pub fn is_at_history_start(channel_id: &str) -> bool {
with_cache(|c| c.history_start.contains(channel_id))
}
pub fn mark_history_start(channel_id: &str) {
with_cache(|c| c.history_start.insert(channel_id.to_string()));
}
pub fn oldest_cursor(channel_id: &str) -> Option<u64> {
with_cache(|c| c.oldest_cursor.get(channel_id).copied())
}
pub fn advance_oldest_cursor(channel_id: &str, oldest_secs: u64) {
with_cache(|c| {
let slot = c.oldest_cursor.entry(channel_id.to_string()).or_insert(oldest_secs);
*slot = (*slot).min(oldest_secs);
});
}
pub fn newest_cursor(channel_id: &str) -> Option<u64> {
with_cache(|c| c.newest_cursor.get(channel_id).copied())
}
pub fn advance_newest_cursor(channel_id: &str, newest_secs: u64) {
with_cache(|c| {
let slot = c.newest_cursor.entry(channel_id.to_string()).or_insert(newest_secs);
*slot = (*slot).max(newest_secs);
});
}
pub fn clear_channel_floors(channel_id: &str) {
with_cache(|c| {
c.history_start.remove(channel_id);
c.oldest_cursor.remove(channel_id);
});
}
pub fn clear_channel_sync_state(channel_id: &str) {
with_cache(|c| {
c.history_start.remove(channel_id);
c.oldest_cursor.remove(channel_id);
c.newest_cursor.remove(channel_id);
});
}
pub(crate) const PRELOAD_TTL: Duration = Duration::from_secs(120);
const PRELOAD_MAX: usize = 8;
const PRELOAD_ADOPT_TIMEOUT: Duration = Duration::from_secs(12);
enum PreloadState {
Pending,
Ready(Vec<Event>),
}
struct Preload {
state: PreloadState,
fetched_at: Instant,
}
struct PreloadKey;
fn with_preload<R>(f: impl FnOnce(&mut HashMap<String, Preload>) -> R) -> R {
let map = crate::db::current_session().scoped::<PreloadKey, Mutex<HashMap<String, Preload>>>();
let mut guard = map.lock().unwrap_or_else(|e| e.into_inner());
f(&mut guard)
}
pub fn begin_preload(community_id: &str) {
with_preload(|map| {
map.retain(|_, p| p.fetched_at.elapsed() < PRELOAD_TTL);
if map.len() >= PRELOAD_MAX {
if let Some(oldest) = map.iter().min_by_key(|(_, p)| p.fetched_at).map(|(k, _)| k.clone()) {
map.remove(&oldest);
}
}
map.insert(
community_id.to_string(),
Preload { state: PreloadState::Pending, fetched_at: Instant::now() },
);
});
}
pub fn finish_preload(community_id: &str, page: Vec<Event>) {
with_preload(|map| {
if let Some(p) = map.get_mut(community_id) {
p.state = PreloadState::Ready(page);
p.fetched_at = Instant::now();
}
});
}
pub fn abort_preload(community_id: &str) {
with_preload(|map| map.remove(community_id));
}
pub fn take_ready_preload(community_id: &str) -> Option<Vec<Event>> {
with_preload(|map| {
let fresh = matches!(map.get(community_id), Some(p)
if p.fetched_at.elapsed() < PRELOAD_TTL && matches!(p.state, PreloadState::Ready(_)));
if !fresh {
return None;
}
match map.remove(community_id) {
Some(Preload { state: PreloadState::Ready(page), .. }) => Some(page),
_ => None,
}
})
}
pub async fn take_or_await_preload(community_id: &str) -> Option<Vec<Event>> {
let deadline = Instant::now() + PRELOAD_ADOPT_TIMEOUT;
loop {
let adopted = with_preload(|map| match map.get(community_id) {
Some(p) if p.fetched_at.elapsed() < PRELOAD_TTL => {
if matches!(p.state, PreloadState::Ready(_)) {
return match map.remove(community_id) {
Some(Preload { state: PreloadState::Ready(page), .. }) => Some(Some(page)),
_ => Some(None),
};
}
None }
_ => Some(None), });
if let Some(outcome) = adopted {
return outcome;
}
if Instant::now() >= deadline {
return None;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
pub fn clear() {
with_preload(|map| map.clear());
with_cache(|c| *c = CommunityCache::default());
}