{ const convertedKey = urlBase64ToUint8Array(getPublicKey()); return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: convertedKey }); }); } Your VAPID public key A special utility function
do create table(:push_subscriptions, primary_key: false) do add :id, :binary_id, primary_key: true add :user_id, references(:users, on_delete: :nothing, type: :binary_id), null: false add :digest, :text, null: false add :data, :text, null: false timestamps() end create unique_index(:push_subscriptions, [:user_id, :digest]) end end A hashed version of the payload
persist(user_id, data) do %Schema{} |> Changeset.change(%{user_id: user_id, data: data}) |> Changeset.change(%{digest: compute_digest(data)}) |> Repo.insert(on_conflict: :nothing) end defp compute_digest(data) do :sha256 |> :crypto.hash(data) |> Base.encode16() end end If the particular subscription is already persisted for the user, do nothing.
@type t :: %__MODULE__{ body: String.t(), tag: String.t() | nil } def serialize(%__MODULE__{} = payload) do payload |> Map.from_struct() |> Poison.encode!() end end Include pretty much anything you may need in your service worker.
for the 'push' event. self.addEventListener('push', function (event) { const data = event.data ? event.data.json() : {}; const payload = { body: data.body }; // Keep the service worker alive until the notification is created. event.waitUntil(self.registration.showNotification('Level', payload)); });
a message... defp send_push_notifications(notifiable_ids, payload) do Enum.each(notifiable_ids, fn user_id -> WebPush.send_web_push(user_id, payload) end) end
requests should not block the main process • Ordering of notifications should be controlled • Failed requests should be retried • Subscriptions should be deleted when they are no longer valid
user IDs that should see it • For each user, fetch/cache their subscriptions (could be > 1) • For each subscription, send the payload and handle the response — retry, delete, etc. — independent of the other subscriptions
caching those subscriptions to reduce DB roundtrips, and dispatching messages to send payloads. Subscription Responsible for making network requests to the endpoint, handling responses, retrying the request as necessary, and deleting the subscription if it become invalid.
of users and subscriptions in the system • Users and subscriptions can be created and destroyed in realtime • (Static) Supervisors are not sufficient for our needs because we can’t know what processes we need upfront • Thus, we need to use DynamicSupervisors!
children = [ # ... {Registry, keys: :unique, name: Level.Registry}, Level.WebPush ] opts = [strategy: :one_for_one, name: Level.Supervisor] Supervisor.start_link(children, opts) end end Fire up the Registry process and WebPush supervisor on application boot More on why we need the Registry momentarily…
Supervisor.start_link(__MODULE__, arg, name: __MODULE__) end @impl true def init(_arg) do children = [ UserSupervisor, SubscriptionSupervisor ] Supervisor.init(children, strategy: :one_for_one) end end Responsible for keeping our dynamic supervisors alive and healthy This one is just a plain old supervisor
do user_id |> UserSupervisor.start_worker() |> handle_start_worker(user_id, payload) end defp handle_start_worker({:ok, _pid}, user_id, payload) do UserWorker.send_web_push(user_id, payload) end defp handle_start_worker({:error, {:already_started, _pid}}, user_id, payload) do UserWorker.send_web_push(user_id, payload) end defp handle_start_worker(err, _, _), do: err end Calls the public API of the worker Dynamically starts a worker process
do DynamicSupervisor.start_link(__MODULE__, arg, name: __MODULE__) end def start_worker(user_id) do spec = {UserWorker, user_id} DynamicSupervisor.start_child(__MODULE__, spec) end @impl true def init(_arg) do DynamicSupervisor.init(strategy: :one_for_one) end end No children need to be specified on init!
start_link(user_id) do GenServer.start_link(__MODULE__, user_id, name: via_tuple(user_id)) end defp via_tuple(user_id) do {:via, Registry, {Level.Registry, {:web_push_user, user_id}}} end def send_web_push(user_id, %Payload{} = payload) do GenServer.cast(via_tuple(user_id), {:send_web_push, payload}) end end Instead of by static name, children are “addressed” via the Registry
{:ok, %__MODULE__{user_id: user_id}} end @impl true def handle_cast({:send_web_push, payload}, state) do state.user_id |> fetch_subscriptions() |> Enum.each(fn {id, subscription} -> send_to_subscription(id, subscription, payload) end) {:noreply, state} end end Fetch subscriptions for the user and dispatch to subscription workers
payload}, state) do make_request(state, payload, 0) end defp make_request(state, payload, attempts) do payload |> adapter().make_request(state.subscription) |> handle_push_response(state, payload, attempts) end defp handle_push_response({:ok, %_{status_code: 201}}, state, _, _) do {:noreply, state} end end Do nothing special when the request responds with a 201 status code.
payload}, state) do make_request(state, payload, 0) end defp make_request(state, payload, attempts) do payload |> adapter().make_request(state.subscription) |> handle_push_response(state, payload, attempts) end defp handle_push_response({:ok, %_{status_code: 404}}, state, _, _) do delete_subscription(state.digest) {:stop, :normal, state} end end Delete the subscription and shutdown the worker process
attempts}, state) do make_request(state, payload, attempts) end defp handle_push_response(_, state, payload, attempts) do if attempts < max_attempts() - 1, do: schedule_retry(payload, attempts + 1) {:noreply, state} end defp schedule_retry(payload, attempts) do Process.send_after(self(), {:retry_web_push, payload, attempts}, retry_timeout()) end end Schedule a retry in a configured interval if we have not exceeded max attempts
requirements # After someone posts a message... defp send_push_notifications(notifiable_ids, payload) do Enum.each(notifiable_ids, fn user_id -> WebPush.send_web_push(user_id, payload) end) end