GenServer managing a single collaborative document via Operational Transform.
Each document gets its own CollabServer process that maintains the
authoritative document state, version counter, and operation history.
Clients submit operations along with their last-known version; the server
transforms against any intervening operations before applying.
Architecture
Client A ──apply_op──> CollabServer ──broadcast──> PubSub ──> Client B
Client B ──apply_op──> CollabServer ──broadcast──> PubSub ──> Client AState
:doc_id— unique document identifier:content— current document text (string):version— monotonically increasing integer (starts at 0):history— list of{version, delta, client_id}tuples (newest first):clients—MapSetof connected client identifiers
Usage
# Start a server for a document
{:ok, pid} = PhiaUi.Editor.CollabServer.start_link(doc_id: "doc-123")
# Client submits an operation
{:ok, transformed_op, new_version} =
PhiaUi.Editor.CollabServer.apply_op(pid, [%{retain: 5}, %{insert: "!"}], 0)
# Read current state
{"Hello!", 1} = PhiaUi.Editor.CollabServer.get_document(pid)PubSub Broadcasting
After each successful operation, the server broadcasts on
"collab:doc:<doc_id>" via Phoenix.PubSub (configured in
:phia_ui, :collab_pubsub). The broadcast payload is:
%{
event: :op_applied,
doc_id: doc_id,
op: transformed_delta,
version: new_version,
client_id: client_id
}History Pruning
History is capped at 1000 entries. Older entries are discarded to bound memory usage. Clients that fall behind by more than 1000 versions must re-fetch the full document.
Summary
Functions
Submit a delta operation from a client.
Returns a specification to start this module under a supervisor.
Get the set of connected client identifiers.
Get the current document content and version.
Get operations from the history since a given version.
Get the current document version.
Register a client as connected to this document.
Unregister a client from this document.
Start a CollabServer process for a document.
Types
@type t() :: %PhiaUi.Editor.CollabServer{ clients: MapSet.t(), content: String.t(), doc_id: String.t(), history: [{non_neg_integer(), list(), String.t() | nil}], version: non_neg_integer() }
Functions
@spec apply_op(GenServer.server(), list(), non_neg_integer(), String.t() | nil) :: {:ok, list(), non_neg_integer()} | {:error, term()}
Submit a delta operation from a client.
The client_version is the version the client's delta was composed against.
The server transforms the delta against all operations that occurred between
client_version and the current server version, then applies the result.
Returns {:ok, transformed_delta, new_version} on success, or
{:error, reason} if the operation cannot be applied.
Parameters
server— pid or registered namedelta— list of OT operationsclient_version— the version the client is based onclient_id— optional identifier for the submitting client
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec get_clients(GenServer.server()) :: MapSet.t()
Get the set of connected client identifiers.
@spec get_document(GenServer.server()) :: {String.t(), non_neg_integer()}
Get the current document content and version.
Returns {content, version}.
@spec get_ops_since(GenServer.server(), non_neg_integer()) :: {:ok, [{non_neg_integer(), list(), String.t() | nil}]} | {:error, :version_too_old}
Get operations from the history since a given version.
Returns a list of {version, delta, client_id} tuples in ascending
version order. If the requested version is older than the oldest entry
in history, returns {:error, :version_too_old}.
@spec get_version(GenServer.server()) :: non_neg_integer()
Get the current document version.
@spec join(GenServer.server(), String.t()) :: :ok
Register a client as connected to this document.
Used for tracking active collaborators. Purely informational.
@spec leave(GenServer.server(), String.t()) :: :ok
Unregister a client from this document.
@spec start_link(keyword()) :: GenServer.on_start()
Start a CollabServer process for a document.
Options
:doc_id(required) — unique document identifier:initial_content— starting document text (default""):name— process name (defaults tovia_tuple(doc_id)using thePhiaUi.Collab.RoomRegistryif available, otherwise the pid)