shelf/set
Types
An open persistent set table with typed keys and values.
Reads always go to ETS (fast). Writes go to ETS immediately and to DETS according to the configured write mode.
The table stores decoders used to validate data loaded from DETS, ensuring type safety at the persistence boundary.
pub opaque type PSet(k, v)
Values
pub fn close(table: PSet(k, v)) -> Result(Nil, shelf.ShelfError)
Close the table, saving all data to disk.
Performs a final snapshot of ETS to DETS, closes the DETS file, and deletes the ETS table.
On Ok(Nil), the handle must not be used again. If the final save
fails with a retryable persistence error, close() returns
Error(...) and leaves the table open so the caller can retry.
If close fails terminally, shelf still releases resources and future
operations on the handle return Error(TableClosed).
pub fn delete_all(
from table: PSet(k, v),
) -> Result(Nil, shelf.ShelfError)
Delete all entries from the table.
The table remains open and usable after this call — only the data
is removed. To release the table entirely, use close.
pub fn delete_key(
from table: PSet(k, v),
key key: k,
) -> Result(Nil, shelf.ShelfError)
Delete the entry with the given key.
pub fn delete_object(
from table: PSet(k, v),
key key: k,
value value: v,
) -> Result(Nil, shelf.ShelfError)
Atomic Compare-and-Delete: delete the entry only if both key and value match.
Unlike delete_key, which removes the entry regardless of its value,
this function checks the full #(key, value) tuple. If the stored
value doesn’t match, nothing is deleted — useful for optimistic
concurrency patterns where you want to avoid clobbering a concurrent
update.
pub fn fold(
over table: PSet(k, v),
from initial: acc,
with fun: fn(acc, k, v) -> acc,
) -> Result(acc, shelf.ShelfError)
Fold over all entries. Order is unspecified.
pub fn insert(
into table: PSet(k, v),
key key: k,
value value: v,
) -> Result(Nil, shelf.ShelfError)
Insert a key-value pair. Overwrites if key exists.
In WriteBack mode, only ETS is updated — call save() to persist.
In WriteThrough mode, both ETS and DETS are updated.
pub fn insert_list(
into table: PSet(k, v),
entries entries: List(#(k, v)),
) -> Result(Nil, shelf.ShelfError)
Insert multiple key-value pairs at once.
pub fn insert_new(
into table: PSet(k, v),
key key: k,
value value: v,
) -> Result(Nil, shelf.ShelfError)
Insert a key-value pair only if the key does not already exist.
Returns Error(KeyAlreadyPresent) if the key exists.
In WriteThrough mode, uniqueness is checked in ETS first, then DETS is written, then ETS. Since writes are owner-only (single process), there is no race between the check and write.
pub fn lookup(
from table: PSet(k, v),
key key: k,
) -> Result(v, shelf.ShelfError)
Look up the value for a key.
Reads from ETS — consistent microsecond latency regardless of table size or whether the data has been saved to disk.
Returns Error(NotFound) if the key does not exist.
pub fn member(
of table: PSet(k, v),
key key: k,
) -> Result(Bool, shelf.ShelfError)
Check if a key exists without returning the value.
pub fn open(
name name: String,
path path: String,
base_directory base_directory: String,
key key_decoder: decode.Decoder(k),
value value_decoder: decode.Decoder(v),
) -> Result(PSet(k, v), shelf.ShelfError)
Open a persistent set table with defaults (WriteBack mode).
let assert Ok(table) =
set.open(name: "users", path: "users.dets",
base_directory: "/app/data",
key: decode.string, value: decode.int)
pub fn open_config(
config config: shelf.Config,
key key_decoder: decode.Decoder(k),
value value_decoder: decode.Decoder(v),
) -> Result(PSet(k, v), shelf.ShelfError)
Open a persistent set table with full configuration.
If the DETS file exists, its contents are loaded into a fresh ETS table after validating each entry through the provided decoders. If no file exists, both tables start empty.
The DETS file path is validated against the configured base directory.
let config =
shelf.config(name: "cache", path: "cache.dets",
base_directory: "/app/data")
|> shelf.write_mode(shelf.WriteThrough)
let assert Ok(table) =
set.open_config(config, key: decode.string, value: decode.int)
pub fn reload(table: PSet(k, v)) -> Result(Nil, shelf.ShelfError)
Discard unsaved ETS changes and reload from DETS.
Clears the ETS table, re-reads all DETS entries, validates them through the stored decoders, and loads valid entries into ETS. Only useful in WriteBack mode — in WriteThrough mode, ETS and DETS are always in sync.
pub fn save(table: PSet(k, v)) -> Result(Nil, shelf.ShelfError)
Snapshot the current ETS contents to DETS.
Uses an atomic save strategy: data is written to a temporary file first, then atomically renamed over the original DETS file. This prevents data loss if the process is killed mid-save (the original file remains intact until the rename succeeds).
See the Durability story for the full layer-by-layer breakdown of which crashes this protects against.
// After a batch of writes...
let assert Ok(Nil) = set.save(table)
pub fn size(
of table: PSet(k, v),
) -> Result(Int, shelf.ShelfError)
Return the number of entries in the table.
pub fn sync(table: PSet(k, v)) -> Result(Nil, shelf.ShelfError)
Drain the DETS in-memory write buffer into the open DETS file.
Calls dets:sync/1. Most useful in WriteThrough mode after a critical
write, to make sure pending DETS writes are reflected in the file on
disk. Note: this does not call fsync(2) on the file descriptor — see
the Durability story
for what sync() does and does not guarantee.
pub fn to_list(
from table: PSet(k, v),
) -> Result(List(#(k, v)), shelf.ShelfError)
Return all key-value pairs as a list.
Warning: loads entire table into memory.
pub fn update_counter(
in table: PSet(k, Int),
key key: k,
increment amount: Int,
) -> Result(Int, shelf.ShelfError)
Atomically increment an integer value by the given amount.
The value associated with the key must be an integer. Returns the new value after incrementing. The increment can be negative.
let assert Ok(Nil) = set.insert(into: table, key: "hits", value: 0)
let assert Ok(1) = set.update_counter(in: table, key: "hits", increment: 1)
let assert Ok(3) = set.update_counter(in: table, key: "hits", increment: 2)
In WriteThrough mode, the ETS atomic increment happens first (only ETS supports update_counter), then DETS is updated. If the DETS write fails, the ETS increment is rolled back by applying the negated amount.
pub fn with_table(
name name: String,
path path: String,
base_directory base_directory: String,
key key_decoder: decode.Decoder(k),
value value_decoder: decode.Decoder(v),
fun fun: fn(PSet(k, v)) -> Result(a, shelf.ShelfError),
) -> Result(a, shelf.ShelfError)
Use a table within a callback, ensuring it is closed afterward.
The table is opened before the callback and closed after it returns
(even if it returns an error). Data is auto-saved on close; if the
final save fails, with_table force-cleans the table to release
resources. If the callback succeeded, the close error is returned;
if both the callback and close fail, the callback error is preserved.
use table <- set.with_table("cache", "cache.dets",
base_directory: "/app/data",
key: decode.string, value: decode.string)
set.insert(into: table, key: "key", value: "value")