use crate::models::{
ClaudeQuotaSnapshot, CodexQuotaSnapshot, CopilotQuotaSnapshot, CursorQuotaSnapshot, QuotaSource,
};
use serde::Serialize;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
pub trait QuotaSnapshot: Clone + Default + Send + Serialize + 'static {
fn fetched_at(&self) -> i64;
fn is_present(&self) -> bool;
fn set_needs_login(&mut self, value: bool);
}
impl QuotaSnapshot for ClaudeQuotaSnapshot {
fn fetched_at(&self) -> i64 {
self.fetched_at
}
fn is_present(&self) -> bool {
self.five_hour.is_some()
|| self.seven_day.is_some()
|| (self.scoped_weekly.is_some() && self.scoped_label.is_some())
|| self.needs_login
}
fn set_needs_login(&mut self, value: bool) {
self.needs_login = value;
}
}
impl QuotaSnapshot for CodexQuotaSnapshot {
fn fetched_at(&self) -> i64 {
self.fetched_at
}
fn is_present(&self) -> bool {
self.source != QuotaSource::None || self.needs_login
}
fn set_needs_login(&mut self, value: bool) {
self.needs_login = value;
}
}
impl QuotaSnapshot for CopilotQuotaSnapshot {
fn fetched_at(&self) -> i64 {
self.fetched_at
}
fn is_present(&self) -> bool {
self.premium.is_some()
|| self.premium_unlimited
|| self.plan_type.is_some()
|| self.needs_login
}
fn set_needs_login(&mut self, value: bool) {
self.needs_login = value;
}
}
impl QuotaSnapshot for CursorQuotaSnapshot {
fn fetched_at(&self) -> i64 {
self.fetched_at
}
fn is_present(&self) -> bool {
self.total.is_some()
|| self.auto.is_some()
|| self.api.is_some()
|| self.plan_type.is_some()
|| self.needs_login
}
fn set_needs_login(&mut self, value: bool) {
self.needs_login = value;
}
}
pub enum QuotaOutcome<T> {
Data(T),
NeedsLogin,
Transient,
}
pub fn spawn_quota_worker<T, R, S>(
label: &'static str,
shared: Arc<Mutex<T>>,
shutdown: Arc<AtomicBool>,
refresh_secs: u64,
mut resolve: R,
save: S,
) -> JoinHandle<()>
where
T: QuotaSnapshot,
R: FnMut() -> QuotaOutcome<T> + Send + 'static,
S: Fn(&T) + Send + 'static,
{
std::thread::spawn(move || {
loop {
if shutdown.load(Ordering::Relaxed) {
break;
}
match catch_unwind(AssertUnwindSafe(&mut resolve)) {
Ok(QuotaOutcome::Data(snap)) => {
if let Ok(mut guard) = shared.lock() {
*guard = snap.clone();
}
save(&snap);
}
Ok(QuotaOutcome::NeedsLogin) => {
let mut updated = None;
if let Ok(mut guard) = shared.lock() {
guard.set_needs_login(true);
updated = Some(guard.clone());
}
if let Some(snap) = updated {
save(&snap);
}
}
Ok(QuotaOutcome::Transient) => {}
Err(_) => log::warn!("{label} quota worker panicked; keeping last snapshot"),
}
for _ in 0..(refresh_secs * 5) {
if shutdown.load(Ordering::Relaxed) {
break;
}
std::thread::sleep(Duration::from_millis(200));
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::CodexQuotaSnapshot;
#[test]
fn needs_login_snapshot_is_present() {
let snap = CodexQuotaSnapshot {
needs_login: true,
..Default::default()
};
assert!(snap.is_present());
}
}