use std::collections::HashMap;
use std::sync::Mutex;
use alloy::primitives::{B256, U256};
use crate::lane::{LaneKey, LaneState, PoolId};
pub trait PoolStateStore: Send + Sync {
fn load_all(&self) -> Result<Vec<LaneState>, StoreError>;
fn record(&self, state: &LaneState) -> Result<(), StoreError>;
fn forget(&self, key: LaneKey) -> Result<(), StoreError>;
fn get(&self, key: LaneKey) -> Result<Option<LaneState>, StoreError>;
fn flush(&self) -> Result<(), StoreError> {
Ok(())
}
fn set_registered_until(&self, key: LaneKey, registered_until: u64) -> Result<(), StoreError> {
let _ = (key, registered_until);
Ok(())
}
fn set_paid_cumulative(&self, key: LaneKey, paid_cumulative: U256) -> Result<(), StoreError> {
let _ = (key, paid_cumulative);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PendingSettle {
pub pool_id: B256,
pub settle_after: u64,
}
pub trait PendingSettleStore: Send + Sync {
fn record_pending(&self, entry: &PendingSettle) -> Result<(), StoreError>;
fn load_pending(&self) -> Result<Vec<PendingSettle>, StoreError>;
fn forget_pending(&self, pool_id: B256) -> Result<(), StoreError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CheckpointKey {
PoolOpened,
}
impl CheckpointKey {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::PoolOpened => "pool_opened_last_block",
}
}
}
pub trait KeyedCheckpointStore: Send + Sync {
fn load_checkpoint(&self, key: CheckpointKey) -> Result<Option<u64>, StoreError>;
fn record_checkpoint(&self, key: CheckpointKey, block: u64) -> Result<(), StoreError>;
fn flush_checkpoint(&self, key: CheckpointKey) -> Result<(), StoreError> {
let _ = key;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("pool store I/O: {0}")]
Io(#[from] std::io::Error),
#[error("pool store encoding: {0}")]
Codec(String),
#[error("pool store backend: {0}")]
Backend(String),
#[error("pool store on-disk schema version {found} not supported (max {supported})")]
UnsupportedSchema {
found: u32,
supported: u32,
},
#[error("corrupt record for {pool_id:?}: {detail}")]
Corrupt {
pool_id: Option<PoolId>,
detail: String,
},
#[error("failed to tighten permissions on {path}: {source}")]
PermissionTighten {
path: std::path::PathBuf,
source: std::io::Error,
},
#[error(
"another decdn process is using the pool store at {path} — run one fetch/pull \
at a time per --data-dir, or give each invocation a separate --data-dir"
)]
AlreadyOpen {
path: std::path::PathBuf,
},
#[error(
"the pool store at {path} was not shut down cleanly and needs a repair pass that only \
a writable open can run"
)]
NeedsRepair {
path: std::path::PathBuf,
},
#[error("no pool store exists at {path}")]
Absent {
path: std::path::PathBuf,
},
}
#[derive(Debug, Default)]
pub struct MemoryPoolStateStore {
inner: Mutex<HashMap<LaneKey, LaneState>>,
}
impl MemoryPoolStateStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.lock().map_or(0, |m| m.len())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl PoolStateStore for MemoryPoolStateStore {
fn load_all(&self) -> Result<Vec<LaneState>, StoreError> {
let guard = self
.inner
.lock()
.map_err(|err| StoreError::Backend(format!("memory store mutex poisoned: {err}")))?;
Ok(guard.values().cloned().collect())
}
fn record(&self, state: &LaneState) -> Result<(), StoreError> {
let mut guard = self
.inner
.lock()
.map_err(|err| StoreError::Backend(format!("memory store mutex poisoned: {err}")))?;
let mut next = state.clone();
if let Some(existing) = guard.get(&next.key()) {
next.registered_until = next.registered_until.max(existing.registered_until);
next.paid_cumulative = next.paid_cumulative.max(existing.paid_cumulative);
}
guard.insert(next.key(), next);
Ok(())
}
fn set_registered_until(&self, key: LaneKey, registered_until: u64) -> Result<(), StoreError> {
let mut guard = self
.inner
.lock()
.map_err(|err| StoreError::Backend(format!("memory store mutex poisoned: {err}")))?;
if let Some(state) = guard.get_mut(&key) {
state.registered_until = state.registered_until.max(registered_until);
}
Ok(())
}
fn set_paid_cumulative(&self, key: LaneKey, paid_cumulative: U256) -> Result<(), StoreError> {
let mut guard = self
.inner
.lock()
.map_err(|err| StoreError::Backend(format!("memory store mutex poisoned: {err}")))?;
if let Some(state) = guard.get_mut(&key) {
state.paid_cumulative = state.paid_cumulative.max(paid_cumulative);
}
Ok(())
}
fn forget(&self, key: LaneKey) -> Result<(), StoreError> {
let mut guard = self
.inner
.lock()
.map_err(|err| StoreError::Backend(format!("memory store mutex poisoned: {err}")))?;
guard.remove(&key);
Ok(())
}
fn get(&self, key: LaneKey) -> Result<Option<LaneState>, StoreError> {
let guard = self
.inner
.lock()
.map_err(|err| StoreError::Backend(format!("memory store mutex poisoned: {err}")))?;
Ok(guard.get(&key).cloned())
}
}
#[cfg(test)]
mod tests;