use nostr_sdk::Event;
use std::collections::{HashMap, HashSet};
use std::sync::{LazyLock, Mutex, MutexGuard};
use std::time::{Duration, Instant};
#[derive(Default)]
struct CommunityCache {
generation: u64,
inflight: HashSet<String>,
history_start: HashSet<String>,
oldest_cursor: HashMap<String, u64>,
newest_cursor: HashMap<String, u64>,
}
static CACHE: LazyLock<Mutex<CommunityCache>> = LazyLock::new(|| Mutex::new(CommunityCache::default()));
fn locked() -> MutexGuard<'static, CommunityCache> {
let generation = crate::state::current_session_generation();
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
if cache.generation != generation {
*cache = CommunityCache { generation, ..Default::default() };
}
cache
}
pub fn try_begin_page_fetch(key: &str) -> bool {
locked().inflight.insert(key.to_string())
}
pub fn end_page_fetch(key: &str) {
locked().inflight.remove(key);
}
pub fn is_at_history_start(channel_id: &str) -> bool {
locked().history_start.contains(channel_id)
}
pub fn mark_history_start(channel_id: &str) {
locked().history_start.insert(channel_id.to_string());
}
pub fn oldest_cursor(channel_id: &str) -> Option<u64> {
locked().oldest_cursor.get(channel_id).copied()
}
pub fn advance_oldest_cursor(channel_id: &str, oldest_secs: u64) {
let mut cache = locked();
let slot = cache.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> {
locked().newest_cursor.get(channel_id).copied()
}
pub fn advance_newest_cursor(channel_id: &str, newest_secs: u64) {
let mut cache = locked();
let slot = cache.newest_cursor.entry(channel_id.to_string()).or_insert(newest_secs);
*slot = (*slot).max(newest_secs);
}
pub fn clear_channel_floors(channel_id: &str) {
let mut cache = locked();
cache.history_start.remove(channel_id);
cache.oldest_cursor.remove(channel_id);
}
pub fn clear_channel_sync_state(channel_id: &str) {
let mut cache = locked();
cache.history_start.remove(channel_id);
cache.oldest_cursor.remove(channel_id);
cache.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,
generation: u64,
}
static PRELOAD: LazyLock<Mutex<HashMap<String, Preload>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
fn preload_locked() -> MutexGuard<'static, HashMap<String, Preload>> {
PRELOAD.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn begin_preload(community_id: &str) {
let generation = crate::state::current_session_generation();
let mut map = preload_locked();
map.retain(|_, p| p.generation == generation && 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(), generation },
);
}
pub fn finish_preload(community_id: &str, page: Vec<Event>) {
let generation = crate::state::current_session_generation();
let mut map = preload_locked();
let stale = match map.get_mut(community_id) {
Some(p) if p.generation == generation => {
p.state = PreloadState::Ready(page);
p.fetched_at = Instant::now();
false
}
Some(_) => true,
None => false,
};
if stale {
map.remove(community_id);
}
}
pub fn abort_preload(community_id: &str) {
preload_locked().remove(community_id);
}
pub fn take_ready_preload(community_id: &str) -> Option<Vec<Event>> {
let generation = crate::state::current_session_generation();
let mut map = preload_locked();
let fresh = matches!(map.get(community_id), Some(p)
if p.generation == generation
&& 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 generation = crate::state::current_session_generation();
let mut map = preload_locked();
match map.get(community_id) {
Some(p) if p.generation == generation && 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(page),
_ => None,
};
}
}
_ => return None, }
}
if Instant::now() >= deadline {
return None;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
pub fn clear() {
*PRELOAD.lock().unwrap_or_else(|e| e.into_inner()) = HashMap::new();
*CACHE.lock().unwrap_or_else(|e| e.into_inner()) = CommunityCache {
generation: crate::state::current_session_generation(),
..Default::default()
};
}