use super::*;
impl Scraper {
pub(super) async fn scrape_cached(
&self,
url: &str,
opts: &ScrapeOptions,
mode: RobotsMode,
) -> Result<Document, ApiError> {
let capture =
!opts.wants("rawHtml") && self.audit.as_ref().is_some_and(|a| a.wants_raw_html());
let with_raw = capture.then(|| {
let mut o = opts.clone();
o.formats.push(pagesift_api::types::Format::new("rawHtml"));
o
});
let fetch_opts = with_raw.as_ref().unwrap_or(opts);
let Some((store, key)) = self.cache_key(url, opts) else {
return self.fetch_and_extract(url, fetch_opts, mode).await;
};
let policy = pagesift_store::CachePolicy::from_firecrawl(
opts.max_age,
opts.min_age,
Some(opts.store_in_cache),
);
let lookup = {
let (store, key) = (store.clone(), key.clone());
cpu_task(move || {
store
.cache()
.lookup(&key, &policy)
.map(|found| match found {
pagesift_store::CacheLookup::Hit(entry) => {
match pagesift_sink::cache::decode(&entry.value) {
Some(doc) => Cached::Hit(Box::new(doc), entry.stored_at),
None => Cached::Miss,
}
}
pagesift_store::CacheLookup::NoCachedData => Cached::NoData,
pagesift_store::CacheLookup::Miss => Cached::Miss,
})
})
.await?
};
match lookup {
Ok(Cached::Hit(doc, stored_at)) => {
let mut doc = *doc;
doc.metadata.cache_state = Some("hit".into());
doc.metadata.cached_at = Some(pagesift_api::rfc3339(stored_at));
return Ok(doc);
}
Ok(Cached::NoData) => {
return Err(ApiError::new(
ErrorCode::NotFound,
"no cached data matches minAge for this URL",
));
}
Ok(Cached::Miss) => {}
Err(e) => tracing::warn!("cache lookup failed: {e}"),
}
let mut doc = self.fetch_and_extract(url, fetch_opts, mode).await?;
doc.metadata.cache_state = Some("miss".into());
let captured = if capture { doc.raw_html.take() } else { None };
if policy.store_in_cache {
let (back, r) = cpu_task(move || {
let r =
pagesift_sink::cache::encode(&doc).map(|bytes| store.cache().put(&key, &bytes));
(doc, r)
})
.await?;
doc = back;
if let Ok(Err(e)) = r {
tracing::warn!("cache write failed: {e}");
}
}
if captured.is_some() {
doc.raw_html = captured;
}
Ok(doc)
}
pub(super) fn cache_key(
&self,
url: &str,
opts: &ScrapeOptions,
) -> Option<(pagesift_store::Store, Vec<u8>)> {
let store = self.store.clone()?;
if !opts.headers.is_empty()
|| opts.discover_scripts
|| opts.explain
|| !opts.actions.is_empty()
|| opts.if_none_match.is_some()
|| opts.if_modified_since.is_some()
{
return None;
}
let mut formats: Vec<&str> = opts.formats.iter().map(|f| f.kind.as_str()).collect();
formats.sort_unstable();
let mut key = serde_json::json!([
2,
url,
formats,
opts.only_main_content,
opts.include_tags,
opts.exclude_tags,
opts.engine.as_deref().unwrap_or("auto"),
opts.mobile,
opts.wait_for,
opts.block_ads,
opts.location,
]);
if let (Some(p), Some(items)) = (&opts.parsers, key.as_array_mut()) {
items.push(p.clone());
}
Some((store, key.to_string().into_bytes()))
}
}
enum Cached {
Hit(Box<Document>, std::time::SystemTime),
NoData,
Miss,
}