Declares that a module subscribes to topics of a manifest, and defines its handlers.
defmodule MyAppWeb.PetsLive do
use MyAppWeb, :live_view
use VerifiedPubSub.Subscriber, manifest: MyApp.Topics
def mount(_params, _session, socket) do
if connected?(socket), do: subscribe(:pets, %{owner_id: socket.assigns.id})
{:ok, socket}
end
handle_message :pets, :adopted, payload, socket do
{:noreply, stream_insert(socket, :pets, payload)}
end
ignore_message :pets, :rehomed
endThe handle_message/5 and ignore_message/2 calls of the module set the topics that it
subscribes to. You keep no separate list of topics. The module must handle or dismiss
each event that those topics declare, or the module does not compile.
use also imports VerifiedPubSub.Api. The module can then call subscribe/2,
broadcast!/4, and the other macros without a separate
use VerifiedPubSub, manifest: ....
Generated code
use generates two handle_info/2 clauses at the use site. The first clause matches
a VerifiedPubSub.Message from the manifest of this module. It passes the message to
private __verified_pubsub_dispatch__/4 clauses, which the module defines at
@before_compile. The second clause matches a message from any other manifest and
raises VerifiedPubSub.ManifestMismatchError. See "Messages from another manifest"
below.
handle_message/5 therefore generates no code of its own. It adds the clause to a
module attribute. The coverage check needs every clause, and the module has every
clause only at @before_compile. The attribute does not exist to avoid the
clause-grouping warning of Elixir, as an earlier version of this documentation said. A
clause that a macro generates carries generated: true metadata, and Elixir gives no
such warning for it.
The handle_info/2 clauses come from the use site, not from @before_compile, so
your own handle_info/2 clauses still work. Elixir tries your clauses after the
generated clauses. A catch-all clause of yours thus receives each message that is not
verified, and verified messages still reach the generated clauses.
Messages from another manifest
An application can declare more than one manifest. Two manifests can produce the same
wire topic when they share a :pubsub and declare topics that build the same string.
The disjointness check works only within one manifest, because a manifest cannot see
the other manifests while it compiles.
In that case, Phoenix.PubSub delivers the message, and its topic and event atoms
can match a clause here. But its payload follows the field declarations of the
sender. The compiler never checked that payload against the handler. Thus the
generated clause raises VerifiedPubSub.ManifestMismatchError, and the handler does
not run.
That clause comes before each handle_info/2 clause that you write, on purpose. A
catch-all clause of yours thus does not receive a message from another manifest. A
catch-all clause is the documented way to ignore unrelated messages. If it received
this message, nothing would ever report the collision.
For full isolation, give each manifest its own Phoenix.PubSub. Each instance has its
own registry, so a message never goes from one instance to the other:
children = [
Supervisor.child_spec({Phoenix.PubSub, name: Pets.PubSub}, id: :pets_pubsub),
Supervisor.child_spec({Phoenix.PubSub, name: Treats.PubSub}, id: :treats_pubsub)
]Which modules can bind which manifest is a separate question of dependency boundaries.
A manifest is a plain module, so mix xref or the boundary package can enforce
those boundaries better than this library can.
Messages this module does not expect
A module that defines a handle_info/2 clause loses the default clause that
use GenServer and use Phoenix.LiveView add. An unexpected message then raises
FunctionClauseError, and nothing logs it. Each GenServer with its own
handle_info/2 has this behavior. This module cannot prevent it: Elixir 1.20 made
super/2 for GenServer callbacks an error, so no code can call the default body.
When your process receives messages that are not verified, add your own catch-all clause:
handle_message :pets, :adopted, payload, state do
{:noreply, state}
end
# Matched after the generated clauses, so verified messages still reach them.
def handle_info(_other, state), do: {:noreply, state}
Summary
Functions
Handles one event on one topic.
Handles one event on one topic, and matches the params of the topic.
Declares that this module does nothing with an event, or with a list of events.
Functions
Handles one event on one topic.
pattern matches the payload of the message. When pattern is a
%VerifiedPubSub.Message{} pattern in the source, it matches the whole message
instead, so it can match on params.
Handles one event on one topic, and matches the params of the topic.
The first arguments are the arguments of broadcast!/4, in the same order, but here
they match and do not build. Unlike a broadcast, the pattern can name a subset of
the params. A param that the topic does not declare is a compile error, because that
pattern can never match.
Usage
broadcast! :pets, %{owner_id: id}, :adopted, payload
handle_message :pets, %{owner_id: owner}, :adopted, payload, socketThe clause binds owner from the params, without a match on the whole message. A
literal limits the clause to that value:
handle_message :pets, %{owner_id: "7"}, :adopted, payload, socket
Declares that this module does nothing with an event, or with a list of events.
ignore_message :pets, :rehomed
ignore_message :pets, [:renamed, :rehomed]The call satisfies the coverage check without a handler. The generated clause returns
{:noreply, state}, which is correct for both a GenServer and a LiveView.