cover/Elixir.WalletNotifications.NotificationStore.html

1 defmodule WalletNotifications.NotificationStore do
2 @moduledoc """
3 ETS-backed GenServer for notification persistence.
4
5 Tables:
6 - `:wallet_notifications` — notifications keyed by notification_id.
7 - `:wallet_notifications_user_idx` — bag: {user_id, notification_id}.
8 - `:wallet_notifications_idem_idx` — set: {{user_id, idempotency_key}, notification_id}.
9 """
10 use GenServer
11
12 alias WalletNotifications.Notification
13
14 @table :wallet_notifications
15 @user_idx :wallet_notifications_user_idx
16 @idem_idx :wallet_notifications_idem_idx
17
18
:-(
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
19
20 @spec store(Notification.t()) :: :ok | {:ok, Notification.t()}
21 34 def store(%Notification{} = n), do: GenServer.call(__MODULE__, {:store, n})
22
23 @spec get(String.t()) :: {:ok, Notification.t()} | {:error, :not_found}
24 35 def get(id), do: GenServer.call(__MODULE__, {:get, id})
25
26 @spec update(Notification.t()) :: :ok | {:error, :not_found}
27 35 def update(%Notification{} = n), do: GenServer.call(__MODULE__, {:update, n})
28
29 @spec list_by_user(String.t()) :: [Notification.t()]
30 4 def list_by_user(user_id), do: GenServer.call(__MODULE__, {:list_by_user, user_id})
31
32 @spec list_by_status(atom()) :: [Notification.t()]
33
:-(
def list_by_status(status), do: GenServer.call(__MODULE__, {:list_by_status, status})
34
35 43 def reset, do: GenServer.call(__MODULE__, :reset)
36
37 @impl true
38 def init(_) do
39
:-(
:ets.new(@table, [:set, :protected, :named_table])
40
:-(
:ets.new(@user_idx, [:bag, :protected, :named_table])
41
:-(
:ets.new(@idem_idx, [:set, :protected, :named_table])
42 {:ok, %{}}
43 end
44
45 @impl true
46 def handle_call({:store, n}, _from, state) do
47 # Idempotency check
48 34 result = case n.idempotency_key do
49 30 nil -> do_store(n); :ok
50 key ->
51 4 idem_key = {n.user_id, key}
52 4 case :ets.lookup(@idem_idx, idem_key) do
53 3 [] -> do_store(n); :ok
54 [{_, existing_id}] ->
55 1 case :ets.lookup(@table, existing_id) do
56 1 [{_, existing}] -> {:ok, existing}
57
:-(
[] -> do_store(n); :ok
58 end
59 end
60 end
61 34 {:reply, result, state}
62 end
63
64 @impl true
65 def handle_call({:get, id}, _from, state) do
66 35 result = case :ets.lookup(@table, id) do
67 34 [{_, n}] -> {:ok, n}
68 1 [] -> {:error, :not_found}
69 end
70 35 {:reply, result, state}
71 end
72
73 @impl true
74 def handle_call({:update, n}, _from, state) do
75 35 result = case :ets.lookup(@table, n.notification_id) do
76 35 [_] -> :ets.insert(@table, {n.notification_id, n}); :ok
77
:-(
[] -> {:error, :not_found}
78 end
79 35 {:reply, result, state}
80 end
81
82 @impl true
83 def handle_call({:list_by_user, user_id}, _from, state) do
84 4 ids = :ets.lookup(@user_idx, user_id) |> Enum.map(fn {_, id} -> id end)
85 4 ns = Enum.flat_map(ids, fn id ->
86 6 case :ets.lookup(@table, id) do
87 6 [{_, n}] -> [n]
88
:-(
[] -> []
89 end
90 end)
91 4 {:reply, ns, state}
92 end
93
94 @impl true
95 def handle_call({:list_by_status, status}, _from, state) do
96
:-(
ns = :ets.tab2list(@table)
97
:-(
|> Enum.map(fn {_, n} -> n end)
98
:-(
|> Enum.filter(&(&1.status == status))
99
:-(
{:reply, ns, state}
100 end
101
102 @impl true
103 def handle_call(:reset, _from, state) do
104 43 :ets.delete_all_objects(@table)
105 43 :ets.delete_all_objects(@user_idx)
106 43 :ets.delete_all_objects(@idem_idx)
107 43 {:reply, :ok, state}
108 end
109
110 defp do_store(n) do
111 33 :ets.insert(@table, {n.notification_id, n})
112 33 :ets.insert(@user_idx, {n.user_id, n.notification_id})
113 33 if n.idempotency_key do
114 3 :ets.insert(@idem_idx, {{n.user_id, n.idempotency_key}, n.notification_id})
115 end
116 end
117 end
Line Hits Source