Packages

Persistent ETS tables backed by DETS — fast in-memory access with automatic disk persistence for the BEAM

Current section

Files

Jump to
shelf src shelf@bag.erl
Raw

src/shelf@bag.erl

-module(shelf@bag).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/shelf/bag.gleam").
-export([open_config/3, open/5, close/1, with_table/6, member/2, to_list/1, fold/3, size/1, insert/3, insert_list/2, delete_key/2, delete_object/3, delete_all/1, save/1, reload/1, sync/1, lookup/2]).
-export_type([p_bag/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-opaque p_bag(LXA, LXB) :: {p_bag,
shelf@internal:ets_ref(),
shelf@internal:dets_ref(),
shelf@internal:guardian_ref(),
shelf:write_mode(),
gleam@dynamic@decode:decoder({LXA, LXB})}.
-file("src/shelf/bag.gleam", 65).
?DOC(
" Open a persistent bag table with full configuration.\n"
"\n"
" If the DETS file exists, its contents are loaded into a fresh ETS\n"
" table after validating each entry through the provided decoders.\n"
" If no file exists, both tables start empty.\n"
"\n"
" The DETS file path is validated against the configured base directory.\n"
"\n"
" ```gleam\n"
" let config =\n"
" shelf.config(name: \"tags\", path: \"tags.dets\",\n"
" base_directory: \"/app/data\")\n"
" |> shelf.write_mode(shelf.WriteThrough)\n"
" let assert Ok(table) =\n"
" bag.open_config(config, key: decode.string, value: decode.string)\n"
" ```\n"
).
-spec open_config(
shelf:config(),
gleam@dynamic@decode:decoder(LXC),
gleam@dynamic@decode:decoder(LXE)
) -> {ok, p_bag(LXC, LXE)} | {error, shelf:shelf_error()}.
open_config(Config, Key_decoder, Value_decoder) ->
gleam@result:'try'(
shelf@internal:generic_open(
Config,
<<"bag"/utf8>>,
Key_decoder,
Value_decoder
),
fun(Result) ->
{ok,
{p_bag,
erlang:element(1, Result),
erlang:element(2, Result),
erlang:element(3, Result),
erlang:element(4, Result),
erlang:element(5, Result)}}
end
).
-file("src/shelf/bag.gleam", 94).
?DOC(
" Open a persistent bag table with defaults (WriteBack mode).\n"
"\n"
" ```gleam\n"
" let assert Ok(table) =\n"
" bag.open(name: \"tags\", path: \"tags.dets\",\n"
" base_directory: \"/app/data\",\n"
" key: decode.string, value: decode.string)\n"
" ```\n"
).
-spec open(
binary(),
binary(),
binary(),
gleam@dynamic@decode:decoder(LXK),
gleam@dynamic@decode:decoder(LXM)
) -> {ok, p_bag(LXK, LXM)} | {error, shelf:shelf_error()}.
open(Name, Path, Base_directory, Key_decoder, Value_decoder) ->
open_config(
shelf:config(Name, Path, Base_directory),
Key_decoder,
Value_decoder
).
-file("src/shelf/bag.gleam", 116).
?DOC(
" Close the table, saving all data to disk.\n"
"\n"
" On `Ok(Nil)`, the handle must not be used again. If the final save\n"
" fails with a retryable persistence error, `close()` returns\n"
" `Error(...)` and leaves the table open so the caller can retry.\n"
" If close fails terminally, Shelf still releases resources and future\n"
" operations on the handle return `Error(TableClosed)`.\n"
).
-spec close(p_bag(any(), any())) -> {ok, nil} | {error, shelf:shelf_error()}.
close(Table) ->
shelf_ffi:close(
erlang:element(2, Table),
erlang:element(3, Table),
erlang:element(4, Table)
).
-file("src/shelf/bag.gleam", 134).
?DOC(
" Use a table within a callback, ensuring it is closed afterward.\n"
"\n"
" If the final save fails during close, `with_table` force-cleans the\n"
" table to release resources. If the callback succeeded, the close\n"
" error is returned; if both the callback and close fail, the callback\n"
" error is preserved.\n"
"\n"
" ```gleam\n"
" use table <- bag.with_table(\"tags\", \"tags.dets\",\n"
" base_directory: \"/app/data\",\n"
" key: decode.string, value: decode.string)\n"
" bag.insert(table, \"color\", \"red\")\n"
" ```\n"
).
-spec with_table(
binary(),
binary(),
binary(),
gleam@dynamic@decode:decoder(LXY),
gleam@dynamic@decode:decoder(LYA),
fun((p_bag(LXY, LYA)) -> {ok, LYE} | {error, shelf:shelf_error()})
) -> {ok, LYE} | {error, shelf:shelf_error()}.
with_table(Name, Path, Base_directory, Key_decoder, Value_decoder, Fun) ->
shelf@internal:generic_with_table(
fun() ->
open(Name, Path, Base_directory, Key_decoder, Value_decoder)
end,
fun close/1,
Fun
).
-file("src/shelf/bag.gleam", 169).
?DOC(" Check if a key exists without returning the values.\n").
-spec member(p_bag(LYQ, any()), LYQ) -> {ok, boolean()} |
{error, shelf:shelf_error()}.
member(Table, Key) ->
shelf_ffi:member(erlang:element(2, Table), Key).
-file("src/shelf/bag.gleam", 177).
?DOC(
" Return all key-value pairs as a list.\n"
"\n"
" **Warning**: loads entire table into memory.\n"
).
-spec to_list(p_bag(LYW, LYX)) -> {ok, list({LYW, LYX})} |
{error, shelf:shelf_error()}.
to_list(Table) ->
shelf_ffi:to_list(erlang:element(2, Table)).
-file("src/shelf/bag.gleam", 183).
?DOC(" Fold over all entries. Order is unspecified.\n").
-spec fold(p_bag(LZD, LZE), LZH, fun((LZH, LZD, LZE) -> LZH)) -> {ok, LZH} |
{error, shelf:shelf_error()}.
fold(Table, Initial, Fun) ->
shelf@internal:generic_fold(erlang:element(2, Table), Initial, Fun).
-file("src/shelf/bag.gleam", 193).
?DOC(" Return the number of entries in the table.\n").
-spec size(p_bag(any(), any())) -> {ok, integer()} |
{error, shelf:shelf_error()}.
size(Table) ->
shelf_ffi:size(erlang:element(2, Table)).
-file("src/shelf/bag.gleam", 201).
?DOC(" Insert a key-value pair. Duplicate key-value pairs are ignored.\n").
-spec insert(p_bag(LZQ, LZR), LZQ, LZR) -> {ok, nil} |
{error, shelf:shelf_error()}.
insert(Table, Key, Value) ->
shelf@internal:generic_insert(
erlang:element(2, Table),
erlang:element(3, Table),
erlang:element(5, Table),
Key,
Value
).
-file("src/shelf/bag.gleam", 211).
?DOC(" Insert multiple key-value pairs.\n").
-spec insert_list(p_bag(LZW, LZX), list({LZW, LZX})) -> {ok, nil} |
{error, shelf:shelf_error()}.
insert_list(Table, Entries) ->
shelf@internal:generic_insert_list(
erlang:element(2, Table),
erlang:element(3, Table),
erlang:element(5, Table),
Entries
).
-file("src/shelf/bag.gleam", 222).
?DOC(" Delete all values for the given key.\n").
-spec delete_key(p_bag(MAD, any()), MAD) -> {ok, nil} |
{error, shelf:shelf_error()}.
delete_key(Table, Key) ->
shelf@internal:generic_delete_key(
erlang:element(2, Table),
erlang:element(3, Table),
erlang:element(5, Table),
Key
).
-file("src/shelf/bag.gleam", 231).
?DOC(
" Delete a specific key-value pair.\n"
"\n"
" Only the exact matching pair is removed. Other values for the same\n"
" key are preserved.\n"
).
-spec delete_object(p_bag(MAJ, MAK), MAJ, MAK) -> {ok, nil} |
{error, shelf:shelf_error()}.
delete_object(Table, Key, Value) ->
shelf@internal:generic_delete_object(
erlang:element(2, Table),
erlang:element(3, Table),
erlang:element(5, Table),
Key,
Value
).
-file("src/shelf/bag.gleam", 250).
?DOC(
" Delete all entries from the table.\n"
"\n"
" The table remains open and usable after this call — only the data\n"
" is removed. To release the table entirely, use `close`.\n"
).
-spec delete_all(p_bag(any(), any())) -> {ok, nil} |
{error, shelf:shelf_error()}.
delete_all(Table) ->
shelf@internal:generic_delete_all(
erlang:element(2, Table),
erlang:element(3, Table),
erlang:element(5, Table)
).
-file("src/shelf/bag.gleam", 262).
?DOC(
" Snapshot the current ETS contents to DETS.\n"
"\n"
" Uses an atomic save strategy: data is written to a temporary file\n"
" first, then atomically renamed over the original DETS file. This\n"
" prevents data loss if the process is killed mid-save.\n"
).
-spec save(p_bag(any(), any())) -> {ok, nil} | {error, shelf:shelf_error()}.
save(Table) ->
shelf_ffi:save(erlang:element(2, Table), erlang:element(3, Table)).
-file("src/shelf/bag.gleam", 273).
?DOC(
" Discard unsaved ETS changes and reload from DETS.\n"
"\n"
" Clears the ETS table, re-reads all DETS entries, validates them\n"
" through the stored decoders, and loads valid entries into ETS.\n"
" Only useful in WriteBack mode — in WriteThrough mode, ETS and\n"
" DETS are always in sync.\n"
).
-spec reload(p_bag(any(), any())) -> {ok, nil} | {error, shelf:shelf_error()}.
reload(Table) ->
shelf@internal:generic_reload(
erlang:element(2, Table),
erlang:element(3, Table),
erlang:element(6, Table)
).
-file("src/shelf/bag.gleam", 283).
?DOC(
" Flush the DETS write buffer to the OS.\n"
"\n"
" DETS buffers writes internally. This forces them to be written\n"
" to the underlying filesystem. Most useful in WriteThrough mode\n"
" when you want to guarantee durability.\n"
).
-spec sync(p_bag(any(), any())) -> {ok, nil} | {error, shelf:shelf_error()}.
sync(Table) ->
shelf_ffi:sync_dets(erlang:element(2, Table), erlang:element(3, Table)).
-file("src/shelf/bag.gleam", 163).
?DOC(
" Look up all values for a key.\n"
"\n"
" Returns `Error(NotFound)` if the key does not exist.\n"
).
-spec lookup(p_bag(LYJ, LYK), LYJ) -> {ok, list(LYK)} |
{error, shelf:shelf_error()}.
lookup(Table, Key) ->
shelf_ffi:lookup_bag(erlang:element(2, Table), Key).