111 lines
3.4 KiB
Elixir
111 lines
3.4 KiB
Elixir
defmodule Forum.WsHandler do
|
|
@moduledoc """
|
|
One instance of this module runs per connected WebSocket client.
|
|
Each instance is its own BEAM process — isolated state, isolated crashes.
|
|
|
|
Client messages arrive in `handle_in/2`. Reply with `{:push, frame, state}`,
|
|
do nothing with `{:ok, state}`, or close with `{:stop, reason, state}`.
|
|
"""
|
|
@behaviour WebSock
|
|
|
|
require Logger
|
|
|
|
@impl true
|
|
def init(opts) do
|
|
Registry.register(Forum.FormsSubscriberRegistry, :forms, nil)
|
|
Logger.info("ws connected: user_id=#{opts[:user_id]}")
|
|
|
|
state = %{
|
|
message_count: 0,
|
|
user_id: opts[:user_id],
|
|
email: opts[:email]
|
|
}
|
|
|
|
welcome = Jason.encode!(%{
|
|
type: "welcome to this session: ",
|
|
user_id: opts[:user_id]
|
|
})
|
|
|
|
schedule_heartbeat()
|
|
|
|
{:push, {:text, welcome}, state}
|
|
end
|
|
|
|
# Text frame from client.
|
|
# Supported commands (sent as JSON over the wire):
|
|
# {"type": "ping"} -> {"type": "pong"}
|
|
# {"type": "send", "text": "hi"} -> inserts row, returns the row
|
|
# {"type": "list"} -> returns latest 50 rows
|
|
# {"type": "get_doc"} -> returns the backend body HTML
|
|
# {"type": "reload_forms"} -> re-reads forms.html and reconciles runtimes
|
|
# {"type": "list_processes"} -> returns a snapshot of all live processes
|
|
# {"type": "list_logs"} -> returns recent request logs
|
|
# {"type": "list_vm_memory"} -> returns BEAM VM memory categories
|
|
# Anything else echoes back.
|
|
@impl true
|
|
def handle_in({raw, [opcode: :text]}, state) do
|
|
state = %{state | message_count: state.message_count + 1}
|
|
|
|
response =
|
|
case Jason.decode(raw) do
|
|
{:ok, %{"type" => "ping"}} ->
|
|
%{type: "pong"}
|
|
|
|
{:ok, %{"type" => "send", "text" => text}} when is_binary(text) and text != "" ->
|
|
row = Forum.DB.insert_message(text)
|
|
%{type: "inserted", row: row}
|
|
|
|
{:ok, %{"type" => "list"}} ->
|
|
%{type: "list", rows: Forum.DB.list_messages(limit: 50)}
|
|
|
|
{:ok, %{"type" => "get_doc"}} ->
|
|
%{type: "doc", html: Forum.Forms.html()}
|
|
|
|
{:ok, %{"type" => "reload_forms"}} ->
|
|
:ok = Forum.Forms.reload()
|
|
%{type: "doc", html: Forum.Forms.html()}
|
|
|
|
{:ok, %{"type" => "list_processes"}} ->
|
|
%{type: "processes", rows: Forum.Processes.list()}
|
|
|
|
{:ok, %{"type" => "list_modules"}} ->
|
|
%{type: "modules", rows: Forum.Modules.list()}
|
|
|
|
{:ok, %{"type" => "list_logs"}} ->
|
|
%{type: "logs", rows: Forum.LogStore.list()}
|
|
|
|
{:ok, %{"type" => "list_vm_memory"}} ->
|
|
%{type: "vm_memory", rows: Forum.VmMemory.list()}
|
|
|
|
_ ->
|
|
%{type: "echo", raw: raw, count: state.message_count}
|
|
end
|
|
|
|
{:push, {:text, Jason.encode!(response)}, state}
|
|
end
|
|
|
|
# Binary frame from client — ignore in this scaffold.
|
|
def handle_in({_data, [opcode: :binary]}, state), do: {:ok, state}
|
|
|
|
# Messages sent to this process's mailbox from other processes.
|
|
@impl true
|
|
def handle_info({:forms_reloaded, html}, state) do
|
|
{:push, {:text, Jason.encode!(%{type: "doc", html: html})}, state}
|
|
end
|
|
|
|
def handle_info(_msg, state), do: {:ok, state}
|
|
|
|
def handle_info(:heartbeat, state) do
|
|
schedule_heartbeat()
|
|
{:push, {:ping, ""}, state}
|
|
end
|
|
|
|
defp schedule_heartbeat, do: Process.send_after(self(), :heartbeat, 30_000)
|
|
|
|
@impl true
|
|
def terminate(reason, _state) do
|
|
Logger.info("ws disconnected: #{inspect(reason)}")
|
|
:ok
|
|
end
|
|
end
|