init
This commit is contained in:
110
lib/forum/ws_handler.ex
Normal file
110
lib/forum/ws_handler.ex
Normal file
@@ -0,0 +1,110 @@
|
||||
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
|
||||
Reference in New Issue
Block a user