Packages

A supposed-to-be-good CouchDB interface for elixir

Current section

Files

Jump to
couchdb_ex lib changes_communicator.ex
Raw

lib/changes_communicator.ex

defmodule CouchDBEx.Worker.ChangesCommunicator do
use GenServer
require Logger
@moduledoc false
# Client
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
# Server
def init(args) do
{:ok, _} =
Supervisor.start_link([], strategy: :one_for_one, name: CouchDBEx.Worker.ChangesCommunicator.Supervisor)
{:ok, Keyword.put(args, :watchers, %{})}
end
def handle_cast({:add_watcher, database, module_name, watcher_name, opts}, state) do
if Enum.any?(opts, fn {e, _} -> e in [:feed, :heartbeet] end) do
raise "Changing :feed or :heartbeet parameters is not supported"
end
{:ok, _} = Supervisor.start_child(
CouchDBEx.Worker.ChangesCommunicator.Supervisor,
%{
id: watcher_name,
start: {module_name, :start_link, [[name: watcher_name]]}
}
)
default_opts = %{
feed: "continuous",
include_docs: true,
since: "now",
heartbeat: 25 # Sends an empty string to keep the connection alive every once in a while
}
pre_final_opts = Map.merge(default_opts, Enum.into(opts, %{}))
maybe_body =
%{
doc_ids: pre_final_opts[:doc_ids],
selector: pre_final_opts[:selector]
}
|> Enum.filter(fn {_, v} -> not is_nil(v) end)
|> (fn m -> if(Enum.empty?(m), do: "", else: m) end).()
|> Enum.into(%{})
|> Poison.encode!
final_opts =
pre_final_opts
|> Map.delete(:doc_ids)
|> Map.delete(:selector)
{:ok, %HTTPoison.AsyncResponse{id: resp_id}} = HTTPoison.post(
"#{state[:hostname]}:#{state[:port]}/#{database}/_changes",
maybe_body,
[{"Content-Type", "application/json"}],
stream_to: self(),
params: final_opts,
recv_timeout: :infinity
)
state = put_in(state[:watchers][resp_id], watcher_name)
{:noreply, state}
end
def handle_cast({:remove_watcher, watcher_name}, state) do
Supervisor.terminate_child(CouchDBEx.Worker.ChangesCommunicator.Supervisor, watcher_name)
Supervisor.delete_child(CouchDBEx.Worker.ChangesCommunicator.Supervisor, watcher_name)
state =
case Enum.find(state[:watchers], fn{_, nm} -> nm == watcher_name end) do
{id, _} ->
:hackney.close(id)
Keyword.put(
state,
:watchers,
Map.delete(state[:watchers], id)
)
nil -> state
end
{:noreply, state}
end
def handle_info(msg, state) do
case msg do
%HTTPoison.AsyncStatus{code: code, id: id} ->
if code != 200 do
modname = Map.fetch!(state[:watchers], id)
Logger.error "Request to #{modname} finished with non-200 code (#{code})"
Logger.error "Stopping the #{modname} watcher"
GenServer.cast(__MODULE__, {:remove_watcher, modname})
end
%HTTPoison.AsyncHeaders{} -> :ok
%HTTPoison.AsyncChunk{chunk: chunk, id: id} ->
if chunk != "\n" do
modname = Map.fetch!(state[:watchers], id)
if not is_nil(modname) do
# Chunks come in splitted by a newline symbol
Enum.each(String.split(chunk, "\n", trim: true), fn ch ->
send(modname, {:couchdb_change, Poison.decode!(ch)})
end)
else
Logger.warn "Request id #{id} is not binded to any watcher, skipping"
end
end
%HTTPoison.AsyncEnd{id: id} ->
modname = Map.fetch!(state[:watchers], id)
Logger.warn "Connection to #{modname} closed, stopping the watcher"
GenServer.cast(__MODULE__, {:remove_watcher, modname})
other -> Logger.warn "#{__MODULE__} got an unexpected message: #{inspect other}"
end
{:noreply, state}
end
end