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 Int, String, Bool or Atom values. Function values, Pids, and Tasks cannot be used as keys, and neither can Tuple or Ctor values in compiled code (it panics: a heap value carries no arity at runtime; the interpreter accepts them). Keys are ENCODED to strings on the way in, which is why the handle is not parameterised by a key type. The encoding keeps the key's kind, so set(t, 1, v) and set(t, "1", v) are two different entries. (Compiled code cannot tell Int, Bool and Atom keys apart: true and 1 are the same key there.) keys(t) is typed List(String): String keys come back unchanged, but any other key comes back encoded in compiled code (Int 1 as "i:1") and as its original value in the interpreter, so rely on keys only in String-keyed tables.

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

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

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. (Config no longer needs it: its tables are Vault(Config.Value), a tagged sum read back through typed keys.)

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, in unspecified order. Built from keys, so it carries the same caveat: non-String keys come back encoded in compiled code. O(n).

Examples

march> let va = Vault.new("doc_all")
march> Vault.set(va, "x", 1)
march> let va = Vault.new("doc_all")
march> Vault.all(va)
[("x", 1)]
march> Vault.all(Vault.new("doc_all_empty"))
[]
fnclearclear(table)#

Remove all entries from the table, leaving it empty. The table stays registered under its name (contrast close). O(n) in the number of entries.

Examples

march> let vc = Vault.new("doc_clear")
march> Vault.set(vc, "a", 1)
march> let vc = Vault.new("doc_clear")
march> Vault.set(vc, "b", 2)
march> let vc = Vault.new("doc_clear")
march> Vault.clear(vc)
march> let vc = Vault.new("doc_clear")
march> Vault.size(vc)
0
fncloseclose(table) : Unit#

Retire a table: unregister its name (so whereis and open no longer find it, and a later new of the same name makes a fresh table) and release everything it holds. The table's memory is freed once the last handle to it is gone.

For a table with one owner -- per-session or per-request state -- close it when the owner is done: a table that is never closed stays registered, with its contents, for the life of the process. A handle used after the close still works, on an empty, unregistered table, so a task that outlives its owner by a little reads None rather than crashing.

march> let req = Vault.new("request_42")
march> Vault.set(req, "user", "ada")
march> let req = Vault.new("request_42")
march> Vault.close(req)
march> let req = Vault.new("request_42")
march> Vault.get_or(req, "user", "gone")  -- the handle still works, on an empty table
"gone"
march> Vault.has(Vault.open("request_42"), "user")  -- open now makes a fresh table
false
fndeletedelete(table, key)#

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

Examples

march> let vdel = Vault.new("doc_delete")
march> Vault.put(vdel, "k", 1)
march> let vdel = Vault.new("doc_delete")
march> Vault.delete(vdel, "k")
march> let vdel = Vault.new("doc_delete")
march> Vault.get(vdel, "k")
None
march> let vdel = Vault.new("doc_delete")
march> Vault.delete(vdel, "never_set")
()
fndropdrop(table, key)#

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

Examples

march> let vd = Vault.new("doc_drop")
march> Vault.set(vd, "x", 1)
march> let vd = Vault.new("doc_drop")
march> Vault.drop(vd, "x")
march> let vd = Vault.new("doc_drop")
march> Vault.has(vd, "x")
false
fngetget(table, key)#

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

Examples

march> let vg = Vault.new("doc_get")
march> Vault.set(vg, "x", 10)
march> let vg = Vault.new("doc_get")
march> Option.is_some(Vault.get(vg, "x"))  -- Vault.get gives Some(10)
true
march> let vg = Vault.new("doc_get")
march> Vault.get(vg, "missing")
None
fnget_orget_or(table, key, default)#

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

Examples

march> let vo = Vault.new("doc_get_or")
march> Vault.set(vo, "x", 5)
march> let vo = Vault.new("doc_get_or")
march> Vault.get_or(vo, "x", 0)
5
march> let vo = Vault.new("doc_get_or")
march> Vault.get_or(vo, "y", 0)
0
fnhashas(table, key) : Bool#

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

Examples

march> let vh = Vault.new("doc_has")
march> Vault.set(vh, "x", 1)
march> let vh = Vault.new("doc_has")
march> Vault.has(vh, "x")
true
march> let vh = Vault.new("doc_has")
march> Vault.has(vh, "y")
false
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).

Examples

march> let vi = Vault.new("doc_incr")
march> Vault.incr(vi, "hits", 1)
1
march> let vi = Vault.new("doc_incr")
march> Vault.incr(vi, "hits", 5)
6
march> let vi = Vault.new("doc_incr")
march> Vault.incr(vi, "hits", -2)
4
fnkeyskeys(table)#

Return all live (non-expired) keys in the table as a List(String). String keys come back unchanged; a key of any other kind comes back in its internal encoding in compiled code (Int 2 as "i:2"), so use keys on String-keyed tables (see the module header).

march> let ex = Vault.new("ex")
march> Vault.set(ex, "a", 1)
march> let ex = Vault.new("ex")
march> Vault.set(ex, "b", 2)
march> let ex = Vault.new("ex")
march> List.length(Vault.keys(ex))
2
march> let ints = Vault.new("ints")
march> Vault.set(ints, 2, 20)  -- an Int key: not the same entry as "2"
march> let ints = Vault.new("ints")
march> Vault.get_or(ints, "2", 0)
0

