Simple, global cache optimised for concurrent accesses.
8

Configure Feed

Select the types of activity you want to include in your feed.

remove worker, add erase

yoshie (Nov 6, 2025, 12:24 PM +0100) e44a9e70 f208560d

+78 -75
+4 -2
README.md
··· 6 6 Booklet is a simple, global cache optimised for concurrent accesses. 7 7 8 8 Booklets are designed for caching values that persist throughout your program's 9 - lifetime. Booklets can never be deleted, but offer thread-safe, atomic updates 10 - and fast concurrent reads. 9 + lifetime. Booklets are never automatically deleted or garbage collected. Call `erase` 10 + to reset a booklets' value to its default and remove internal references. 11 + 12 + They offer thread-safe, atomic updates and fast concurrent reads. 11 13 12 14 Booklet is implemented using atomic compare-and-swap operations on top of a 13 15 managed ETS table on Erlang, or a simple mutable reference on Javascript.
+15 -6
src/booklet.gleam
··· 1 1 /// A Booklet is an opaque, typed reference to a concurrent, atomic, global cache. 2 2 /// 3 3 /// Booklets are designed for caching values that persist throughout your program's 4 - /// lifetime. Booklets can never be deleted, but offer thread-safe, atomic updates 5 - /// and fast concurrent reads. 4 + /// lifetime. Booklets are never automatically deleted or garbage collected. Call `erase` 5 + /// to reset a booklets' value to its default and remove internal references. 6 + /// 7 + /// They offer thread-safe, atomic updates and fast concurrent reads. 6 8 /// 7 9 /// Booklet is implemented using atomic compare-and-swap operations on top of a 8 10 /// managed ETS table on Erlang, or a simple mutable reference on Javascript. ··· 35 37 ) -> value 36 38 37 39 /// Atomically replace the value stored in the booklet. 38 - pub fn set(in booklet: Booklet(value), to new_value: value) -> Nil { 39 - update(booklet, fn(_) { new_value }) 40 - Nil 41 - } 40 + @external(erlang, "booklet_ffi", "set") 41 + @external(javascript, "./booklet_ffi.mjs", "set") 42 + pub fn set(in booklet: Booklet(value), to new_value: value) -> Nil 43 + 44 + /// Erase the value stored in a booklet, resetting it to it's default 45 + /// value provided to `new`. 46 + /// 47 + /// On Erlang, this will remove the ETS Table entry. 48 + @external(erlang, "booklet_ffi", "erase") 49 + @external(javascript, "./booklet_ffi.mjs", "erase") 50 + pub fn erase(booklet: Booklet(value)) -> Nil
+46 -62
src/booklet_ffi.erl
··· 1 1 -module(booklet_ffi). 2 2 3 3 -behaviour(application). 4 - -export([start/2, stop/1]). 5 4 6 - -behaviour(supervisor). 7 - -export([init/1, start_worker/0]). 8 - 9 - -export([make/1, get/1, update/2]). 10 - 5 + -export([start/2, stop/1]). 6 + -export([make/1, get/1, set/2, update/2, erase/1]). 11 7 12 - % TODO: Query the worker again for the table if the table ref is invalid 13 - % TODO: store default_value inside the tuple so we can always recover 8 + -compile([no_auto_import]). 14 9 15 10 % -- Public API ---------------------------------------------------------------- 16 11 17 - 18 12 make(DefaultValue) -> 19 - Table = get_table(), 20 - Key = make_ref(), 21 - true = ets:insert_new(Table, {Key, 0, DefaultValue}), 22 - {Table, Key}. 13 + {erlang:make_ref(), DefaultValue}. 23 14 24 - 25 - get({Table, Key}) -> 26 - ets:lookup_element(Table, Key, 3). 15 + get({Key, DefaultValue}) -> 16 + Table = persistent_term:get(?MODULE), 17 + try 18 + ets:lookup_element(Table, Key, 3) 19 + catch 20 + error:badarg -> 21 + DefaultValue 22 + end. 27 23 24 + set({Key, _}, Value) -> 25 + Table = persistent_term:get(?MODULE), 26 + true = ets:insert(Table, {Key, 0, Value}), 27 + nil. 28 28 29 - update({Table, Key}, Updater) -> 30 - [{Key, Rev, Value}] = ets:lookup(Table, Key), 31 - NewValue = Updater(Value), 32 - Pattern = {Key, Rev, '_'}, 33 - Replacement = {const, {Key, Rev + 1, NewValue}}, 34 - case ets:select_replace(Table, [{Pattern, [], [Replacement]}]) of 35 - 0 -> update({Table, Key}, Updater); 36 - _ -> NewValue 29 + update({Key, DefaultValue} = Booklet, Updater) -> 30 + Table = persistent_term:get(?MODULE), 31 + case ets:lookup(Table, Key) of 32 + [{_, Rev, Value}] -> 33 + NewValue = Updater(Value), 34 + Replacement = {const, {Key, Rev + 1, NewValue}}, 35 + case ets:select_replace(Table, [{{Key, Rev, '_'}, [], [Replacement]}]) of 36 + 0 -> 37 + update(Booklet, Updater); 38 + _ -> 39 + NewValue 40 + end; 41 + [] -> 42 + NewValue = Updater(DefaultValue), 43 + case ets:insert_new(Table, {Key, 0, NewValue}) of 44 + true -> 45 + NewValue; 46 + false -> 47 + update(Booklet, Updater) 48 + end 37 49 end. 38 50 51 + erase({Key, _}) -> 52 + Table = persistent_term:get(?MODULE), 53 + ets:delete(Table, Key), 54 + nil. 39 55 40 56 % -- APPLICATION --------------------------------------------------------------- 41 57 42 58 start(_StartType, _StartArgs) -> 43 - supervisor:start_link(?MODULE, supervisor). 59 + Table = 60 + ets:new(booklet, 61 + [set, public, {keypos, 1}, {write_concurrency, auto}, {read_concurrency, true}]), 62 + ok = persistent_term:put(?MODULE, Table), 63 + {ok, erlang:self()}. 44 64 45 65 stop(_State) -> 66 + persistent_term:erase(?MODULE), 46 67 ok. 47 - 48 - % -- SUPERVISOR ---------------------------------------------------------------- 49 - 50 - init(supervisor) -> 51 - SupFlags = #{ 52 - strategy => one_for_one, 53 - intensity => 6, 54 - period => 3600 55 - }, 56 - ChildSpec = #{ 57 - id => booklet, 58 - start => {?MODULE, start_worker, []}, 59 - shutdown => brutal_kill 60 - }, 61 - {ok, {SupFlags, [ChildSpec]}}. 62 - 63 - % -- CHILD --------------------------------------------------------------------- 64 - 65 - start_worker() -> 66 - Pid = spawn_link(fun worker/0), 67 - true = register(booklet, Pid), 68 - {ok, Pid}. 69 - 70 - worker() -> 71 - Table = ets:new(booklet, [set, public, {keypos, 1}, {write_concurrency, auto}, {read_concurrency, true}]), 72 - worker_loop(Table). 73 - 74 - worker_loop(Table) -> 75 - receive 76 - {get, From, Ref} -> From ! {Ref, Table}, worker_loop(Table); 77 - _ -> worker_loop(Table) 78 - end. 79 - 80 - get_table() -> 81 - Ref = make_ref(), 82 - booklet ! {get, self(), Ref}, 83 - receive {Ref, Table} -> Table end.
+13 -5
src/booklet_ffi.mjs
··· 1 1 export function make(default_value) { 2 - return { value: default_value } 2 + return { value: default_value, default_value: default_value }; 3 3 } 4 4 5 5 export function get(key) { 6 - return key.value 6 + return key.value; 7 + } 8 + 9 + export function set(key, value) { 10 + key.value = value; 7 11 } 8 12 9 13 export function update(key, updater) { 10 - const new_value = updater(key.value); 11 - key.value = new_value; 12 - return new_value; 14 + const new_value = updater(key.value); 15 + key.value = new_value; 16 + return new_value; 17 + } 18 + 19 + export function erase(key) { 20 + key.value = key.default_value; 13 21 }