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@duplicate_bag.erl
Raw

src/shelf@duplicate_bag.erl

-module(shelf@duplicate_bag).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/shelf/duplicate_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_duplicate_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_duplicate_bag(MFL, MFM) :: {p_duplicate_bag,
shelf@internal:ets_ref(),
shelf@internal:dets_ref(),
shelf@internal:guardian_ref(),
shelf:write_mode(),
gleam@dynamic@decode:decoder({MFL, MFM})}.
-file("src/shelf/duplicate_bag.gleam", 65).
?DOC(
" Open a persistent duplicate 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: \"events\", path: \"events.dets\",\n"
" base_directory: \"/app/data\")\n"
" |> shelf.write_mode(shelf.WriteThrough)\n"
" let assert Ok(table) =\n"
" duplicate_bag.open_config(config,\n"
" key: decode.string, value: decode.string)\n"
" ```\n"
).
-spec open_config(
shelf:config(),
gleam@dynamic@decode:decoder(MFN),
gleam@dynamic@decode:decoder(MFP)
) -> {ok, p_duplicate_bag(MFN, MFP)} | {error, shelf:shelf_error()}.
open_config(Config, Key_decoder, Value_decoder) ->
gleam@result:'try'(
shelf@internal:generic_open(
Config,
<<"duplicate_bag"/utf8>>,
Key_decoder,
Value_decoder
),
fun(Result) ->
{ok,
{p_duplicate_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/duplicate_bag.gleam", 94).
?DOC(
" Open a persistent duplicate bag table with defaults (WriteBack mode).\n"
"\n"
" ```gleam\n"
" let assert Ok(table) =\n"
" duplicate_bag.open(name: \"events\", path: \"events.dets\",\n"
" base_directory: \"/app/data\",\n"
" key: decode.string, value: decode.string)\n"
" ```\n"
).
-spec open(
binary(),
binary(),
binary(),
gleam@dynamic@decode:decoder(MFV),
gleam@dynamic@decode:decoder(MFX)
) -> {ok, p_duplicate_bag(MFV, MFX)} | {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/duplicate_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_duplicate_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/duplicate_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 <- duplicate_bag.with_table(\"events\", \"events.dets\",\n"
" base_directory: \"/app/data\",\n"
" key: decode.string, value: decode.string)\n"
" duplicate_bag.insert(table, \"click\", \"btn_1\")\n"
" ```\n"
).
-spec with_table(
binary(),
binary(),
binary(),
gleam@dynamic@decode:decoder(MGJ),
gleam@dynamic@decode:decoder(MGL),
fun((p_duplicate_bag(MGJ, MGL)) -> {ok, MGP} | {error, shelf:shelf_error()})
) -> {ok, MGP} | {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/duplicate_bag.gleam", 172).
?DOC(" Check if a key exists without returning the values.\n").
-spec member(p_duplicate_bag(MHB, any()), MHB) -> {ok, boolean()} |
{error, shelf:shelf_error()}.
member(Table, Key) ->
shelf_ffi:member(erlang:element(2, Table), Key).
-file("src/shelf/duplicate_bag.gleam", 183).
?DOC(
" Return all key-value pairs as a list.\n"
"\n"
" **Warning**: loads entire table into memory.\n"
).
-spec to_list(p_duplicate_bag(MHH, MHI)) -> {ok, list({MHH, MHI})} |
{error, shelf:shelf_error()}.
to_list(Table) ->
shelf_ffi:to_list(erlang:element(2, Table)).
-file("src/shelf/duplicate_bag.gleam", 191).
?DOC(" Fold over all entries. Order is unspecified.\n").
-spec fold(p_duplicate_bag(MHO, MHP), MHS, fun((MHS, MHO, MHP) -> MHS)) -> {ok,
MHS} |
{error, shelf:shelf_error()}.
fold(Table, Initial, Fun) ->
shelf@internal:generic_fold(erlang:element(2, Table), Initial, Fun).
-file("src/shelf/duplicate_bag.gleam", 201).
?DOC(" Return the number of entries in the table.\n").
-spec size(p_duplicate_bag(any(), any())) -> {ok, integer()} |
{error, shelf:shelf_error()}.
size(Table) ->
shelf_ffi:size(erlang:element(2, Table)).
-file("src/shelf/duplicate_bag.gleam", 209).
?DOC(" Insert a key-value pair. Duplicates are preserved.\n").
-spec insert(p_duplicate_bag(MIB, MIC), MIB, MIC) -> {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/duplicate_bag.gleam", 219).
?DOC(" Insert multiple key-value pairs.\n").
-spec insert_list(p_duplicate_bag(MIH, MII), list({MIH, MII})) -> {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/duplicate_bag.gleam", 230).
?DOC(" Delete all values for the given key.\n").
-spec delete_key(p_duplicate_bag(MIO, any()), MIO) -> {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/duplicate_bag.gleam", 242).
?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_duplicate_bag(MIU, MIV), MIU, MIV) -> {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/duplicate_bag.gleam", 261).
?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_duplicate_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/duplicate_bag.gleam", 273).
?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_duplicate_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/duplicate_bag.gleam", 284).
?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_duplicate_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/duplicate_bag.gleam", 294).
?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_duplicate_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/duplicate_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_duplicate_bag(MGU, MGV), MGU) -> {ok, list(MGV)} |
{error, shelf:shelf_error()}.
lookup(Table, Key) ->
shelf_ffi:lookup_bag(erlang:element(2, Table), Key).