March Docs

Vault

Vault module: ETS-like in-memory key-value store.

Vault tables are mutable, process-global hash tables. They are created once (typically at app startup) and shared across all actors without message passing — every actor that holds the same VaultTable handle sees the same data.

Keys can be any plain value: Int, String, Bool, Atom, Tuple, or Ctor. Function values, Pids, and Tasks cannot be used as keys. Keys are STRINGIFIED on the way in, which is why the handle is not parameterised by a key type: set(t, 1, v) and set(t, "1", v) address the same entry, and keys(t) gives back List(String), never the original key values.

A table handle is Vault(v), phantom in the type of the values it holds:

  let counters = Vault.new("counters")   -- Vault(v), v not yet chosen
  Vault.set(counters, "hits", 1)         -- v := Int
  Vault.get(counters, "hits")            -- Option(Int)

The element type is fixed at the binding (Vault handles do not let-generalize — see demote_vault_handle_vars in the typechecker), so storing an Int and reading it back at some other type is a type error instead of a value reinterpreted at the wrong type.

Two doors are still untyped, deliberately:

  • new/open/whereis MINT a handle from a name, so they choose v
  rather than check it — a name-keyed global table cannot do better.
* `ns_set`/`ns_get`/`ns_drop` take a namespace STRING instead of a
  handle, so there is no handle to carry `v`.  Prefer the handle API.

Writing the type out (fn table() : Vault(v)) is the explicit opt back into an element-erased table, which Config uses on purpose.

Basic operations: Vault.new(name) — create a named table Vault.set(table, key, value) — insert or overwrite Vault.set_ttl(table, key, value, s) — insert with TTL in seconds Vault.get(table, key) — returns Some(value) or None Vault.drop(table, key) — remove a key (no-op if absent) Vault.update(table, key, fn) — apply fn to existing value Vault.size(table) — number of live entries

TTL semantics: expiry is checked lazily on get/update/size. There is no background sweeper thread; expired entries are evicted on access.

Functions

fnallall(table)#

Return all key-value pairs in the table as a list of (String, a) tuples.

fnclearclear(table)#

Remove all entries from the table, leaving it empty.

fndeletedelete(table, key)#

Remove a key from the table. Alias for drop. No-op if the key does not exist.

fndropdrop(table, key)#

Remove a key from the table. No-op if the key does not exist.

fngetget(table, key)#

Look up a key. Returns Some(value) if present and not expired, None otherwise.

fnget_orget_or(table, key, default)#

Return the value at key, or default if absent or expired.

fnhashas(table, key) : Bool#

Return true if key exists in the table and has not expired.

fnincrincr(table, key, delta) : Int#

Atomically add delta to the integer stored at key and return the new value. Requires a Vault(Int) — the runtime reads and writes an integer cell, so the element type is pinned rather than generic. A missing or non-integer entry is treated as 0. Any existing TTL on the key is preserved. Concurrent increments do not lose updates (unlike a get + set pair).

fnkeyskeys(table)#

Return all live (non-expired) keys in the table as a List(String). Always List(String), whatever was passed as a key — keys are stringified on insert.

    let t = Vault.new("ex")
    Vault.set(t, "a", 1)
    Vault.set(t, "b", 2)
    List.length(Vault.keys(t))  -- 2

Cost: O(n) where n is the number of entries in the table.
fnnewnew(name) : Vault(v)#

Create a new named vault table. Returns a Vault(v) handle, where v is the type of the values the table holds.

    let counters = Vault.new("counters")
    Vault.set(counters, "hits", 1)
    Vault.get(counters, "hits")   -- Some(1) : Option(Int)

The element type is fixed at the binding, so reading the table back at a
different type is a type error rather than a value reinterpreted at the
wrong type.
fnns_dropns_drop(ns, key)#

Remove a key from a named vault namespace. No-op if the namespace or key is absent.

fnns_getns_get(ns, key)#

Look up a key using a string namespace name. Returns None if the namespace does not exist or the key is absent.

ELEMENT-ERASED, like `ns_set` — the result type is whatever the caller
expects, not what was stored.

    Vault.ns_get("my_ns", "key")  -- Some(42) or None
fnns_setns_set(ns, key, value)#

Insert or overwrite a key-value pair using a string namespace name. Auto-creates the named vault if it does not yet exist. Useful when you store a namespace string rather than a vault handle.

ELEMENT-ERASED: there is no handle here to carry the element type, so
`ns_get` will hand back whatever type the caller asks for. Prefer
`open` + `get`/`set` when you can.

    Vault.ns_set("my_ns", "key", 42)
fnopenopen(name) : Vault(v)#

Open (or create) a named vault table. Returns an existing table if one with this name already exists; otherwise creates a new empty table.

    let rate_table = Vault.open("rate_limits")

Like `new` and `whereis`, this MINTS a handle from a string, so it is where
the element type is chosen rather than checked: two `Vault.open` calls on
the same name may pick different `v`. Bind the handle once and pass it
around — that is what makes the element type stick.
fnpush_cappedpush_capped(table, key, value, max) : Unit#

Atomically append value to the list stored at key (newest at the tail) and keep only the last max elements. Requires a Vault(List(e)); value is an e. The whole read-append-trim-write runs under the shard lock, so concurrent pushes to the same bounded buffer do not clobber each other (unlike a get + List.append + set). A missing or non-list entry starts from the empty list; max <= 0 keeps everything. Any existing TTL is preserved. Ideal for fixed-size ring buffers and inboxes.

fnputput(table, key, value)#

Insert or overwrite a key-value pair. Alias for set. Removes any previous TTL.

fnput_newput_new(table, key, value) : Bool#

Atomically insert value at key only if no live entry already exists. Returns true if this call inserted the value, false if a live entry was already present.

Unlike `set`, the check-and-insert is a single atomic step under the shard
lock, so concurrent callers racing on the same key cannot both succeed —
exactly one receives `true`. Use it as a lock or an idempotency claim.
fnput_new_ttlput_new_ttl(table, key, value, ttl_secs) : Bool#

Like put_new, but the claimed entry expires after ttl_secs seconds so a caller that crashes after claiming the key does not hold it forever.

fnsetset(table, key, value)#

Insert or overwrite a key-value pair. Removes any previous TTL.

fnset_ttlset_ttl(table, key, value, ttl_secs)#

Insert or overwrite a key-value pair with a TTL (time-to-live). The entry expires automatically after ttl_secs seconds. Expiry is checked lazily — no background thread required.

fnsizesize(table) : Int#

Return the number of live (non-expired) entries in the table.

fnupdateupdate(table, key, f)#

Apply f to the current value stored at key and replace it with the result. No-op if the key is absent or expired.

`f` must be `(v) -> v` for a `Vault(v)` — an update cannot change the
table's element type.
fnwhereiswhereis(name) : Option(Vault(v))#

Look up a vault table by its registered name. Returns Some(table) if a table with that name exists, None otherwise. Any actor can call this — no handle required.

Mints a handle from a string, so like `new`/`open` it chooses the element
type rather than checking it.