use std::path::PathBuf;
use redb::ReadableDatabase as _;
use crate::client::{default_base_url, fetch_event_by_id, fetch_event_by_key};
use crate::error::GlwEventCacheError;
use crate::types::{EventId, GlwEvent, GlwEventKey};
type CachedEvent = (Option<GlwEvent>, http_cache_semantics::CachePolicy);
const GLW_EVENT_BY_ID_TABLE: redb::TableDefinition<u32, String> =
redb::TableDefinition::new("glw_event_by_id");
const GLW_EVENT_BY_ID_POLICY_TABLE: redb::TableDefinition<u32, String> =
redb::TableDefinition::new("glw_event_by_id_policy");
const GLW_EVENT_BY_KEY_TABLE: redb::TableDefinition<String, String> =
redb::TableDefinition::new("glw_event_by_key");
const GLW_EVENT_BY_KEY_POLICY_TABLE: redb::TableDefinition<String, String> =
redb::TableDefinition::new("glw_event_by_key_policy");
#[expect(
clippy::module_name_repetitions,
reason = "GlwEventCache is the primary public type of this module"
)]
#[derive(Debug)]
pub struct GlwEventCache {
client: reqwest::Client,
base_url: url::Url,
db: redb::Database,
memory_by_id: lru::LruCache<EventId, CachedEvent>,
memory_by_key: lru::LruCache<GlwEventKey, CachedEvent>,
}
impl GlwEventCache {
pub fn new(
cache_directory: PathBuf,
base_url: Option<url::Url>,
) -> Result<Self, GlwEventCacheError> {
let client = crate::client::build_http_client();
let base_url = match base_url {
Some(u) => u,
None => default_base_url().map_err(GlwEventCacheError::FetchError)?,
};
let db = redb::Database::create(cache_directory.join("glw_event.redb"))?;
Ok(Self {
client,
base_url,
db,
memory_by_id: lru::LruCache::unbounded(),
memory_by_key: lru::LruCache::unbounded(),
})
}
#[tracing::instrument(skip(self))]
pub async fn get_event_by_id(
&mut self,
id: EventId,
) -> Result<Option<GlwEvent>, GlwEventCacheError> {
tracing::debug!("Retrieving GLW event by id {id}");
let cached = self.load_by_id_from_caches(id)?;
let (event, cache_policy) =
fetch_event_by_id(&self.client, &self.base_url, id, cached).await?;
self.store_by_id(id, event.as_ref(), &cache_policy)?;
Ok(event)
}
#[tracing::instrument(skip(self))]
pub async fn get_event_by_key(
&mut self,
key: &GlwEventKey,
) -> Result<Option<GlwEvent>, GlwEventCacheError> {
tracing::debug!("Retrieving GLW event by key {key}");
let cached = self.load_by_key_from_caches(key)?;
let (event, cache_policy) =
fetch_event_by_key(&self.client, &self.base_url, key, cached).await?;
self.store_by_key(key, event.as_ref(), &cache_policy)?;
Ok(event)
}
pub async fn refresh_event_by_id(
&mut self,
id: EventId,
) -> Result<Option<GlwEvent>, GlwEventCacheError> {
let (event, cache_policy) =
fetch_event_by_id(&self.client, &self.base_url, id, None).await?;
self.store_by_id(id, event.as_ref(), &cache_policy)?;
Ok(event)
}
pub async fn refresh_event_by_key(
&mut self,
key: &GlwEventKey,
) -> Result<Option<GlwEvent>, GlwEventCacheError> {
let (event, cache_policy) =
fetch_event_by_key(&self.client, &self.base_url, key, None).await?;
self.store_by_key(key, event.as_ref(), &cache_policy)?;
Ok(event)
}
fn load_by_id_from_caches(
&mut self,
id: EventId,
) -> Result<Option<CachedEvent>, GlwEventCacheError> {
if let Some(hit) = self.memory_by_id.get(&id) {
return Ok(Some(hit.to_owned()));
}
let read_txn = self.db.begin_read()?;
let policy = match read_txn.open_table(GLW_EVENT_BY_ID_POLICY_TABLE) {
Ok(table) => match table.get(id.get())? {
Some(value) => {
let policy: http_cache_semantics::CachePolicy =
serde_json::from_str(&value.value())?;
Some(policy)
}
None => None,
},
Err(_) => None,
};
let Some(policy) = policy else {
return Ok(None);
};
let value = match read_txn.open_table(GLW_EVENT_BY_ID_TABLE) {
Ok(table) => match table.get(id.get())? {
Some(value) => {
let event: Option<GlwEvent> = serde_json::from_str(&value.value())?;
event
}
None => None,
},
Err(_) => None,
};
Ok(Some((value, policy)))
}
fn load_by_key_from_caches(
&mut self,
key: &GlwEventKey,
) -> Result<Option<CachedEvent>, GlwEventCacheError> {
if let Some(hit) = self.memory_by_key.get(key) {
return Ok(Some(hit.to_owned()));
}
let read_txn = self.db.begin_read()?;
let policy = match read_txn.open_table(GLW_EVENT_BY_KEY_POLICY_TABLE) {
Ok(table) => match table.get(key.as_str().to_owned())? {
Some(value) => {
let policy: http_cache_semantics::CachePolicy =
serde_json::from_str(&value.value())?;
Some(policy)
}
None => None,
},
Err(_) => None,
};
let Some(policy) = policy else {
return Ok(None);
};
let value = match read_txn.open_table(GLW_EVENT_BY_KEY_TABLE) {
Ok(table) => match table.get(key.as_str().to_owned())? {
Some(value) => {
let event: Option<GlwEvent> = serde_json::from_str(&value.value())?;
event
}
None => None,
},
Err(_) => None,
};
Ok(Some((value, policy)))
}
fn store_by_id(
&mut self,
id: EventId,
event: Option<&GlwEvent>,
cache_policy: &http_cache_semantics::CachePolicy,
) -> Result<(), GlwEventCacheError> {
let write_txn = self.db.begin_write()?;
if cache_policy.is_storable() {
{
let mut policy_table = write_txn.open_table(GLW_EVENT_BY_ID_POLICY_TABLE)?;
policy_table.insert(id.get(), serde_json::to_string(cache_policy)?)?;
}
{
let mut value_table = write_txn.open_table(GLW_EVENT_BY_ID_TABLE)?;
value_table.insert(id.get(), serde_json::to_string(&event)?)?;
}
write_txn.commit()?;
self.memory_by_id
.put(id, (event.cloned(), cache_policy.clone()));
} else {
tracing::debug!("GLW event by id is not storable; evicting any stale entry");
{
let mut policy_table = write_txn.open_table(GLW_EVENT_BY_ID_POLICY_TABLE)?;
policy_table.remove(id.get())?;
}
{
let mut value_table = write_txn.open_table(GLW_EVENT_BY_ID_TABLE)?;
value_table.remove(id.get())?;
}
write_txn.commit()?;
self.memory_by_id.pop(&id);
}
Ok(())
}
fn store_by_key(
&mut self,
key: &GlwEventKey,
event: Option<&GlwEvent>,
cache_policy: &http_cache_semantics::CachePolicy,
) -> Result<(), GlwEventCacheError> {
let write_txn = self.db.begin_write()?;
if cache_policy.is_storable() {
{
let mut policy_table = write_txn.open_table(GLW_EVENT_BY_KEY_POLICY_TABLE)?;
policy_table.insert(
key.as_str().to_owned(),
serde_json::to_string(cache_policy)?,
)?;
}
{
let mut value_table = write_txn.open_table(GLW_EVENT_BY_KEY_TABLE)?;
value_table.insert(key.as_str().to_owned(), serde_json::to_string(&event)?)?;
}
write_txn.commit()?;
self.memory_by_key
.put(key.to_owned(), (event.cloned(), cache_policy.clone()));
} else {
tracing::debug!("GLW event by key is not storable; evicting any stale entry");
{
let mut policy_table = write_txn.open_table(GLW_EVENT_BY_KEY_POLICY_TABLE)?;
policy_table.remove(key.as_str().to_owned())?;
}
{
let mut value_table = write_txn.open_table(GLW_EVENT_BY_KEY_TABLE)?;
value_table.remove(key.as_str().to_owned())?;
}
write_txn.commit()?;
self.memory_by_key.pop(key);
}
Ok(())
}
}