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).

// 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)

Flush the DETS write buffer to the OS.

DETS buffers writes internally. This forces them to be written to the underlying filesystem. Most useful in WriteThrough mode when you want to guarantee durability.

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(table, "hits", 0)
let assert Ok(1) = set.update_counter(table, "hits", 1)
let assert Ok(3) = set.update_counter(table, "hits", 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(table, "key", "value")
Search Document