Current section
Files
Jump to
Current section
Files
lib/server/doc_server.ex
defmodule Yex.DocServer do
@moduledoc """
Define a `Yex.DocServer` process.
`DocServer` defines a module to handle document update messages by yjs.
## Examples
defmodule MyDocServer do
use Yex.DocServer
def init(_arg, state) do
{:ok, state}
end
def handle_update_v1(doc, update, origin, state) do
# Handle document updates from Yjs
# - doc: The current document state
# - update: Binary encoded update
# - origin: Source of the update
{:noreply, state}
end
def handle_awareness_update(awareness, %{removed: removed, added: added, updated: updated}, origin, state) do
# Handle presence/awareness update
# - awareness: Current awareness state
# - removed/added/updated: Lists of changed client IDs
{:noreply, state}
end
end
"""
alias Yex.DocServer.State
@type option :: {:doc_option, Yex.Doc.Options.t()} | {:assigns, map()} | {term(), term()}
@type options :: [option()]
@doc """
Starts a `DocServer` process linked to the current process.
"""
@callback start_link(
arg :: options(),
genserver_option :: GenServer.options()
) :: {:ok, pid()} | {:error, term()}
@doc """
Starts a `DocServer` process without linking (for use outside of a supervision tree).
Returns the same success and error responses as `start_link/2`.
"""
@callback start(
arg :: options(),
genserver_option :: GenServer.options()
) :: {:ok, pid()} | {:error, term()}
@doc """
Interprets and processes v1 encode message.
"""
@callback process_message_v1(
server :: GenServer.server(),
message :: binary(),
origin :: term()
) :: :ok | {:ok, replies :: list(binary())} | {:error, term()}
@doc """
Handle document updates in v1 encoding format.
## Parameters
- doc: Current document state
- update: Binary encoded update from Yjs
- origin: Source of the update (can be nil for local updates)
- state: Current server state
## Returns
- `{:noreply, state}` to continue with new state
- `{:stop, reason, state}` to stop the server
"""
@callback handle_update_v1(
doc :: Yex.Doc.t(),
update :: binary(),
origin :: term(),
state :: State.t()
) ::
{:noreply, State.t()}
| {:stop, reason :: term, State.t()}
@doc """
Handle awareness change
"""
@callback handle_awareness_change(
awareness :: Yex.Awareness.t(),
update :: %{removed: list(), added: list(), updated: list()},
origin :: term(),
state :: State.t()
) ::
{:noreply, State.t()}
| {:stop, reason :: term, State.t()}
@doc """
Handle awareness update
"""
@callback handle_awareness_update(
awareness :: Yex.Awareness.t(),
update :: %{removed: list(), added: list(), updated: list()},
origin :: term(),
state :: State.t()
) ::
{:noreply, State.t()}
| {:stop, reason :: term, State.t()}
@doc """
Initialize the doc process.
"""
@callback init(arg :: term, state :: State.t()) ::
{:ok, State.t()}
| {:stop, reason :: term}
@doc """
Handle regular Elixir process messages.
See `c:GenServer.handle_info/2`.
"""
@callback handle_info(msg :: term, state :: State.t()) ::
{:noreply, State.t()}
| {:stop, reason :: term, State.t()}
@doc """
Handle regular GenServer call messages.
See `c:GenServer.handle_call/3`.
"""
@callback handle_call(msg :: term, from :: {pid, tag :: term}, state :: State.t()) ::
{:reply, response :: term, State.t()}
| {:noreply, State.t()}
| {:stop, reason :: term, State.t()}
@doc """
Handle regular GenServer cast messages.
See `c:GenServer.handle_cast/2`.
"""
@callback handle_cast(msg :: term, state :: State.t()) ::
{:noreply, State.t()}
| {:stop, reason :: term, State.t()}
@doc """
Invoked when the document server process is about to exit.
See `c:GenServer.terminate/2`.
"""
@callback terminate(
reason :: :normal | :shutdown | {:shutdown, :left | :closed | term},
state :: State.t()
) ::
term
@optional_callbacks init: 2,
handle_info: 2,
handle_call: 3,
handle_cast: 2,
terminate: 2,
handle_update_v1: 4,
handle_awareness_change: 4,
handle_awareness_update: 4
defmacro __using__(opts \\ []) do
quote do
opts = unquote(opts)
@behaviour unquote(__MODULE__)
import unquote(__MODULE__)
import Yex.DocServer.State, only: [assign: 3, assign: 2]
def child_spec(arg) do
default = %{
id: __MODULE__,
start: {__MODULE__, :start_link, [arg]}
}
Supervisor.child_spec(default, unquote(Macro.escape(opts)))
end
def start_link(arg, opt \\ []) do
GenServer.start_link(Yex.DocServer.Worker, [{:module, __MODULE__} | arg], opt)
end
def start(arg, opt \\ []) do
GenServer.start(Yex.DocServer.Worker, [{:module, __MODULE__} | arg], opt)
end
def process_message_v1(server, message, origin \\ nil) do
Yex.DocServer.Worker.process_message_v1(server, message, origin)
end
defoverridable child_spec: 1
end
end
end
defmodule Yex.DocServer.State do
@moduledoc """
Provides the `State` struct and functions to manage the internal state of `Yex.DocServer`.
This module allows tracking the state of `Yex.DocServer` through a structured `State` record.
It includes functions for assigning and updating state attributes with custom key-value pairs.
"""
defstruct doc: nil, assigns: %{}, module: nil, awareness: nil
@type t :: %__MODULE__{
assigns: map,
doc: Yex.Doc.t(),
awareness: Yex.Awareness.t() | nil,
module: module()
}
@doc """
Assigns a single key-value pair to the `assigns` map in the state.
This function allows for adding or updating a specific attribute in the
state, useful for tracking individual properties in `Yex.DocServer`.
"""
@spec assign(t(), term(), term()) :: t()
def assign(state, key, value) do
assign(state, [{key, value}])
end
@doc """
Assigns multiple key-value pairs to the `assigns` map in the state.
This function accepts a list or map of attributes to update the state in
bulk, allowing for more comprehensive state updates at once.
"""
@spec assign(t(), [{term(), term()}] | map()) :: t()
def assign(state, attrs)
when is_map(attrs) or is_list(attrs) do
%{state | assigns: Map.merge(state.assigns, Map.new(attrs))}
end
end