Skip to content

Key-Value Store

Sema includes a persistent, JSON-backed key-value store for storing structured data across sessions. Data is automatically flushed to disk on every write.

TIP

kv/open, kv/set, and kv/delete require filesystem write capabilities (they are gated by FS_WRITE).

How It Works

File path — You control where data is stored via the second argument to kv/open. Relative paths resolve from the current working directory. The file is not created until the first write (kv/set or kv/delete).

Store names — The first argument to kv/open is a logical handle used to reference the store in subsequent calls. Store names are scoped to the current process. Opening the same name twice replaces the previous handle.

Flushing — Every kv/set and kv/delete rewrites the entire backing file immediately. kv/close also flushes. There is no separate manual flush — persistence is automatic.

JSON format — The backing file is pretty-printed JSON, so you can inspect or edit it with any text editor. If an existing file contains malformed JSON, kv/open raises an error.

Supported value types:

SemaJSONNotes
nilnull
#t / #ftrue / false
Integersnumber
FloatsnumberNaN and Infinity become null
Stringsstring
ListsarrayRecursive
Maps (keyword keys)objectKeys become strings

Performance — Each write rewrites the whole file. This is ideal for small-to-medium stores (config, caches, counters). For large datasets or high-frequency writes, consider using file/write directly.

Functions

kv/open

Open (or create) a named KV store backed by a JSON file. If the file exists, its contents are loaded. Returns the store name.

sema
(kv/open "config" "/path/to/config.json")  ; => "config"
(kv/open "cache" "cache.json")             ; relative to CWD

If the file doesn't exist yet, no file is created — that happens on the first kv/set.

kv/get

Get a value by key. Returns nil if the key doesn't exist.

sema
(kv/get "config" "api-key")  ; => "sk-..." or nil

kv/set

Set a key-value pair. The value is serialized as JSON. Returns the value. Flushes to disk immediately.

sema
(kv/set "config" "api-key" "sk-...")
(kv/set "config" "retries" 3)
(kv/set "config" "tags" '("a" "b" "c"))
(kv/set "config" "user" {:name "Alice" :role "admin"})

kv/delete

Delete a key. Returns #t if the key existed, #f otherwise. Flushes to disk immediately.

sema
(kv/delete "config" "api-key")  ; => #t
(kv/delete "config" "api-key")  ; => #f (already deleted)

kv/keys

List all keys in the store. Returns a list of strings.

sema
(kv/keys "config")  ; => ("api-key" "retries" "tags")

kv/close

Close a store, flushing data and freeing the handle. Returns nil.

sema
(kv/close "config")

Data is safe even without calling kv/close (every write already flushes), but closing frees memory and releases the store name.

Examples

Basic usage

sema
;; Create a persistent store for caching API results
(kv/open "cache" "api-cache.json")

;; Store some data
(kv/set "cache" "user:123" {:name "Alice" :email "alice@example.com"})
(kv/set "cache" "user:456" {:name "Bob" :email "bob@example.com"})

;; Retrieve it
(kv/get "cache" "user:123")
; => {:email "alice@example.com" :name "Alice"}

;; List keys
(kv/keys "cache")
; => ("user:123" "user:456")

;; Clean up
(kv/delete "cache" "user:123")
(kv/close "cache")

Application configuration with defaults

sema
(kv/open "config" "app-config.json")

;; Set defaults only if not already configured
(when (nil? (kv/get "config" "theme"))
  (kv/set "config" "theme" "dark"))

(when (nil? (kv/get "config" "max-retries"))
  (kv/set "config" "max-retries" 3))

;; Use config values
(def theme (kv/get "config" "theme"))
(println (string/append "Using theme: " theme))

On first run this creates app-config.json with defaults. On subsequent runs, existing values are preserved.

Persistent run counter

sema
(kv/open "stats" "run-stats.json")

;; Increment run count across sessions
(let ((runs (or (kv/get "stats" "run-count") 0)))
  (kv/set "stats" "run-count" (+ runs 1))
  (kv/set "stats" "last-run" (time/format (time/now) "%Y-%m-%d %H:%M:%S")))

(println (string/append "Run #" (string (kv/get "stats" "run-count"))))
(kv/close "stats")

Structured data with maps and lists

sema
(kv/open "contacts" "contacts.json")

(kv/set "contacts" "alice"
  {:name "Alice" :email "alice@example.com" :tags '("admin" "dev")})

(kv/set "contacts" "bob"
  {:name "Bob" :email "bob@example.com" :tags '("dev")})

;; Retrieve and destructure
(def alice (kv/get "contacts" "alice"))
(:name alice)   ; => "Alice"
(:tags alice)   ; => ("admin" "dev")

;; List all contacts
(for-each (fn (key) (println (:name (kv/get "contacts" key))))
          (kv/keys "contacts"))
(kv/close "contacts")

Tips

  • The backing file is human-readable JSON — you can inspect or hand-edit it between runs.
  • Store names are just logical handles. Choose descriptive names like "config", "cache", or "sessions".
  • Use kv/keys with iteration for bulk operations like export or cleanup.
  • For write-heavy workloads on large datasets, consider writing JSON directly with file/write to avoid rewriting the entire file on each operation.