Cost: O(n) where n is the number of entries in the table. Collected shard by shard, so — like size — the list is not a single instant's snapshot of the whole table, and the key order is unspecified.

fnlive_tableslive_tables() : Int#

The number of Vault tables this process holds: created and not yet freed. A leak gauge: a long-running program whose count keeps rising is making tables it never closes. Always counts what the runtime holds, so a closed table still counts until its last handle is dropped.

Examples

The count depends on everything else the process has created, so compare it rather than pinning it:

march> Vault.live_tables() > 0
true
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.

march> let visits = Vault.new("visits")
march> Vault.set(visits, "home", 1)
march> let visits = Vault.new("visits")
march> Vault.get_or(visits, "home", 0)  -- Vault.get gives Some(1)
1

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; unlike ns_set it never creates the namespace.

Examples

march> Vault.ns_set("doc_ns_drop", "k", 7)
march> Vault.ns_drop("doc_ns_drop", "k")
march> Vault.ns_get("doc_ns_drop", "k")
None
march> Vault.ns_drop("doc_ns_absent", "k")
()
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.

march> Option.unwrap_or(Vault.ns_get("my_ns", "key"), 0)  -- ns_get gave Some(42)
42
march> Vault.ns_get("no_such_ns", "key")
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.

march> Vault.ns_set("my_ns", "key", 42)
march> Vault.get_or(Vault.open("my_ns"), "key", 0)
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.

march> let rate_table = Vault.open("rate_limits")
march> Vault.set(rate_table, "alice", 3)
march> Vault.get_or(Vault.open("rate_limits"), "alice", 0)  -- the same table
3

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.

Examples

march> let vb = Vault.new("doc_push_capped")
march> Vault.push_capped(vb, "log", 1, 2)
march> let vb = Vault.new("doc_push_capped")
march> Vault.push_capped(vb, "log", 2, 2)
march> let vb = Vault.new("doc_push_capped")
march> Vault.push_capped(vb, "log", 3, 2)
march> let vb = Vault.new("doc_push_capped")
march> Vault.get(vb, "log")
Some([2, 3])
fnputput(table, key, value)#

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

Examples

march> let vp = Vault.new("doc_put")
march> Vault.put(vp, "k", 1)
march> let vp = Vault.new("doc_put")
march> Vault.put(vp, "k", 2)
march> let vp = Vault.new("doc_put")
march> Vault.get_or(vp, "k", 0)  -- Vault.get gives Some(2)
2
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.

Examples

march> let vn = Vault.new("doc_put_new")
march> Vault.put_new(vn, "lock", "a")
true
march> let vn = Vault.new("doc_put_new")
march> Vault.put_new(vn, "lock", "b")
false
march> let vn = Vault.new("doc_put_new")
march> Vault.get_or(vn, "lock", "")  -- the first claim is kept
"a"
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.

Examples

march> let vj = Vault.new("doc_put_new_ttl")
march> Vault.put_new_ttl(vj, "job", "worker1", 60)
true
march> let vj = Vault.new("doc_put_new_ttl")
march> Vault.put_new_ttl(vj, "job", "worker2", 60)
false
fnsetset(table, key, value)#

Insert or overwrite a key-value pair. Removes any previous TTL. Keys keep their kind, so an Int key and a String key that print alike are two entries (see the module header for the compiled-code caveats).

Examples

march> let vs = Vault.new("doc_set")
march> Vault.set(vs, 1, "int key")
march> let vs = Vault.new("doc_set")
march> Vault.set(vs, "1", "string key")
march> let vs = Vault.new("doc_set")
march> Vault.get(vs, 1)
Some("int key")
march> let vs = Vault.new("doc_set")
march> Vault.size(vs)
2
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.

Examples

march> let vt = Vault.new("doc_ttl")
march> Vault.set_ttl(vt, "session", "abc", 3600)
march> let vt = Vault.new("doc_ttl")
march> Vault.get_or(vt, "session", "expired")  -- Vault.get gives Some("abc")
"abc"
fnsizesize(table) : Int#

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

Counted shard by shard rather than under one table-wide lock, so the result is not a single instant's snapshot: an entry added to an already-counted shard while a later one is being counted is not included. Concurrent writers could always change the answer, so treat it as a recent count, not a guarantee.

Examples

march> let vz = Vault.new("doc_size")
march> Vault.size(vz)
0
march> let vz = Vault.new("doc_size")
march> Vault.set(vz, "a", 1)
march> let vz = Vault.new("doc_size")
march> Vault.set(vz, "a", 2)
march> let vz = Vault.new("doc_size")
march> Vault.size(vz)
1
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.

Examples

march> let vu = Vault.new("doc_update")
march> Vault.set(vu, "n", 10)
march> let vu = Vault.new("doc_update")
march> Vault.update(vu, "n", fn x -> x * 2)
march> let vu = Vault.new("doc_update")
march> Vault.get_or(vu, "n", 0)  -- Vault.get gives Some(20)
20
march> let vu = Vault.new("doc_update")
march> Vault.update(vu, "absent", fn x -> x * 2)
march> let vu = Vault.new("doc_update")
march> Vault.has(vu, "absent")
false
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.

Examples

A handle has no printable form, so test for presence:

march> let vw = Vault.new("doc_whereis")
march> Option.is_some(Vault.whereis("doc_whereis"))
true
march> Option.is_some(Vault.whereis("doc_never_created"))
false