A transport-independent, explainable HTTP cache policy and runtime toolkit.
Dependencies
Request
-> normalized cache key
-> Store lookup
-> Vary selection
-> freshness decision
-> Fetch or conditional revalidation
-> 304 merge or replacement
-> Store update
-> RuntimeResponse + CacheTracemoon fmt --check
moon check --deny-warn
moon test --deny-warnmoon check --target native --deny-warn
moon check --target js --deny-warn
moon check --target wasm-gc --deny-warnmoon add Ag108/MoonCache@0.1.3///|
import {
"Ag108/MoonCache" @mooncache,
}let store = @mooncache.MemoryStore::default()
let origin = @mooncache.FakeTransport::new([
@mooncache.TransportResponse::new(
@mooncache.ResponseMeta::complete(
200,
@mooncache.HeaderMap::from_pairs([
("Cache-Control", "max-age=60"),
]),
@mooncache.Timestamp::zero(),
@mooncache.Timestamp::zero(),
),
b"hello",
),
])
let runtime = @mooncache.CachedRuntime::new(
store,
origin,
@mooncache.CacheOptions::private_cache(),
)
let request = @mooncache.RuntimeRequest::get(
"https://example.test/greeting",
)
let first = try! runtime.execute(request, @mooncache.Timestamp::zero())
let second = try! runtime.execute(
request,
@mooncache.Timestamp::from_seconds(10L),
)
println(first.source.label()) // upstream
println(second.source.label()) // cachemoon run cmd/main -- explain examples/scenarios/stale-etag.json
moon run cmd/main -- explain examples/scenarios/stale-etag.json --json
moon run cmd/main -- validate examples/scenarios/basic-cache.json
moon run cmd/main -- replay testdata/scenarios
moon run cmd/main -- replay testdata/scenarios --jsonmoon run examples/basic_cache
moon run examples/vary_language
moon run examples/etag_revalidation
moon run examples/shared_private| Example | Demonstrates |
|---|---|
| basic_cache | first request misses, second request hits |
| vary_language | English and Chinese variants remain independent |
| etag_revalidation | stale ETag request receives 304 and retains its body |
| shared_private | private cache reuses a private response; shared cache rejects it |
///|
let cached_request = @mooncache_http11.request_to_runtime_at_uri(
wire_request, "https://example.test/data", request_time,
)///|
let origin = @mooncache_async.MoonbitAsyncHttpTransport::new(clock)
///|
let client = @mooncache_async.AsyncCachedRuntime::new(
@mooncache.MemoryStore::default(),
origin,
@mooncache.CacheOptions::private_cache(),
)
///|
let response = client.execute(request, now)pub(open) trait CacheStore {
fn find_variants(Self, PrimaryCacheKey) -> Array[StoredEntry]
fn put(Self, StoredEntry) -> StorePutResult
fn remove_variant(Self, PrimaryCacheKey, VariantKey) -> Bool
fn invalidate_uri(Self, String) -> Int
fn clear(Self) -> Unit
fn stats(Self) -> StoreStats
}pub(open) trait Transport {
fn execute(Self, RuntimeRequest) -> TransportResponse raise TransportError
}pub(all) struct AgeCalculation {
apparent_age : DeltaSeconds
response_delay : DeltaSeconds
age_value : DeltaSeconds
corrected_age_value : DeltaSeconds
corrected_initial_age : DeltaSeconds
resident_time : DeltaSeconds
current_age : DeltaSeconds
clock_clamped : Bool
overflow_clamped : Bool
} derive(Eq, Debug)pub(all) struct CacheControl {
no_cache : Bool
no_store : Bool
is_private : Bool
is_public : Bool
must_revalidate : Bool
proxy_revalidate : Bool
immutable : Bool
only_if_cached : Bool
max_age : DeltaSeconds?
shared_max_age : DeltaSeconds?
min_fresh : DeltaSeconds?
max_stale : DeltaSeconds?
max_stale_unbounded : Bool
stale_if_error : DeltaSeconds?
stale_while_revalidate : DeltaSeconds?
malformed_delta : Bool
} derive(Eq, Debug)pub(all) struct CacheDecision {
action : CacheActionKind
trace : CacheTrace
storage : StorageDecision
} derive(Eq, Debug)pub(all) struct CacheOptions {
mode : CacheMode
allow_heuristic_freshness : Bool
heuristic_fraction_per_mille : Int
max_heuristic_lifetime : DeltaSeconds
allow_shared_authorized : Bool
cache_head_responses : Bool
strip_sensitive_metadata : Bool
} derive(Eq, Debug)fn CacheOptions::with_heuristic_settings(self : CacheOptions, fraction_per_mille : Int, maximum : DeltaSeconds) -> CacheOptionspub(all) struct CacheReason {
code : CacheReasonCode
detail : String?
rfc : String?
} derive(Eq, Debug)pub(all) enum CacheReasonCode {
StoreAllowed
StoreMethodUnsupported
StoreStatusDefault
StoreStatusExplicit
StoreStatusUnknown
StoreNoStore
StorePrivateShared
StoreAuthorizationShared
StoreIncomplete
StoreVaryStar
StoreOversized
StoreCapacityZero
FreshMaxAge
FreshSharedMaxAge
FreshExpires
FreshHeuristic
FreshNone
FreshStillFresh
FreshStale
AgeApparent
AgeCorrected
AgeClockClamped
AgeOverflowClamped
RequestNoStore
RequestNoCache
RequestOnlyIfCached
RequestMaxAge
RequestMinFresh
RequestMaxStale
VaryDefault
VaryMatched
VaryMismatch
VaryMissing
VaryStar
RevalidateEtag
RevalidateLastModified
RevalidateBoth
RevalidateCallerCondition
RevalidateNotModified
RevalidateModified
RuntimeHit
RuntimeMiss
RuntimeBypass
RuntimeFetch
RuntimeStored
RuntimeNotStored
RuntimeOnlyIfCachedMiss
StoreReplaced
StoreEvictedEntries
StoreEvictedBytes
StoreInvalidated
InvalidateUnsafeMethod
InvalidateRelatedUri
InvalidateSkippedSafeMethod
InvalidateSkippedErrorStatus
} derive(Compare, Eq, Debug)pub(all) struct CacheScenario {
name : String
mode : String
now : Int
request : ScenarioRequest
stored_response : ScenarioResponse?
upstream_response : ScenarioResponse?
expected : ScenarioExpected?
} derive(Eq, ToJson, Debug, FromJson)pub(all) struct CacheTrace {
action : CacheActionKind
reasons : Array[CacheReason]
age : AgeCalculation?
freshness_lifetime : DeltaSeconds?
stale_by : DeltaSeconds?
primary_key : String?
selected_variant : String?
validator : String?
generated_headers : HeaderMap
} derive(Eq, Debug)pub struct CachedRuntime[S, T] {
// private fields
}fn[S : CacheStore, T : Transport] CachedRuntime::execute(self : CachedRuntime[S, T], request : RuntimeRequest, now : Timestamp) -> RuntimeResponse raise RuntimeErrorfn[S, T] CachedRuntime::new(store : S, transport : T, options : CacheOptions) -> CachedRuntime[S, T]fn DeltaSeconds::saturating_scale(self : DeltaSeconds, numerator : Int64, denominator : Int64) -> DeltaSecondspub struct FakeTransport {
// private fields
}impl Transport for FakeTransportfn execute(self : FakeTransport, request : RuntimeRequest) -> TransportResponse raise TransportErrorfn FakeTransport::execute(self : FakeTransport, _request : RuntimeRequest) -> TransportResponse raise TransportErrorpub(all) struct FreshnessCalculation {
lifetime : DeltaSeconds
source : FreshnessSource
date_value : Timestamp
expires_value : Timestamp?
last_modified_value : Timestamp?
} derive(Eq, Debug)pub(all) struct InvalidationPlan {
uris : Array[String]
reasons : Array[CacheReason]
} derive(Eq, Debug)pub struct MemoryStore {
// private fields
}impl CacheStore for MemoryStorefn MemoryStore::contains_variant(self : MemoryStore, key : PrimaryCacheKey, variant : VariantKey) -> Boolfn MemoryStore::remove_variant(self : MemoryStore, key : PrimaryCacheKey, variant : VariantKey) -> Boolfn MemoryStoreOptions::with_entry_limit(self : MemoryStoreOptions, max_entries : Int) -> MemoryStoreOptionsfn MemoryStoreOptions::with_limits(max_entries : Int, max_body_bytes : Int64, max_body_size : Int64) -> MemoryStoreOptionsfn MemoryStoreOptions::with_request_credential_stripping(self : MemoryStoreOptions, strip_request_credentials : Bool) -> MemoryStoreOptionsfn MemoryStoreOptions::with_set_cookie_stripping(self : MemoryStoreOptions, strip_set_cookie : Bool) -> MemoryStoreOptionsfn MemoryStoreOptions::with_single_body_limit(self : MemoryStoreOptions, max_body_size : Int64) -> MemoryStoreOptionsfn MemoryStoreOptions::with_total_body_limit(self : MemoryStoreOptions, max_body_bytes : Int64) -> MemoryStoreOptionspub(all) struct NotModifiedResult {
entry : StoredEntry
storage : StorageDecision
reasons : Array[CacheReason]
} derive(Eq, Debug)pub struct RecordingTransport {
// private fields
}impl Transport for RecordingTransportfn execute(self : RecordingTransport, request : RuntimeRequest) -> TransportResponse raise TransportErrorfn RecordingTransport::execute(self : RecordingTransport, request : RuntimeRequest) -> TransportResponse raise TransportErrorfn RequestMeta::new(http_method : String, uri : String, headers : HeaderMap, request_time : Timestamp) -> RequestMetapub(all) struct RequestPolicy {
bypass : Bool
force_revalidation : Bool
only_if_cached : Bool
max_age : DeltaSeconds?
min_fresh : DeltaSeconds
max_stale : DeltaSeconds?
max_stale_unbounded : Bool
} derive(Eq, Debug)fn ResponseMeta::complete(status : Int, headers : HeaderMap, request_time : Timestamp, response_time : Timestamp) -> ResponseMetafn ResponseMeta::new(status : Int, headers : HeaderMap, request_time : Timestamp, response_time : Timestamp, body_complete : Bool) -> ResponseMetapub(all) struct RevalidationPlan {
request : RequestMeta
generated_headers : HeaderMap
validator_kind : ValidatorKind?
reasons : Array[CacheReason]
} derive(Eq, Debug)pub(all) struct RuntimeResponse {
meta : ResponseMeta
body : Bytes
source : ResponseSource
trace : CacheTrace
} derive(Eq, Debug)pub(all) struct ScenarioAnalysis {
name : String
trace : CacheTrace
valid : Bool
errors : Array[String]
} derive(Eq, Debug)fn ScenarioResponse::to_meta(self : ScenarioResponse, fallback_request_time : Timestamp) -> ResponseMetapub(all) struct StorageDecision {
storable : Bool
policy : StoredPolicy
reasons : Array[CacheReason]
} derive(Eq, Debug)pub(all) struct StoredEntry {
primary_key : PrimaryCacheKey
variant_key : VariantKey
request : RequestMeta
response : ResponseMeta
body : Bytes
policy : StoredPolicy
stored_at : Timestamp
last_accessed_at : Timestamp
} derive(Eq, Debug)fn StoredEntry::new(primary_key : PrimaryCacheKey, variant_key : VariantKey, request : RequestMeta, response : ResponseMeta, body : Bytes, policy : StoredPolicy, stored_at : Timestamp) -> StoredEntrypub(all) struct StoredPolicy {
cacheable : Bool
freshness_lifetime : DeltaSeconds
requires_revalidation : Bool
no_store : Bool
vary_star : Bool
must_revalidate : Bool
proxy_revalidate : Bool
is_private : Bool
is_public : Bool
} derive(Eq, Debug)fn StoredPolicy::from_directives(lifetime : DeltaSeconds, directives : CacheControl, vary_star : Bool) -> StoredPolicypub(all) enum TransportOutcome {
TransportSuccess(TransportResponse)
TransportFailure(String)
} derive(Eq, Debug)pub(all) struct VariantSelection {
entry : StoredEntry?
inspected : Int
variant_key : VariantKey?
reasons : Array[CacheReason]
} derive(Eq, Debug)pub(all) struct VaryMatch {
kind : VaryMatchKind
differing_field : String?
stored_key : VariantKey?
candidate_key : VariantKey?
reason : CacheReason
} derive(Eq, Debug)let MAX_DELTA_SECONDS : Int64fn calculate_freshness_lifetime(response : ResponseMeta, options : CacheOptions) -> FreshnessCalculationfn create_revalidation_plan(request : RequestMeta, stored_response : ResponseMeta) -> RevalidationPlanfn evaluate_cached_response(request : RequestMeta, response : ResponseMeta, options : CacheOptions, now : Timestamp) -> CacheDecisionfn evaluate_storage(request : RequestMeta, response : ResponseMeta, options : CacheOptions) -> StorageDecisionfn[S : CacheStore, T : Transport] execute_cached(store : S, transport : T, options : CacheOptions, request : RuntimeRequest, now : Timestamp) -> RuntimeResponse raise RuntimeErrorfn is_default_cacheable_status(status : Int) -> Boolfn is_valid_header_name(name : String) -> Boolfn match_vary(stored_request : RequestMeta, candidate_request : RequestMeta, stored_response : ResponseMeta) -> VaryMatchfn merge_not_modified(entry : StoredEntry, not_modified : ResponseMeta, options : CacheOptions, now : Timestamp) -> NotModifiedResult raise ValidationErrorfn normalize_cache_uri(uri : String) -> String?fn normalize_header_name(name : String) -> Stringfn normalize_header_value(value : String) -> Stringfn resolve_related_uri(base : String, reference : String) -> String?A transport-independent, explainable HTTP cache policy and runtime toolkit.
Dependencies