Tiptapex.Collab.Channel behaviour (Tiptapex v0.1.0)

Copy Markdown View Source

A Phoenix Channel implementing the Tiptapex collaboration wire protocol.

defmodule MyAppWeb.DocChannel do
  use Tiptapex.Collab.Channel

  @impl true
  def authorize("doc:" <> slug, _params, socket) do
    case MyApp.Docs.get_by_slug(slug) do
      nil -> {:error, %{reason: "not_found"}}
      doc -> {:ok, Phoenix.Socket.assign(socket, :doc_id, doc.id)}
    end
  end
end

Mount it on your socket with a wildcard route matching the topics your editors use (channel "doc:*", MyAppWeb.DocChannel).

Wire protocol

Matches PhoenixCollabProvider in assets/js/tiptapex/collaboration.js:

  • "client_sync" (binary) — a Yjs sync message from one peer, rebroadcast to the others as "server_sync".
  • "client_awareness" (binary) — Yjs awareness (cursors, names), rebroadcast as "server_awareness".
  • "request_state" — a freshly-joined peer asks the room for the current state; rebroadcast so an existing peer answers with a sync message.

The server never interprets the CRDT payloads — it is a relay. Your regular "save" flow remains the source of truth for persistence.

Optional persistence hooks

load_state/2 and persist_update/3 (both default to no-ops) let you snapshot the opaque Yjs state: return {:ok, binary} from load_state/2 to seed late joiners, and store the binaries you receive in persist_update/3. Merging Yjs updates server-side would require a Yjs-compatible runtime, so the recommended strategy is peer-relay plus a snapshot written by your save action.

Summary

Callbacks

Authorizes a join. Return {:ok, socket}, {:ok, socket, reply} (the reply map is sent to the client), or {:error, reply}.

Loads a previously-persisted Yjs state binary to push to a joining peer as an initial "server_sync". Default: {:ok, nil} (no stored state).

Called with every binary sync update relayed through the channel. Default: :ok (no persistence).

Callbacks

authorize(topic, params, socket)

@callback authorize(topic :: String.t(), params :: map(), socket :: Phoenix.Socket.t()) ::
  {:ok, Phoenix.Socket.t()} | {:ok, Phoenix.Socket.t(), map()} | {:error, map()}

Authorizes a join. Return {:ok, socket}, {:ok, socket, reply} (the reply map is sent to the client), or {:error, reply}.

load_state(topic, socket)

(optional)
@callback load_state(topic :: String.t(), socket :: Phoenix.Socket.t()) ::
  {:ok, binary() | nil}

Loads a previously-persisted Yjs state binary to push to a joining peer as an initial "server_sync". Default: {:ok, nil} (no stored state).

persist_update(topic, update, socket)

(optional)
@callback persist_update(
  topic :: String.t(),
  update :: binary(),
  socket :: Phoenix.Socket.t()
) :: :ok

Called with every binary sync update relayed through the channel. Default: :ok (no persistence).