feat(cache): add file cache, cache commands, and offline fallback

This commit is contained in:
Ciphercat 2026-03-05 18:28:32 +00:00
commit c0a3d744b2
3 changed files with 296 additions and 4 deletions

View file

@ -1,5 +1,8 @@
use clap::{Args, Subcommand};
use crate::cache::Cache;
use crate::error::IdxError;
#[derive(Debug, Args)]
pub struct CacheCmd {
#[command(subcommand)]
@ -11,3 +14,32 @@ pub enum CacheSubcommand {
Info,
Clear,
}
pub fn handle(cmd: &CacheCmd) -> Result<(), IdxError> {
let cache = Cache::new()?;
match &cmd.command {
CacheSubcommand::Info => {
let info = cache.info()?;
println!("path: {}", info.path.display());
println!("files: {}", info.files);
println!("size_bytes: {}", info.total_size);
println!(
"oldest: {}",
info.oldest
.map(|v| v.to_rfc3339())
.unwrap_or_else(|| "-".to_string())
);
println!(
"newest: {}",
info.newest
.map(|v| v.to_rfc3339())
.unwrap_or_else(|| "-".to_string())
);
}
CacheSubcommand::Clear => {
let removed = cache.clear()?;
println!("cleared {removed} files");
}
}
Ok(())
}

View file

@ -2,6 +2,7 @@ use clap::{Args, Subcommand};
use crate::api::types::{Interval, Period};
use crate::api::MarketDataProvider;
use crate::cache::Cache;
use crate::config::IdxConfig;
use crate::error::IdxError;
use crate::output::{render_history, render_quotes};
@ -28,13 +29,48 @@ pub fn handle(
cmd: &StocksCmd,
config: &IdxConfig,
provider: &dyn MarketDataProvider,
offline: bool,
no_cache: bool,
) -> Result<(), IdxError> {
let cache = Cache::new()?;
match &cmd.command {
StocksSubcommand::Quote { symbols } => {
let mut quotes = Vec::new();
for sym in symbols.iter().flat_map(|s| s.split(',')) {
let resolved = crate::api::resolve_symbol(sym, &config.exchange);
quotes.push(provider.quote(&resolved)?);
if !no_cache
&& let Some(q) = cache.get("quote", &resolved)?
{
quotes.push(q);
continue;
}
if offline {
let stale = cache
.get_stale("quote", &resolved)?
.ok_or_else(|| IdxError::CacheMiss(format!("quote/{resolved}")))?;
quotes.push(stale);
continue;
}
match provider.quote(&resolved) {
Ok(q) => {
if !no_cache {
cache.put("quote", &resolved, &q, config.quote_ttl)?;
}
quotes.push(q);
}
Err(err) => {
if !no_cache
&& let Some(stale) = cache.get_stale("quote", &resolved)?
{
eprintln!("warning: network failed, serving stale cache for {resolved}");
quotes.push(stale);
continue;
}
return Err(err);
}
}
}
render_quotes(&quotes, &config.output, config.no_color)
}
@ -44,8 +80,36 @@ pub fn handle(
interval,
} => {
let resolved = crate::api::resolve_symbol(symbol, &config.exchange);
let history = provider.history(&resolved, period, interval)?;
render_history(&resolved, &history, &config.output)
let key = format!("{}-{}", period.as_str(), interval.as_str());
if !no_cache
&& let Some(history) = cache.get::<Vec<crate::api::types::Ohlc>>("history", &format!("{resolved}-{key}"))?
{
return render_history(&resolved, &history, &config.output);
}
if offline {
let stale = cache
.get_stale::<Vec<crate::api::types::Ohlc>>("history", &format!("{resolved}-{key}"))?
.ok_or_else(|| IdxError::CacheMiss(format!("history/{resolved}-{key}")))?;
return render_history(&resolved, &stale, &config.output);
}
match provider.history(&resolved, period, interval) {
Ok(history) => {
if !no_cache {
cache.put("history", &format!("{resolved}-{key}"), &history, config.quote_ttl)?;
}
render_history(&resolved, &history, &config.output)
}
Err(err) => {
if !no_cache
&& let Some(stale) = cache.get_stale::<Vec<crate::api::types::Ohlc>>("history", &format!("{resolved}-{key}"))?
{
eprintln!("warning: network failed, serving stale cache for {resolved}");
return render_history(&resolved, &stale, &config.output);
}
Err(err)
}
}
}
}
}