///|
/// Parsed Cache-Control header directives
pub(all) struct CacheDirectives {
max_age : Int?
no_cache : Bool
no_store : Bool
must_revalidate : Bool
public_ : Bool
private_ : Bool
immutable : Bool
} derive(Debug)
///|
pub fn CacheDirectives::default() -> CacheDirectives {
{
max_age: None,
no_cache: false,
no_store: false,
must_revalidate: false,
public_: false,
private_: false,
immutable: false,
}
}
///|
pub impl Show for CacheDirectives with fn output(self, logger) {
logger.write_string("{max_age: ")
logger.write_object(to_repr(self.max_age))
logger.write_string(", no_cache: ")
self.no_cache.output(logger)
logger.write_string(", no_store: ")
self.no_store.output(logger)
logger.write_string(", must_revalidate: ")
self.must_revalidate.output(logger)
logger.write_string(", public_: ")
self.public_.output(logger)
logger.write_string(", private_: ")
self.private_.output(logger)
logger.write_string(", immutable: ")
self.immutable.output(logger)
logger.write_string("}")
}
///|
/// Parse a Cache-Control header value into CacheDirectives.
pub fn parse_cache_control(header : String) -> CacheDirectives {
let mut max_age : Int? = None
let mut no_cache = false
let mut no_store = false
let mut must_revalidate = false
let mut public_ = false
let mut private_ = false
let mut immutable = false
let parts = header.split(",")
for part in parts {
let trimmed = part.trim(chars=" ").to_owned().to_lower()
if trimmed == "no-cache" {
no_cache = true
} else if trimmed == "no-store" {
no_store = true
} else if trimmed == "must-revalidate" {
must_revalidate = true
} else if trimmed == "public" {
public_ = true
} else if trimmed == "private" {
private_ = true
} else if trimmed == "immutable" {
immutable = true
} else if trimmed.has_prefix("max-age=") {
let value_str = trimmed[8:].to_owned()
max_age = parse_int_string(value_str)
}
}
{ max_age, no_cache, no_store, must_revalidate, public_, private_, immutable }
}
///|
/// Parse an integer from a string, returning None on failure.
fn parse_int_string(s : String) -> Int? {
if s.length() == 0 {
return None
}
let mut result = 0
for i = 0; i < s.length(); i = i + 1 {
let c = s[i]
if c >= '0' && c <= '9' {
result = result * 10 + (c.to_int() - '0'.to_int())
} else {
return None
}
}
Some(result)
}
///|
/// Cached HTTP response entry
pub(all) struct CacheEntry {
url : String
status : Int
headers : Map[String, String]
body : String
etag : String?
last_modified : String?
directives : CacheDirectives
stored_at : Double
} derive(Debug)
///|
/// Shared cache backend contract used by cached_fetch.
pub(open) trait HttpCacheBackend {
fn lookup(Self, String) -> CacheEntry?
fn store(Self, CacheEntry) -> Unit
fn remove(Self, String) -> Unit
fn clear(Self) -> Unit
}
///|
/// Check whether a cache entry is still fresh at the given time.
pub fn is_fresh(entry : CacheEntry, now : Double) -> Bool {
let d = entry.directives
// no_store: never use cache
if d.no_store {
return false
}
// immutable: always fresh
if d.immutable {
return true
}
// no_cache: must revalidate every time
if d.no_cache {
return false
}
// max_age: explicit freshness lifetime
match d.max_age {
Some(max_age) => {
let elapsed = now - entry.stored_at
let fresh = elapsed < max_age.to_double()
// must-revalidate: stale entries must NOT be served without revalidation
// (When fresh, serve normally. When stale, force revalidation — which
// is the default behavior, so must-revalidate is enforced by returning false.)
return fresh
}
None => ()
}
// must-revalidate without max-age: always stale (must revalidate)
if d.must_revalidate {
return false
}
// Heuristic freshness based on last_modified
match entry.last_modified {
Some(lm_str) => {
let lm_epoch = parse_epoch_string(lm_str)
let resource_age = entry.stored_at - lm_epoch
if resource_age > 0.0 {
let heuristic_lifetime = resource_age * 0.1
let elapsed = now - entry.stored_at
return elapsed < heuristic_lifetime
}
false
}
None => false
}
}
///|
/// Check if an HTTP status code is cacheable.
/// Only 200, 203, 204, 206, 300, 301, 308 are cacheable by default (RFC 9111 §4.2.2).
pub fn is_cacheable_status(status : Int) -> Bool {
status == 200 ||
status == 203 ||
status == 204 ||
status == 206 ||
status == 300 ||
status == 301 ||
status == 308
}
///|
/// Parse a numeric string to Double. Returns 0.0 on failure.
fn parse_epoch_string(s : String) -> Double {
if s.length() == 0 {
return 0.0
}
let mut result = 0.0
for i = 0; i < s.length(); i = i + 1 {
let c = s[i]
if c >= '0' && c <= '9' {
result = result * 10.0 + (c.to_int() - '0'.to_int()).to_double()
} else {
return 0.0
}
}
result
}
///|
/// Get current time in seconds since epoch.
/// Delegates to platform-specific FFI.
pub fn now_seconds() -> Double {
now_seconds_ffi() / 1000.0
}