moonkart

    A cache for MoonBit: bounded by count or weight, with expiry, loaders and eviction listeners.

    cache
    caching
    lru
    tinylfu
    w-tinylfu
    s3fifo
    arc
    s3-fifo
    trace
    benchmark
    hit-rate
    eviction-policy
    ttl
    tti
    expiry
    eviction
    loader
    concurrent
    disk-cache
    caffeine
    moka
    moonbit
    Download zip
    Version
    0.1.2
    License
    Apache-2.0
    Last updated
    10 hours ago
    Downloads
    25

    #moonkart

    A cache for MoonBit. It is bounded by entry count or by total weight, it can expire entries on a schedule you control, it computes missing values on demand, and it tells you what left the cache and why. The eviction strategy is erased behind an open contract, so every constructor returns the same Cache[K, V] type and a third-party policy plugs in without changing it.

    let cache = @moonkart.tinylfu_cache(cap=1000)
    cache.set("answer", 42)

    assert_eq(cache.get("answer"), Some(42))
    assert_eq(cache.get("unknown"), None)

    #Install

    moon add moonbitstack/moonkart

    Then import it in a package:

    // moon.pkg
    import {
    "moonbitstack/moonkart",
    }

    #Choosing an eviction policy

    Every constructor returns a Cache[K, V]. The policy decides which entry leaves when the cache is full; it is hidden behind the Policy contract, so the cache type does not change when you switch strategies.

    ConstructorPolicyGood at
    tinylfu_cacheW-TinyLFU (frequency + recency)general workloads; keeps frequently used entries
    lru_cacheleast recently usedsimple, predictable recency
    s3fifo_cachesecond-chance FIFOscan floods and one-hit wonders

    tinylfu_cache, lru_cache and s3fifo_cache share the same knobs; use new(policy, ...) with your own policy if none of these fit.

    #Measured hit rates

    The bench package replays a trace of keys through each policy and reports the hit rate, so a change to a policy can be judged against real access patterns rather than intuition. The figures below come from the three ARC traces (the request streams published with the ARC paper), replaying every request:

    TraceCapacitylru_caches3fifo_cachetinylfu_cache
    OLTP (914,145 requests)1,8690.41640.42190.4269
    DS1 (2,505,947 requests)101,7640.10870.11090.2411
    S3 (16.4 million requests)524,2880.2526n/a0.4640

    Two checks make these comparable with published results. First, lru_cache on S3 at 524,288 entries returns 0.25258, reproducing the 25.26% that the ARC paper reports for the same trace at the same capacity, which validates both the replay harness and the LRU implementation. Second, the ordering matches the published conclusions: frequency-aware admission wins by a wide margin on the scan-heavy S3 trace, holds a similar lead on DS1, and only edges ahead on OLTP, where recency dominates and frequency has little to add.

    The frequency sketch is keyed by the runtime's hash seed, so a different seed reshuffles its collisions and moves a TinyLFU figure by a few tenths of a point; the recency policies are unaffected.

    #Capacity

    Bound the cache by entry count with cap, or by total weight with max_weight plus a weigher:

    // At most 500 entries.
    let by_count = @moonkart.lru_cache(cap=500)
    by_count.set("a", 1)
    assert_eq(by_count.size(), 1L)

    // At most 12 units of weight, where a value weighs one per character.
    let by_weight = @moonkart.lru_cache(
    max_weight=12L,
    weigher=fn(_key : String, value : String) { value.length().to_int64() },
    )
    by_weight.set("foo", "123456") // 6
    by_weight.set("emm", "123456") // 6 + 6 = 12
    assert_eq(by_weight.size(), 2L)

    by_weight.set("bar", "123456") // 18 > 12, so the oldest entry leaves
    assert_eq(by_weight.contains("foo"), false)
    assert_eq(by_weight.size(), 2L)

    Without a weigher every entry weighs one, so cap and max_weight mean the same thing.

    #Expiry

    Expiry deadlines are absolute nanosecond timestamps produced by a clock you supply. The cache never reads the wall clock itself, so tests are deterministic and do not sleep.

    // Time to live from the moment of writing; reads do not extend it.
    let now : @ref.Ref[Int64] = @ref.Ref(0L)
    let ttl_cache = @moonkart.lru_cache(
    cap=100,
    expiry=@moonkart.ttl(10L),
    clock=fn() { now.val },
    )
    ttl_cache.set("user:7", "Vega")
    assert_eq(ttl_cache.contains("user:7"), true)

    now.val = 11L
    assert_eq(ttl_cache.contains("user:7"), false)

    // Time to idle: every read restarts the countdown.
    let idle_now : @ref.Ref[Int64] = @ref.Ref(0L)
    let tti_cache = @moonkart.lru_cache(
    cap=100,
    expiry=@moonkart.tti(10L),
    clock=fn() { idle_now.val },
    )
    tti_cache.set("session", "open")
    idle_now.val = 6L
    assert_eq(tti_cache.get("session"), Some("open")) // read at 6, so it lives to 16

    idle_now.val = 15L
    assert_eq(tti_cache.contains("session"), true)
    idle_now.val = 17L
    assert_eq(tti_cache.contains("session"), false)

    ttl and tti are shorthands over the Expiry record, whose after_create, after_read and after_update callbacks compute a deadline per entry, so a rule can depend on the value or apply to only some keys.

    A single write can also carry its own deadline, which then outranks the rule:

    let fixed_now : @ref.Ref[Int64] = @ref.Ref(0L)
    let mixed = @moonkart.lru_cache(
    cap=100,
    expiry=@moonkart.ttl(1_000L),
    clock=fn() { fixed_now.val },
    )
    mixed.set("short", "lived", expire_after=10L) // ten nanoseconds from now
    mixed.set("long", "lived") // the cache's rule
    assert_eq(mixed.entry("short").expire_at, Some(10L))
    assert_eq(mixed.entry("long").expire_at, Some(1_000L))

    fixed_now.val = 11L
    assert_eq(mixed.contains("short"), false)
    assert_eq(mixed.contains("long"), true)

    A deadline fixed that way is not moved by a read, even on a time-to-idle cache, and writing the entry again without expire_after hands it back to the rule. get_with and try_get_with take the same parameter for the value they load.

    #Loading

    get_with runs a loader only on a miss and stores its result. The loader may raise; a raised error is not cached and reaches the caller.

    let users = @moonkart.lru_cache(cap=64)
    let loads : @ref.Ref[Int] = @ref.Ref(0)
    let fetch_user = fn(id : Int) raise {
    loads.val = loads.val + 1
    if id < 0 {
    fail("no such user")
    }
    "user-\{id}"
    }
    assert_eq(users.get_with(7, fetch_user), "user-7")
    assert_eq(users.get_with(7, fetch_user), "user-7")
    assert_eq(loads.val, 1) // the second call was a plain hit

    let failed = users.get_with(-1, fetch_user) catch { _ => "none" }
    assert_eq(failed, "none")
    assert_eq(users.contains(-1), false) // a failed load leaves nothing behind

    try_get_with lets the loader return None for "no value", which is not cached either. When the loader cannot fail, wrap it with infallible_loader so the call site needs no error handling:

    let parsed = users.try_get_with(
    12,
    @moonkart.infallible_loader(fn(id : Int) {
    if id % 2 == 0 {
    Some("user-\{id}")
    } else {
    None
    }
    }),
    )
    assert_eq(parsed, Some("user-12"))
    assert_eq(users.size(), 2L)

    #Remembering a function

    #kart.cached is functools.lru_cache for MoonBit. The function stays as it is, and the generator adds a remembering one beside it:

    #kart.cached(capacity="1024", policy="lru", ttl="60s")
    #kart.count_calls
    pub fn lookup(id : Int) -> String {
    "item-\{id}"
    }

    lookup_cached(id) reads through the cache, lookup_cached_clear() empties it, and lookup_cached_calls() reports how many times lookup itself ran — the number of misses, not the number of questions. The parameters are the key, several of them a tuple. capacity is 128 when nothing says, policy is tinylfu, lru or s3fifo, ttl and tti are durations such as 250ms, 60s, 5m or 1h, and name renames the function. An async fn goes through the asynchronous cache. A function that raises is refused, since there is nothing to keep when it does.

    For a value that is worked out once and never again, #kart.once turns a function with no arguments into a one-entry store with no way back:

    #kart.once
    pub fn banner() -> String {
    "kart no. 128"
    }

    banner_once() answers with the first result for the rest of the process. There is no clear function, by design: a value that can be forgotten is a cache, and #kart.cached is the way to ask for one.

    The generator wires in as a dev_build rule, in moon.mod:

    rule(name: "cached", command: "moonx moonbitstack/moonkart/cmd/cached -C \"$mod_dir\" \"$input\" -o \"$output\"")

    and in the moon.pkg of the package, which imports moonbitstack/moonkart and moonbitstack/moonkart/policy:

    dev_build(rule: "cached", input: "slow.mbt", output: "slow.g.mbt") formatter(ignore: [ "slow.g.mbt" ])

    src/examples/03-cached is a working copy of both files.

    #The asynchronous cache

    @moonkart.concurrent.AsyncCache wraps any cache so that loads can be awaited. Concurrent readers of one key share a single loader; readers of different keys never wait on each other, and no lock is held while a loader runs.

    // moon.pkg
    import {
    "moonbitstack/moonkart",
    "moonbitstack/moonkart/concurrent",
    }

    let async_cache = @concurrent.AsyncCache::new(@moonkart.lru_cache(cap=64))
    let value = async_cache.get_with(1, id => "user-\{id}")
    assert_eq(value, "user-1")
    assert_eq(async_cache.get(1), Some("user-1"))

    The first task to miss records a per-key in-flight load and awaits it; later tasks asking for the same key await the same result. A loader that raises delivers its error to every waiter and leaves nothing in the cache, so the next call retries. Three concurrent readers of one key call the loader exactly once. concurrent builds on the synchronous core unchanged, so every policy, expiry rule and counter carries over.

    #Shards

    ShardedCache splits the key space over independent caches. A key always lands in the same shard, so a read or a write behaves as it does on one cache; what changes is that each shard holds its own budget, so traffic in one cannot evict entries in another, and each has its own in-flight table.

    let sharded = @concurrent.ShardedCache::new(
    shards=4,
    make=fn(_index) { @moonkart.lru_cache(cap=256L) },
    )
    sharded.set("user:1", "Leo")
    assert_eq(sharded.get("user:1"), Some("Leo"))
    assert_eq(sharded.shards(), 4)
    assert_eq(sharded.stats().hit_count + sharded.stats().miss_count, 1L)

    make is called once per shard with its index, so each one can be bounded and tuned on its own. size() and stats() report the shards added together. Sharding is not by itself a speed-up on a runtime that schedules tasks cooperatively; it is the unit that keeps unrelated workloads apart, and the unit a caller can hand to its own threads.

    #Hybrid memory and disk (native)

    @moonkart.disk.DiskCache is a two-tier cache: a memory cache in front, and a file behind it that holds what memory could not keep. Every write appends a record, so the file doubles as the log a restart replays; every read asks memory first and falls to the file, whose answer is put back into memory.

    // moon.pkg
    import {
    "moonbitstack/moonkart",
    "moonbitstack/moonkart/disk",
    }

    async fn persist(path : String) -> Unit raise {
    let disk = @disk.DiskCache::open(
    @moonkart.lru_cache(cap=10_000),
    path,
    fn(key : String) { @utf8.encode(key) },
    fn(bytes) { Some(@utf8.decode_lossy(bytes)) },
    fn(value : String) { @utf8.encode(value) },
    fn(bytes) { Some(@utf8.decode_lossy(bytes)) },
    max_bytes=64L * 1024L * 1024L, // at most 64 MiB of live records
    )
    disk.set("greeting", "hello") // memory, then an appended record
    assert_eq(disk.get("greeting"), Some("hello"))
    assert_eq(disk.stats().entries, 1L)
    disk.compact() // rewrite the file from the live records
    }

    The file is bounded on its own terms. max_bytes caps the live records it keeps; when a write would take it over, the least recently read record leaves first — a read that came from the file counts as recent. A record larger than the whole budget is refused rather than written: it stays in memory, refused goes up, and it is gone when memory evicts it. Without max_bytes the file keeps everything written.

    stats() reports promotions, evictions, refused, and the file's live entries and bytes. Call compact periodically: until it runs, superseded and deleted records still take space.

    The filesystem in the standard library is asynchronous and native-only, so this package is too. It is target-gated: the other backends compile unaffected, and moon check --target all stays green. Records restored on open are replayed through set, so the memory cache's current capacity and expiry policy apply to them.

    #Invalidation

    let c = @moonkart.lru_cache(cap=8)
    c.set("a", 1)
    c.set("b", 2)
    assert_eq(c.invalidate("a"), true) // one key, returns whether it was present
    assert_eq(c.invalidate_if(fn(_k, value) { value > 1 }), 1L) // by predicate, returns how many
    c.invalidate_all() // everything
    assert_eq(c.size(), 0L)

    #Events and statistics

    A removal listener is told about every entry that leaves, with the reason: Capacity, Expired, Explicit or Replaced.

    let gone : Array[String] = []
    let tracked = @moonkart.lru_cache(
    cap=2,
    removal_listener=fn(key : Int, _value : String, reason) {
    let why = match reason {
    Capacity => "capacity"
    Expired => "expired"
    Explicit => "explicit"
    Replaced => "replaced"
    }
    gone.push("\{key}:\{why}")
    },
    )
    tracked.set(1, "a")
    tracked.set(2, "b")
    tracked.set(3, "c")
    assert_eq(gone, ["1:capacity"])

    stats() returns counters for hits, misses, evictions, evicted weight, successful and failed loads, and the total time spent in loaders. hit_rate() and miss_rate() are derived from the first two.

    let counted = @moonkart.lru_cache(cap=4)
    counted.set(1, 1)
    ignore(counted.get(1))
    ignore(counted.get(2))
    let s = counted.stats()
    assert_eq(s.hit_count, 1L)
    assert_eq(s.miss_count, 1L)
    assert_eq(s.hit_rate(), 0.5)

    #Custom policies

    @moonkart.policy.Policy is an open trait. A policy sees opaque numeric entry ids, weights and the key's hash, never the keys or values. Implement hit, admit, forget and size for your own type and build a cache with new:

    pub struct Fifo {
    order : Array[Int64]
    cap : Int64
    }

    impl @policy.Policy for Fifo with fn hit(
    self : Fifo,
    _id : Int64,
    _hash : Int32,
    ) -> Unit {
    ignore(self)
    }

    impl @policy.Policy for Fifo with fn admit(
    self : Fifo,
    id : Int64,
    _weight : Int64,
    _hash : Int32,
    ) -> Array[Int64] {
    self.order.push(id)
    if self.order.length().to_int64() > self.cap {
    [self.order.remove(0)]
    } else {
    []
    }
    }

    impl @policy.Policy for Fifo with fn forget(self : Fifo, id : Int64) -> Unit {
    for i, v in self.order {
    if v == id {
    ignore(self.order.remove(i))
    break
    }
    }
    }

    impl @policy.Policy for Fifo with fn size(self : Fifo) -> Int32 {
    self.order.length()
    }

    let fifo_policy : Fifo = { order: [], cap: 2L }
    let fifo = @moonkart.new(fifo_policy)
    fifo.set(1, "a")
    fifo.set(2, "b")
    fifo.set(3, "c")
    assert_eq(fifo.contains(1), false)
    assert_eq(fifo.size(), 2L)

    admit returns the ids to evict, and may return the new id itself to refuse an entry that is too heavy for the cache. new_with_config takes the same policy together with a Config bundle; default_config() fills in every optional knob.

    #API

    ItemMeaning
    lru_cache, s3fifo_cache, tinylfu_cacheconstructors for the shipped policies
    new(policy, ...)construct with any policy
    new_with_config(policy, config)construct from a Config bundle
    default_config()defaults for every optional knob
    ttl(ns), tti(ns)expiry shorthands
    system_clockthe default clock, from the runtime
    set(key, value)insert or replace
    set(key, value, expire_after=ns)insert or replace, with a deadline for this write only
    get(key)read, or None
    contains(key)presence; no hit, no idle refresh
    entry(key), entry_or_null(key)snapshot of an entry
    remove(key), invalidate(key)remove one, returns whether it was present
    invalidate_all(), invalidate_if(pred)remove many
    get_with(key, loader), try_get_with(key, loader)load on miss; the loader may raise
    infallible_loader(loader)adapt a loader that cannot fail
    to_array()every live entry as (key, value)
    stats(), size(), policy_size()counters and sizes
    CacheStats::merge(a, b)two sets of counters added together

    The same library adds two more packages: @moonkart.concurrent for AsyncCache and ShardedCache, and @moonkart.disk for the native memory-plus-file tier. #kart.cached, #kart.once and #kart.count_calls come from the generator in cmd/cached, run as a dev_build rule.

    The clock, the weigher, the expiry rule and the removal listener are all parameters of the constructor, so one Cache type serves a request path, a long-running job and a test that moves a Ref. Because every deadline is absolute and the clock is passed in, an expiry test never sleeps.

    #Licence

    Apache 2.0.

    Clock

    type Clock = () -> Int64

    A clock returns the current time in nanoseconds. The cache never reads the wall clock itself; every timestamp it compares comes from a clock the caller supplies, so expiry and load timing stay deterministic under test.

    RemovalListener

    type RemovalListener[K, V] = (K, V, EvictionReason) -> Unit

    Receives every entry the cache removes, together with the reason it left.

    Weigher

    type Weigher[K, V] = (K, V) -> Int64

    Weighs an entry so the cache can bound total weight instead of entry count. Return a positive number; the cache treats the result as the entry's weight and evicts by weight when the policy's maximum weight is set.

    Cache

    pub struct Cache[K, V] {
    // private fields
    }

    A bounded cache generic over key K and value V. The eviction strategy is erased behind the open Policy contract, so one Cache type serves every strategy; [new] accepts a third-party policy without changing this type.

    The default strategy, chosen by [tinylfu_cache], is W-TinyLFU; [lru_cache] and [s3fifo_cache] provide the other shipped strategies.

    Cache::contains

    fn[K : Eq + Hash, V] Cache::contains(self : Cache[K, V], key : K) -> Bool

    Whether key is present and not expired. Expired entries are purged and report as absent. This is a presence check: it neither refreshes a time-to-idle deadline nor counts a hit.

    Cache::entry

    fn[K : Eq + Hash, V] Cache::entry(self : Cache[K, V], key : K) -> Entry[K, V]

    Returns a snapshot of the entry for key. Panics if the key is absent or expired; prefer entry_or_null when absence is expected.

    Cache::entry_or_null

    fn[K : Eq + Hash, V] Cache::entry_or_null(self : Cache[K, V], key : K) -> Entry[K, V]?

    Returns a snapshot of the entry for key, or None. The snapshot is a copy detached from the cache.

    Cache::get

    fn[K : Eq + Hash, V] Cache::get(self : Cache[K, V], key : K) -> V?

    Returns the value for key, or None if absent or expired. A successful read counts a hit and bumps the entry's recency; an expired or missing read counts a miss.

    Cache::get_with

    fn[K : Eq + Hash, V, E : Error] Cache::get_with(self : Cache[K, V], key : K, loader : (K) -> V raise E, expire_after? : Int64) -> V raise E

    Returns the value for key. On a miss, runs loader(key) and stores the result. The loader may raise; a raised error is not cached, is counted as a load failure, and is handed to the caller unchanged. expire_after does to the loaded value what it does to [set].

    Cache::invalidate

    fn[K : Eq + Hash, V] Cache::invalidate(self : Cache[K, V], key : K) -> Bool

    Removes key explicitly (alias of remove). Returns whether it was present.

    Cache::invalidate_all

    fn[K : Eq + Hash, V] Cache::invalidate_all(self : Cache[K, V]) -> Unit

    Removes every entry, reporting each to the removal listener as Explicit.

    Cache::invalidate_if

    fn[K : Eq + Hash, V] Cache::invalidate_if(self : Cache[K, V], pred : (K, V) -> Bool) -> Int64

    Removes every entry for which pred(key, value) is true. Returns the number removed.

    Cache::policy_size

    fn[K, V] Cache::policy_size(self : Cache[K, V]) -> Int

    Number of entries the eviction policy currently tracks (equals size).

    Cache::remove

    fn[K : Eq + Hash, V] Cache::remove(self : Cache[K, V], key : K) -> Bool

    Removes key explicitly. Returns whether it was present.

    Cache::set

    fn[K : Eq + Hash, V] Cache::set(self : Cache[K, V], key : K, value : V, expire_after? : Int64) -> Unit

    Inserts or replaces key. A replaced value is reported to the removal listener as Replaced; an insertion calls after_update when the key was already present, otherwise after_create.

    expire_after fixes this entry's deadline at expire_after nanoseconds from now, for this write only: the cache's own rule does not apply to it, and a later read will not move the deadline it computed. Write the entry again without it to hand the entry back to the cache's rule.

    Cache::size

    fn[K, V] Cache::size(self : Cache[K, V]) -> Int64

    Number of entries currently stored.

    Cache::stats

    fn[K, V] Cache::stats(self : Cache[K, V]) -> CacheStats

    A copy of the running counters.

    Cache::to_array

    fn[K, V] Cache::to_array(self : Cache[K, V]) -> Array[(K, V)]

    A snapshot of every live entry as (key, value) pairs, in no particular order. Useful for persistence and for tests.

    Cache::try_get_with

    fn[K : Eq + Hash, V, E : Error] Cache::try_get_with(self : Cache[K, V], key : K, loader : (K) -> V? raise E, expire_after? : Int64) -> V? raise E

    Like get_with, but loader may return None to signal "no value"; such a result is not cached and None is returned. A raised error propagates to the caller and is counted as a load failure.

    CacheStats

    pub(all) struct CacheStats {
    hit_count : Int64
    miss_count : Int64
    eviction_count : Int64
    eviction_weight : Int64
    load_success_count : Int64
    load_failure_count : Int64
    total_load_time : Int64
    }

    Counters the cache keeps while it runs. The raw counts are exposed so callers can aggregate them; hit_rate and miss_rate are derived.

    CacheStats::hit_rate

    fn CacheStats::hit_rate(self : CacheStats) -> Double

    Fraction of reads that hit, in 0.0 ..= 1.0. Zero reads reports 0.0 rather than dividing by zero.

    CacheStats::merge

    fn CacheStats::merge(self : CacheStats, other : CacheStats) -> CacheStats

    Two sets of counters added together, for a cache split into shards or a report built from several caches.

    CacheStats::miss_rate

    fn CacheStats::miss_rate(self : CacheStats) -> Double

    Fraction of reads that missed, in 0.0 ..= 1.0.

    Config

    pub(all) struct Config[K, V] {
    weigher : (K, V) -> Int64?
    clock : () -> Int64
    expiry : Expiry[K, V]?
    removal_listener : (K, V, EvictionReason) -> Unit?
    }

    The cross-cutting knobs of a cache, bundled so they can be built once and reused across caches. Capacity lives on the eviction policy, not here.

    [default_config] fills in every knob; write the record out when one of them differs, and hand the result to new_with_config.

    Entry

    pub(all) struct Entry[K, V] {
    key : K
    value : V
    weight : Int64
    expire_at : Int64?
    }

    An entry as seen by a reader: its value, weight and absolute expiry deadline (if any).

    EvictionReason

    pub(all) enum EvictionReason {
    Capacity
    Expired
    Explicit
    Replaced
    }

    Why an entry left the cache.

    Expiry

    pub(all) struct Expiry[K, V] {
    after_create : (K, V, Int64) -> Int64?
    after_read : (K, V, Int64, Int64?) -> Int64?
    after_update : (K, V, Int64, Int64?) -> Int64?
    }

    Per-entry lifetime policy. Each callback returns an absolute nanosecond deadline, or None to leave the existing deadline untouched (when the callback does not apply) or to let the entry live until capacity eviction (on create). after_read and after_update receive the current deadline so they can extend or keep it.

    A pure time-to-live from creation uses ttl; a time-to-idle (idle) lifetime uses tti; anything else is expressed with the three callbacks directly.

    default_config

    fn[K, V] default_config() -> Config[K, V]

    A config with every knob at its default: no weigher (each entry weighs one), the system clock, no expiry, no listener.

    infallible_loader

    fn[K, V] infallible_loader(loader : (K) -> V) -> ((K) -> V raise)

    Adapts an infallible loader to the raising loader type [get_with] and [try_get_with] accept. Use it when producing a value cannot fail, so the call site needs no error handling while still sharing one read path.

    lru_cache

    fn[K : Eq + Hash, V] lru_cache(cap? : Int64, max_weight? : Int64, weigher? : (K, V) -> Int64, clock? : () -> Int64, expiry? : Expiry[K, V], removal_listener? : (K, V, EvictionReason) -> Unit) -> Cache[K, V]

    A cache that evicts least-recently-used entries.

    new

    fn[K : Eq + Hash, V] new(policy : &
    Policy
    , weigher? : (K, V) -> Int64, clock? : () -> Int64, expiry? : Expiry[K, V], removal_listener? : (K, V, EvictionReason) -> Unit) -> Cache[K, V]

    Builds a cache from a policy and the individual knobs. Capacity belongs to the policy: pass cap or max_weight to the policy preset, not here.

    new_with_config

    fn[K : Eq + Hash, V] new_with_config(policy : &
    Policy
    , config : Config[K, V]) -> Cache[K, V]

    Builds a cache from a policy and a [Config] bundle.

    s3fifo_cache

    fn[K : Eq + Hash, V] s3fifo_cache(cap? : Int64, max_weight? : Int64, weigher? : (K, V) -> Int64, clock? : () -> Int64, expiry? : Expiry[K, V], removal_listener? : (K, V, EvictionReason) -> Unit) -> Cache[K, V]

    A cache using the S3-FIFO-inspired second-chance FIFO: entries that were read since they entered get another round before eviction, which resists scan and one-hit-wonder patterns.

    system_clock

    fn system_clock() -> Int64

    The default clock, built from the runtime clock in milliseconds. Callers may pass their own clock to make expiry deterministic.

    tinylfu_cache

    fn[K : Eq + Hash, V] tinylfu_cache(cap? : Int64, max_weight? : Int64, window_ratio? : Double, protected_ratio? : Double, weigher? : (K, V) -> Int64, clock? : () -> Int64, expiry? : Expiry[K, V], removal_listener? : (K, V, EvictionReason) -> Unit) -> Cache[K, V]

    A cache using W-TinyLFU (the default): a small admission window feeds a frequency-guarded segmented LRU, so frequently used entries survive even after they age out of recency.

    tti

    fn[K, V] tti(duration : Int64) -> Expiry[K, V]

    Time-to-idle: the entry expires duration nanoseconds after its most recent read or write.

    ttl

    fn[K, V] ttl(duration : Int64) -> Expiry[K, V]

    Time-to-live measured from creation: the entry expires duration nanoseconds after it is written and is not refreshed by reads.