Skip to content
Jennifer Programming Language

kv - in-process key/value store

Enable with use kv;. An in-process key/value store with per-key TTL - the local, no-server counterpart to the memcache / redis clients. Open a store, then set / get / delete / touch / increment; values expire on their own. A store is a handle (kv.Store) into a per-interpreter registry, so a kv.Store value shares its backing map across copies and across spawned tasks - the shared mutable state a pure .j module cannot hold itself. This is what backs the in-process option of the kvstore backend selector (and so session / ratelimit). Pure Go stdlib, so it is on both binaries.

jennifer
use kv;

def store as kv.Store init kv.open();
kv.set($store, "greeting", "hello", 60);   # expires in 60 s
io.printf("%s\n", kv.get($store, "greeting"));

Surface

CallReturns
kv.open()kv.Storea fresh, empty in-memory store (reset each run)
kv.openFile(path)kv.Storea store persisted to path - loaded on open, rewritten after every mutation, so it survives across jennifer run invocations
kv.set(store, key, value, ttl)store value, expiring in ttl seconds (0 = never)
kv.add(store, key, value, ttl)boolstore only if the key is absent; whether it stored
kv.get(store, key)stringthe value, or "" when absent / expired
kv.has(store, key)boolwhether the key is present and unexpired (tells "" from absent)
kv.delete(store, key)boolremove the key; whether it existed
kv.touch(store, key, ttl)boolre-arm the key's expiry; whether it existed
kv.incr(store, key, delta)intadd delta (signed - negative decrements, no separate decr) to the numeric value; the new value, or -1 when the key is absent (it is not created)
kv.close(store)drop the store and free its handle

Semantics

  • TTL: lazy + periodic sweep. An expired entry is evicted on the next access to that key, and a full sweep of expired entries runs periodically (every few hundred mutations). Without the sweep, a flood of distinct short-lived keys that are never accessed again (a rate limiter keyed by an untrusted IP) would pile up expired entries forever; the sweep bounds memory to the unexpired working set.
  • No hard size cap on live data. There is no ceiling on unexpired entries - storing unbounded live data OOMs the process, exactly as an unbounded list would. That is the program's own responsibility. For an adversarial or unbounded working set, use memcache / redis (a server-side maxmemory eviction policy) via the kvstore selector; close frees the whole store.
  • incr mirrors memcache. It does not create the key (returns -1 when absent) and errors on a non-numeric value, so it composes into the same increment-then-add window pattern the distributed backends use. delta is signed - kv.incr($s, $k, -1) decrements, so there is no separate decr (one verb, both directions). Unlike memcached's DECR it does not floor at 0; clamp yourself if you need a non-negative counter.
  • Handles are shared, values are copies. The kv.Store handle shares one backing map (that is the point - it survives value-copies and spawn); the string values it stores are ordinary copies.
  • Single process. State lives in this process only. open is in-memory (gone when the program exits); openFile persists to disk and survives across runs (a rewrite-the-whole-file flush after every mutation - simple and correct, not fast). Both are safe for spawned tasks within one process (a per-store mutex; incr is atomic), but not for concurrent separate processes on one file. For state shared across processes (multiple workers, a web fleet), use memcache / redis - the kvstore selector switches backends behind one API.

See also