use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use tokio::sync::Notify;
use super::types::OobState;
pub const FINISHED_RETENTION_SECS: u64 = 300;
pub const MAX_OPEN_REQUESTS: usize = 10_000;
pub const MAX_SEEN_DOCUMENTS: usize = 50_000;
pub const MAX_OPEN_POLLS: usize = 1_000;
#[derive(Debug, Clone, PartialEq)]
pub struct OobRecord {
pub request_id: String,
pub state: OobState,
pub version: u64,
pub purpose: String,
pub origin: String,
pub start_key: String,
pub start_network: Option<String>,
pub location: String,
pub browser: String,
pub os: String,
pub created_at: u64,
pub approver_key: Option<String>,
pub match_number: Option<String>,
pub match_number_delivered: bool,
pub identified_did: Option<String>,
pub step2_digest: Option<String>,
pub claim_deadline: u64,
pub decision_deadline: Option<u64>,
pub grant: Option<serde_json::Value>,
pub grant_not_after: Option<u64>,
pub finished_at: Option<u64>,
}
impl OobRecord {
pub fn lapsed(&self, now: u64) -> bool {
match self.state {
OobState::Pending => now > self.claim_deadline,
s if s.in_decision_window() => now > self.decision_deadline.unwrap_or(0),
_ => false,
}
}
pub fn next(&self, state: OobState, now: u64) -> OobRecord {
let mut n = self.clone();
n.state = state;
n.version = self.version + 1;
if state.is_final() {
n.start_network = None;
n.finished_at = Some(now);
}
n
}
}
#[derive(Default)]
struct Inner {
records: HashMap<String, OobRecord>,
seen: HashMap<(String, String), u64>,
waiters: HashMap<String, Arc<Notify>>,
open_polls: HashSet<String>,
polls_per_ip: HashMap<String, usize>,
}
#[derive(Clone, Default)]
pub struct OobStore {
inner: Arc<Mutex<Inner>>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum CreateError {
Duplicate,
Full,
}
#[derive(Debug, PartialEq, Eq)]
pub enum SeenError {
Replay,
Full,
}
#[derive(Debug, PartialEq, Eq)]
pub enum PollRefused {
AlreadyOpen,
AddressCap,
Full,
}
impl OobStore {
pub fn new() -> Self {
Self::default()
}
fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn create(&self, record: OobRecord, now: u64) -> Result<(), CreateError> {
let mut g = self.lock();
sweep(&mut g, now);
if g.records.contains_key(&record.request_id) {
return Err(CreateError::Duplicate);
}
if g.records.len() >= MAX_OPEN_REQUESTS {
return Err(CreateError::Full);
}
g.records.insert(record.request_id.clone(), record);
Ok(())
}
pub fn get(&self, request_id: &str) -> Option<OobRecord> {
self.lock().records.get(request_id).cloned()
}
pub fn compare_and_set(&self, expected_version: u64, next: OobRecord) -> bool {
let mut g = self.lock();
let ok = matches!(
g.records.get(&next.request_id),
Some(cur) if cur.version == expected_version
);
if ok {
let id = next.request_id.clone();
g.records.insert(id.clone(), next);
if let Some(n) = g.waiters.get(&id) {
n.notify_waiters();
}
}
ok
}
pub fn remember_document(
&self,
issuer: &str,
id: &str,
until: u64,
now: u64,
) -> Result<(), SeenError> {
let mut g = self.lock();
g.seen.retain(|_, exp| *exp >= now);
let key = (issuer.to_string(), id.to_string());
if g.seen.contains_key(&key) {
return Err(SeenError::Replay);
}
if g.seen.len() >= MAX_SEEN_DOCUMENTS {
return Err(SeenError::Full);
}
g.seen.insert(key, until);
Ok(())
}
pub fn waiter(&self, request_id: &str) -> Arc<Notify> {
self.lock()
.waiters
.entry(request_id.to_string())
.or_insert_with(|| Arc::new(Notify::new()))
.clone()
}
pub fn open_poll(
&self,
request_id: &str,
ip: &str,
max_per_ip: usize,
) -> Result<PollGuard, PollRefused> {
let mut g = self.lock();
if g.open_polls.contains(request_id) {
return Err(PollRefused::AlreadyOpen);
}
let n = g.polls_per_ip.get(ip).copied().unwrap_or(0);
if n >= max_per_ip {
return Err(PollRefused::AddressCap);
}
if g.open_polls.len() >= MAX_OPEN_POLLS {
return Err(PollRefused::Full);
}
g.open_polls.insert(request_id.to_string());
g.polls_per_ip.insert(ip.to_string(), n + 1);
Ok(PollGuard {
store: self.clone(),
request_id: request_id.to_string(),
ip: ip.to_string(),
})
}
#[cfg(test)]
pub fn record_count(&self) -> usize {
self.lock().records.len()
}
#[cfg(test)]
pub fn waiter_count(&self) -> usize {
self.lock().waiters.len()
}
#[cfg(test)]
pub fn sweep_now(&self, now: u64) {
sweep(&mut self.lock(), now);
}
}
pub struct PollGuard {
store: OobStore,
request_id: String,
ip: String,
}
impl Drop for PollGuard {
fn drop(&mut self) {
let mut g = self.store.lock();
g.open_polls.remove(&self.request_id);
if let Some(n) = g.polls_per_ip.get_mut(&self.ip) {
*n = n.saturating_sub(1);
if *n == 0 {
g.polls_per_ip.remove(&self.ip);
}
}
}
}
fn sweep(g: &mut Inner, now: u64) {
let cutoff = now.saturating_sub(FINISHED_RETENTION_SECS);
let stale: Vec<String> = g
.records
.values()
.filter(|r| {
let last = r
.finished_at
.unwrap_or_else(|| r.decision_deadline.unwrap_or(r.claim_deadline));
last < cutoff
})
.map(|r| r.request_id.clone())
.collect();
for id in stale {
g.records.remove(&id);
}
let Inner {
records,
waiters,
open_polls,
..
} = &mut *g;
waiters.retain(|id, _| records.contains_key(id) || open_polls.contains(id));
g.seen.retain(|_, exp| *exp >= now);
}