defmodule SData.MerkleList do use GenServer def start_link([cmp, name: name]) do GenServer.start_link(__MODULE__, cmp, [name: name]) end defp term_hash(term) do :crypto.hash(:sha256, (:erlang.term_to_binary term)) end @doc """ Initialize a Merkle List storage. `cmp` is a function that compares stored items and provides a total order. It must return: - `:after` if the first argument is more recent - `:duplicate` if the two items are the same - `:before` if the first argument is older """ def init(cmp) do root_item = :root root_hash = term_hash root_item state = %{ root: root_hash, top: root_hash, cmp: cmp, store: %{ root_hash => root_item } } {:ok, state} end defp state_push(item, state) do new_item = {item, state.top} new_item_hash = term_hash new_item new_store = Map.put(state.store, new_item_hash, new_item) %{ state | :top => new_item_hash, :store => new_store } end defp state_pop(state) do if state.top == state.root do :error else {item, next} = Map.get(state.store, state.top) new_store = Map.delete(state.store, state.top) new_state = %{ state | :top => next, :store => new_store } {:ok, item, new_state} end end defp insert_many(state, [], _callback) do state end defp insert_many(state, [item | rest], callback) do case state_pop(state) do :error -> new_state = state_push(item, insert_many(state, rest, callback)) callback.(item) new_state {:ok, front, state_rest} -> case state.cmp.(item, front) do :after -> new_state = state_push(item, insert_many(state, rest, callback)) callback.(item) new_state :duplicate -> insert_many(state, rest, callback) :before -> state_push(front, insert_many(state_rest, [item | rest], callback)) end end end def handle_cast({:insert, item}, state) do handle_cast({:insert_many, [item]}, state) end def handle_cast({:insert_many, items}, state) do handle_cast({:insert_many, items, fn _ -> nil end}, state) end def handle_cast({:insert_many, items, callback}, state) do items_sorted = Enum.sort(items, fn (x, y) -> state.cmp.(x, y) == :after end) new_state = insert_many(state, items_sorted, callback) {:noreply, new_state} end def handle_call({:read, qbegin, qlimit}, _from, state) do begin = qbegin || state.top limit = qlimit || 20 items = get_items_list(state, begin, limit) {:reply, items, state} end def handle_call(:top, _from, state) do {:reply, state.top, state} end def handle_call(:root, _from, state) do {:reply, state.root, state} end def handle_call({:has, hash}, _from, state) do {:reply, Map.has_key?(state.store, hash), state} end defp get_items_list(state, begin, limit) do case limit do 0 -> {:ok, [], begin} _ -> case Map.fetch(state.store, begin) do {:ok, :root} -> {:ok, [], nil } {:ok, {item, next}} -> case get_items_list(state, next, limit - 1) do {:ok, rest, past} -> {:ok, [ item | rest ], past } {:error, reason} -> {:error, reason} end :error -> {:error, begin} end end end @doc """ Compare function for timestamped strings """ def cmp_ts_str({ts1, str1}, {ts2, str2}) do cond do ts1 > ts2 -> :after ts1 < ts2 -> :before str1 == str2 -> :duplicate str1 > str2 -> :after str1 < str2 -> :before end end end