A single_node pub-sub hub with pattern matching subscriptions.
It has no dependencies.
Example:
# In one process:
Hub.subscribe("some_channel", %{name: _, age: age} when age > 42)
receive do
%{name: name} -> IO.puts("#{name} is older than 42")
end
# In another process:
Hub.publish("some_channel", %{name: "John", age: 48})
Add hub
to your list of dependencies in mix.exs
:
def deps do
[{:hub, "~> 0.6"}]
end
Behind the scenes the pattern is "decompiled" into an Elixir AST and saved with the subscription. When a message is published to a channel, the pattern of each subscription is checked against the message and the subscriptions that match receives the message.
If you have lots of messages and lots of subscribers in the same channel, this is probably not for you, since the performance cost of pattern matching each message against each subscriber could be a problem.
However, many applications with many published messages can easily split messages into multiple channels based on application specific criteria. If performance is a concern, having different channels should be used as much as possible.
A subscription is made with a quoted pattern:
Hub.subscribe_quoted("My channel", quote(do: {:some, pattern}))
A convenience macro, subscribe
, can be used to avoid the quote
. Given the default value of the options, the
following is equivalent to the above:
require Hub
Hub.subscribe("My channel", {:some, pattern})
To publish a message in a channel, call Hub.publish/2
:
Hub.publish("My channel", {:any, "valid", %{elixir: "term"}})
When a message is published to the pid of a subscription, it is send directly and unmodified to that process' mailbox.
The subscriber should receive
the message:
Hub.subscribe("Channel", {:hello, name})
Hub.subscribe("Channel", {:goodbye, name})
receive do
{:hello, name} -> IO.puts("Hello #{name}")
{:goodbye, name} -> IO.puts("Goodbye #{name}")
end
If the receiver is a GenServer, and you don't want a blocking receive
, use handle_info
instead:
def handle_info({:hello, name}, state) do
IO.puts("Hello #{name}")
{:noreply, state}
end
subscribe
and subscribe_quoted
accepts these options:
pid
(defaultself()
) is the process that should receive published messages.count
(default:infinity
) is how many times a subscription can be triggered before it is auto-unsubscribed.multi
(defaultfalse
). Whentrue
, thepattern
argument must be a list of multiple patterns. This is handy if combined withcount
.
Sometimes one wish to subscribe using a pattern involving local variables.
The subscribe
macro accepts a bind_quoted
argument, that will replace pinned variables with the given values.
E.g.
size = 42
Hub.subscribe("my channel", %{size: ^size}, bind_quoted: [size: size])
is equivalent to
Hub.subscribe("my channel", %{size: 42})
To unsubscribe, use the returned reference given when subscribing:
{:ok, subscription} = Hub.subscribe("my channel", {:hello, name})
Hub.unsubscribe(subscription)
To avoid race conditions, it can be useful to also flush matched messages, so they don't leak. Example:
{:ok, subscription} = Hub.subscribe("my channel", {:hello, name})
receive do
{:hello, name} -> IO.puts("Hello #{name}")
after
5_000 ->
Hub.unsubscribe_and_flush(subscription)
end
Subscribe only once to a message:
Hub.subscribe("My channel", {:hello, name}, count: 1)
when
is perfectly legal to use in the pattern:
Hub.subscribe("My channel", %User{age: age} when age > 42)
Subscribe another process:
Hub.subscribe("My channel", {:hello, name}, pid: child_pid)
Subscribe to all messages in a channel:
Hub.subscribe("My channel", _)
Subscribe to the first message that matches one of the patterns:
Hub.subscribe("My channel", [{:hello, name}, {:goodbye, name}], multi: true, count: 1)
Tests are run with mix test
. When submitting new code, make sure mix credo
also passes.
Hub will usually be tested against the two latest minor versions of Elixir and the three latest minor versions of Erlang. In all cases the latest patch version is used.
E.g. if the latest Elixir is 1.7.2 and the latest Erlang is 21.0.5, we test against:
- Elixir: 1.7.2 and 1.6.6
- Erlang: 21.0.5, 20.3.8.6 and 20.2.4
See in the github actions script, which versions are officially supported.