///|
/// Source selected for a response's freshness lifetime.
pub(all) enum FreshnessSource {
SharedMaxAge
MaxAge
ExpiresDate
LastModifiedHeuristic
NoFreshnessInformation
} derive(Debug, Eq)
///|
pub(all) struct FreshnessResult {
source : FreshnessSource
freshness_lifetime : Int64
current_age : Int64
remaining_freshness : Int64
fresh : Bool
trace : Array[TraceStep]
diagnostics : Array[Diagnostic]
} derive(Debug, Eq)
///|
/// Calculate response freshness lifetime and compare it with current age.
pub fn calculate_freshness(
stored : StoredResponse,
mode : CacheMode,
now : Timestamp,
policy? : CachePolicy = default_policy(),
) -> FreshnessResult {
let age = calculate_current_age(stored, now)
let diagnostics = age.diagnostics.copy()
let trace = age.trace.copy()
let control = parse_cache_control(stored.response.headers)
append_diagnostics(diagnostics, control.diagnostics)
let (source, lifetime) = select_freshness_lifetime(
stored, mode, now, policy, control, diagnostics, trace,
)
let current_age = age.breakdown.current_age
let remaining = if lifetime > current_age {
lifetime - current_age
} else {
0L
}
let fresh = current_age < lifetime
trace.push(
step(
if fresh {
"FRESHNESS_FRESH"
} else {
"FRESHNESS_STALE"
},
if fresh {
"stored response is fresh"
} else {
"stored response is stale"
},
"RFC 9111 4.2",
),
)
{
source,
freshness_lifetime: lifetime,
current_age,
remaining_freshness: remaining,
fresh,
trace,
diagnostics,
}
}
///|
fn select_freshness_lifetime(
stored : StoredResponse,
mode : CacheMode,
now : Timestamp,
policy : CachePolicy,
control : CacheControl,
diagnostics : Array[Diagnostic],
trace : Array[TraceStep],
) -> (FreshnessSource, Int64) {
if mode == SharedCache {
match control.delta("s-maxage") {
DeltaValid(value) => {
trace.push(
step(
"FRESHNESS_SHARED_MAX_AGE", "shared cache selected s-maxage", "RFC 9111 5.2.2.10",
),
)
return (SharedMaxAge, value)
}
DeltaInvalid | DeltaRepeated =>
diagnostics.push({
level: Warning,
code: "FRESHNESS_S_MAXAGE_INVALID",
message: "invalid or repeated s-maxage was ignored",
field_name: Some("cache-control"),
})
DeltaMissing => ()
}
}
match control.delta("max-age") {
DeltaValid(value) => {
trace.push(
step(
"FRESHNESS_MAX_AGE", "cache selected response max-age", "RFC 9111 5.2.2.1",
),
)
return (MaxAge, value)
}
DeltaInvalid | DeltaRepeated =>
diagnostics.push({
level: Warning,
code: "FRESHNESS_MAX_AGE_INVALID",
message: "invalid or repeated max-age was ignored",
field_name: Some("cache-control"),
})
DeltaMissing => ()
}
let expires = header_date(stored.response.headers, "expires", now)
if expires is Some(expiration) {
let date = header_date(stored.response.headers, "date", now).unwrap_or(
stored.response_time,
)
let lifetime = nonnegative_difference(expiration, date)
trace.push(
step(
"FRESHNESS_EXPIRES", "cache selected Expires relative to Date or response time",
"RFC 9111 4.2.1",
),
)
return (ExpiresDate, lifetime)
}
if stored.response.headers.contains("expires") {
diagnostics.push({
level: Warning,
code: "FRESHNESS_EXPIRES_INVALID",
message: "invalid or repeated Expires was treated as already expired",
field_name: Some("expires"),
})
return (ExpiresDate, 0L)
}
if policy.allow_heuristic_freshness {
let last_modified = header_date(
stored.response.headers,
"last-modified",
now,
)
if last_modified is Some(modified) {
let date = header_date(stored.response.headers, "date", now).unwrap_or(
stored.response_time,
)
let resource_age = nonnegative_difference(date, modified)
let fraction = clamp_int(policy.heuristic_fraction_percent, 0, 100)
let cap = clamp_int64(policy.heuristic_max_seconds, 0L, 2147483648L)
let candidate = resource_age / 100L * fraction.to_int64() +
resource_age % 100L * fraction.to_int64() / 100L
let lifetime = if candidate > cap { cap } else { candidate }
trace.push(
step(
"FRESHNESS_HEURISTIC_LAST_MODIFIED", "heuristic freshness derived from Last-Modified age",
"RFC 9111 4.2.2",
),
)
return (LastModifiedHeuristic, lifetime)
}
}
trace.push(
step(
"FRESHNESS_NONE", "response contains no usable explicit or heuristic freshness information",
"RFC 9111 4.2",
),
)
(NoFreshnessInformation, 0L)
}
///|
fn clamp_int(value : Int, low : Int, high : Int) -> Int {
if value < low {
low
} else if value > high {
high
} else {
value
}
}
///|
fn clamp_int64(value : Int64, low : Int64, high : Int64) -> Int64 {
if value < low {
low
} else if value > high {
high
} else {
value
}
}