Current section
Files
Jump to
Current section
Files
lib/services/live_helpers/entry.ex
defmodule Sorcery.LiveHelpers do
@moduledoc ~s"""
Functions for combining Sorcery and LiveViews
"""
alias ElixirLS.LanguageServer.Plugins.Phoenix
# {{{ callback docs
@doc ~s"""
Wherever you keep your handle_info functions, put this:
```elixir
defmodule MyModule do
use Sorcery.LiveHelpers
def handle_info({:sorcery, _} = msg, socket), do: handle_sorcery(msg, socket)
# If you have any other handle_info's, put them BELOW
end
```
By doing this, the socket will automatically communicate with the rest of Sorcery.
For example whenever we receive new data about a portal, this is where it happens.
You can optionally pass in an options argument, which is handy for writing your own middleware.
```elixir
defmodule SomeModule do
def my_func(socket, msg), {:ok, socket}
def another_func(socket, msg), {:ok, socket}
end
defmodule MyModule do
use Sorcery.LiveHelpers
def handle_info({:sorcery, _} = msg, socket), do: handle_sorcery(msg, socket,
hook_before: [&SomeModule.my_func/2],
hook_after: [&SomeModule.another_func/2],
)
end
```
Note that every middleware function must return a tuple. Either {:ok, socket} or {:error, socket}
In this case, every time the LiveView receives a sorcery message, it will:
1. Call all the functions in hook_before, in order
2. Do some sorcery magic behind the scenes
3. Call all the functions in hook_after, in order
Often in step 2, it will send a message to the parent PortalServer. Then 3 is called before we get a response. "After" means after you SEND the message, not after you RECEIVE a response.
So this could take some advanced understanding of how Sorcery works, or a lot of fiddling and pattern matching on msg[:command]
"""
@type hook :: (Phoenix.LiveView.Socket, map() -> {:ok, Phoenix.LiveView.Socket} | {:error, Phoenix.LiveView.Socket})
@callback handle_sorcery({:sorcery, map()}, Phoenix.LiveView.Socket, list(hook())) :: {:noreply, Phoenix.LiveView.Socket}
@callback handle_sorcery({:sorcery, map()}, Phoenix.LiveView.Socket) :: {:noreply, Phoenix.LiveView.Socket}
@doc ~s"""
Creates a portal between the LiveView and the PortalServer, using the given arguments.
Takes the socket, returns it unchanged.
## Examples
iex> body = %{portal_server: Postgres, portal_name: :my_portal, query_module: MyQuery, query_args: %{player_id: 1}}
iex> socket = spawn_portal(socket, body)
"""
@type socket_type :: %{optional(any) => any, :__struct__ => Phoenix.LiveView.Socket, :assigns => map()} #%{__struct__: Phoenix.LiveView.Socket, assigns: map(), optional(any) => any}
@callback spawn_portal(socket :: socket_type, body :: %{
portal_server: module(),
portal_name: atom(),
query_module: module(),
query_args: map(),
}, opts :: []) :: socket_type
@doc ~s"""
Puts a :sorcery key in socket.assigns, with a bunch of stuff used behind the scenes by Sorcery.
This is mandatory, before you can spawn any portals
You must pass in the sorcery_module. If you used the generator, it'll just be Src.
## Examples
iex> body = %{sorcery_module: Src}
iex> socket = initialize_sorcery(socket, body)
"""
@callback initialize_sorcery(socket :: socket_type, body :: %{
optional(:sorcery_module) => module(),
optional(:args) => map(),
optional(:store_adapter) => module()
}) :: socket_type
@doc ~s"""
Get a list of entities at the given portal/lvar
You probably want to use this inside heex
```elixir
<% players = portal_view(@sorcery, :my_portal, "?all_players") %>
```
"""
@callback portal_view(sorcery_config :: map(), portal_name :: atom(), lvar :: binary()) :: list()
@doc ~s"""
Get a list of entities at the given lvar.
If this lvar only exists in ONE portal, you can leave out the portal_name, just know that it might be imperceptibly slower since the function needs to iterate over all portals until it finds one with that lvar..
```elixir
<% players = portal_view(@sorcery, :my_portal, "?all_players") %>
```
"""
#@doc """
#When a PortalServer needs to send a message to this LiveView, it goes through this.
#You can still use your own `handle_info` functions with different function heads. But `handle_info({:sorcery, _}, socket)` is reserved.
#"""
# }}}
defmacro __using__(_) do
quote do
@behaviour Sorcery.LiveHelpers
# {{{ spawn_portal/3
def spawn_portal(socket, body), do: spawn_portal(socket, body, [])
@impl true
def spawn_portal(socket, %{portal_server: parent, portal_name: name, query_module: mod, query_args: args} = body, opts) do
passes_checks = cond do
!function_exported?(__MODULE__, :connected?, 1) -> true
apply(__MODULE__, :connected?, [socket]) -> true
true -> false
end
args = Enum.into(opts, args)
if passes_checks do
msg = %{
command: :create_portal,
portal_name: name,
#query_module: Module.concat(socket.assigns.sorcery.config_module, mod),
query_module: mod,
child_pid: self(),
args: args,
}
send(Module.concat([socket.assigns.sorcery.config_module, "PortalServers", parent]), {:sorcery, msg})
end
sorcery = socket.assigns[:sorcery]
|> Map.update(:pending_portals, [name], &([name | &1]))
|> Map.put(:has_loaded?, false)
assign(socket, :sorcery, sorcery)
end
@impl true
def spawn_portal(_, body, _) do
expected = [:portal_server, :portal_name, :query_module, :query_args]
expected_str = Enum.reduce(expected, "", fn k, acc -> ":#{k} " <> acc end)
actual = Map.keys(body)
actual_str = Enum.reduce(actual, "", fn k, acc -> ":#{k} " <> acc end)
missing = MapSet.difference(MapSet.new(expected), MapSet.new(actual)) |> MapSet.to_list()
missing_str = Enum.reduce(missing, "", fn k, acc -> ":#{k} " <> acc end)
raise ~s"""
You called spawn_portal/2 in #{__MODULE__}, which is a good start, but you're missing some data.
The second argument of spawn_portal requires the following keys
#{expected_str}
But you passed in
#{actual_str}
Which is missing
#{missing_str}
"""
end
# }}}
# {{{ initialize_sorcery/2
@impl true
def initialize_sorcery(socket, body \\ %{}) do
mod = Map.get(body, :sorcery_module, Src)
assigns = Sorcery.PortalServer.add_portal_server_state(socket.assigns, %{
config_module: mod,
store_adapter: Map.get(body, :store_adapter, Sorcery.StoreAdapter.InMemory),
args: Map.get(body, :args, %{})
})
socket = assign(socket, :sorcery, assigns.sorcery)
socket
end
# }}}
def has_loaded?(%{has_loaded?: true}), do: true
def has_loaded?(_), do: false
# {{{ handle_sorcery({:sorcery, msg}, socket)
def handle_sorcery(msg, socket), do: handle_sorcery(msg, socket, [])
@impl true
def handle_sorcery({:sorcery, msg}, socket, opts) do
before_hooks = Keyword.get(opts, :hook_before, [])
after_hooks = Keyword.get(opts, :hook_after, [])
# hook_before
socket = Enum.reduce_while(before_hooks, socket, fn cb, socket ->
case cb.(socket, msg) do
{:ok, socket} -> {:cont, socket}
{:error, socket} -> {:halt, socket}
_ -> raise "Invalid middleware #{cb}, it must return {:ok, socket}"
end
end)
# run_command
socket = handle_sorcery(msg, socket)
# hook_after
socket = Enum.reduce_while(after_hooks, socket, fn cb, socket ->
case cb.(socket, msg) do
{:ok, socket} -> {:cont, socket}
{:error, socket} -> {:halt, socket}
end
end)
{:noreply, socket}
end
def handle_sorcery(%{command: :mutation_failed, args: %{error: error, handle_fail: cb}} = msg, socket, _opts) when is_function(cb) do
socket = cb.(error, socket)
inner_state = Sorcery.PortalServer.handle_info(msg, socket.assigns.sorcery)
assign(socket, :sorcery, inner_state)
end
def handle_sorcery(%{command: :mutation_failed, args: %{error: %{reason: reason} }} = msg, socket, _opts) do
socket = Phoenix.LiveView.put_flash(socket, :error, reason)
inner_state = Sorcery.PortalServer.handle_info(msg, socket.assigns.sorcery)
assign(socket, :sorcery, inner_state)
end
def handle_sorcery(%{command: :mutation_success, args: %{data: data, handle_success: cb}} = msg, socket, _opts) when is_function(cb) do
socket = cb.(data, socket)
inner_state = Sorcery.PortalServer.handle_info(msg, socket.assigns.sorcery)
assign(socket, :sorcery, inner_state)
end
def handle_sorcery(%{command: :portal_merge, portal: new_portal, args: %{handle_success: cb}} = msg, socket, _opts) when is_function(cb) do
inner_state = Sorcery.PortalServer.handle_info(msg, socket.assigns.sorcery)
socket = assign(socket, :sorcery, inner_state)
socket = cb.(new_portal, socket)
socket
end
def handle_sorcery(msg, socket, _opts) do
inner_state = Sorcery.PortalServer.handle_info(msg, socket.assigns.sorcery)
assign(socket, :sorcery, inner_state)
end
# }}}
# {{{ portal_view(sorcery, portal_name, lvar)
@impl true
def portal_view(sorcery, portal_name, lvar) do
Sorcery.PortalServer.Portal.get_in(sorcery, portal_name, lvar)
end
# }}}
def portal_one(sorcery, portal_name, lvar) do
case Sorcery.PortalServer.Portal.get_in(sorcery, portal_name, lvar) do
[hd | _] -> hd
_ -> nil
end
end
# {{{ optimistic_mutation(mutation, socket)
def optimistic_mutation(mutation, socket, opts \\ []) do
case Sorcery.Mutation.send_mutation(mutation, socket.assigns.sorcery, opts) do
{:ok, new_sorcery} ->
assign(socket, :sorcery, new_sorcery)
{:error, {:skip, kind, reason}} when is_binary(reason) ->
Phoenix.LiveView.put_flash(socket, kind, reason)
end
end
# }}}
end
end
end