# `Slap.Yjs.DocServer`
[🔗](https://github.com/mjrusso/slap/blob/slap_yjs-v0.3.0/slap_yjs/lib/slap/yjs/doc_server.ex#L1)

A `Yex.DocServer` whose document is stored with `Slap.Yjs.Store`, and shared
with the servers of the same document on other nodes through it.

    defmodule MyApp.DocServer do
      use Slap.Yjs.DocServer
    end

    {:ok, pid} = Slap.Yjs.Docs.join(MyApp.DocServer, {"my-service", "doc-1"})

Several servers may run for one document, one per node with clients
(`Slap.Yjs.Docs` starts them). None owns the document: each appends its own
clients' updates to the document's `.updates` stream and follows that
stream, applying everything in it, so each converges on
the stream's contents. Yjs updates commute and applying one twice changes
nothing, so the order of the appends and the server's own updates coming
back do not matter.

## Storing

Before serving requests the server loads the document (the current
snapshot, then the updates after it) and follows the stream from there.
Every update from a client or a local edit is framed into a buffer. The
buffer is appended after `:flush_after` milliseconds or once it holds
`:flush_bytes`, whichever comes first. One append runs at a time: what is
buffered meanwhile goes in the next one, once it is acknowledged. An
append that fails for a transient reason (`:unavailable`, `:timeout`,
`:overloaded`, as when the document's shard moves) is retried; any other
failure stops the server with `{:slap_yjs_append_failed, reason}`.

The server relays a client's update to its other subscribers before
storing it. If the server stops first, the update is lost unless a client
that has it resynchronises. Servers on other nodes receive only stored
updates.

A server started through `Slap.Yjs.Docs` begins loading after `init/2`
returns. Overrides of `init/2` cannot assume stored updates are present.
`Slap.Yjs.Docs.join/3` and `subscribe/3` wait for loading to finish.

## Compaction

Once the updates read since the last snapshot reach
`max(:compact_bytes, last snapshot size / 2)`, the server compacts the
document (`Slap.SnapshotLog.snapshot/4`) in a task, one compaction at a
time, and keeps serving meanwhile. The snapshot is the document's state
at the offset it has read up to: every update before that offset has been
applied. It may hold more (updates pending, and local ones not appended
yet), which `Slap.SnapshotLog` allows because applying a Yjs update twice
changes nothing. If another server indexed a newer snapshot meanwhile,
the compaction is superseded, which is not an error. A compaction that
fails is tried again once the server reads more.

## Clients

A client process subscribes with `Slap.Yjs.Docs.join/3` (or `subscribe/3`),
sends its messages with `process_message_v1(server, message, self())`,
and receives:

  * `{:slap_yjs_update, doc_id, update}` - a document update (v1) from another
    client, on any node;
  * `{:slap_yjs_awareness, doc_id, update}` - an awareness (presence) update.

`encode_message/1` encodes these messages for the client.

The server monitors its subscribers. When one goes down, the awareness
states it set are removed. After `:idle_timeout` with no subscribers the
server stops.

## Awareness

Awareness is not stored. The servers of a document form a `:pg` group
(scope `Slap.Yjs.PG`, started by `Slap.Yjs.Docs`) and send each other their
clients' awareness updates. Each monitors the group (`:pg.monitor/2`,
which needs OTP 26.2 or later): when a server joins, each of the others
sends it the states its own clients set (not those it learned from other
servers); when one leaves (it stops, or its node goes down), the others
remove the states that came through it.

## Deletion

When the document is deleted (`Slap.Yjs.Docs.delete/2`), each of its
servers stops with `{:shutdown, :deleted}`, told by the delete or once
its follower or an append finds out, and stores nothing more: the
deletion is permanent, and what it had buffered is dropped. Starting a
deleted document directly returns `{:error, {:shutdown, :deleted}}`;
`Slap.Yjs.Docs.join/3` returns `{:error, :deleted}`.

## Stopping

Unless an append failed or the document was deleted, the server appends
what it has buffered on stop, waits up to `:terminate_timeout` for its
appends and a running compaction, then compacts if it has read anything
since the last snapshot (`:compact_on_stop`). It traps exits so that a
supervisor's shutdown reaches `terminate/2`. By default, its
`child_spec/1` and `Slap.Yjs.Docs` allow `:terminate_timeout` plus 30 seconds
per server.
`Docs` stops its document servers concurrently, so its shutdown takes at
most the largest child shutdown timeout, plus supervisor overhead. With
defaults, that is about 35 seconds. A server still stopping at its limit
is killed; a partial snapshot publication is safe to retry.

## Options

Besides `Yex.DocServer`'s options, such as `:assigns`:

  * `:doc_id` (required) - the `Slap.Yjs.Store` document, `{service, name}`.
  * `:flush_after` - milliseconds to buffer an update (default 10).
  * `:flush_bytes` - buffered bytes that start an append (default 256 KiB).
  * `:compact_bytes` - the least bytes read that start a compaction
    (default 1 MiB).
  * `:compact_on_stop` - compact on stop (default true).
  * `:compaction` - options for `Slap.SnapshotLog.snapshot/4`, including
    `:history`, `:cluster`, and a per-call `:timeout` (default `[]`).
  * `:idle_timeout` - milliseconds without subscribers before stopping
    (default `:infinity`; `Slap.Yjs.Docs` starts servers with 30 s).
  * `:terminate_timeout` - milliseconds to wait for appends and a running
    compaction on stop (default 5000).
  * `:store` - `Slap.Yjs.Store` options, including `:prefix` and `:cluster`.

A module that overrides `init/2`, `handle_update_v1/4`,
`handle_awareness_update/4`, `handle_info/2` or `terminate/2` must call
`super` (for `handle_info/2`, with the messages it does not handle
itself).

## Telemetry

  * `[:slap, :yjs, :doc_server, :append]` when an append starts, with
    measurements `%{bytes: b, updates: n}`.
  * `[:slap, :yjs, :doc_server, :compact]` when a compaction finishes, with
    measurements `%{bytes: snapshot_size}` and `:offset` in the metadata.

The metadata of both includes `:doc_id`.

# `doc`

```elixir
@spec doc(GenServer.server(), timeout()) :: {:ok, Yex.Doc.t()} | {:error, term()}
```

Returns the server's `Yex.Doc`. y_ex functions called on it run in the
server. Edits are relayed and stored like a client's updates. The handle
is valid only while the server runs.

# `encode_message`

```elixir
@spec encode_message(
  {:slap_yjs_update | :slap_yjs_awareness, Slap.Yjs.Store.doc(), binary()}
) ::
  binary()
```

Encodes a `{:slap_yjs_update, _, _}` or `{:slap_yjs_awareness, _, _}`
message as the y-protocols v1 message to send to a client.

# `handle_awareness_update`

```elixir
@spec handle_awareness_update(
  Yex.Awareness.t(),
  map(),
  term(),
  Yex.DocServer.State.t()
) ::
  {:noreply, Yex.DocServer.State.t()}
```

Broadcasts an awareness change to the subscribers and, unless it came
from another server, to the document's other servers. Called by the
default `handle_awareness_update/4`.

# `handle_info`

```elixir
@spec handle_info(term(), Yex.DocServer.State.t()) ::
  {:noreply, Yex.DocServer.State.t()} | {:stop, term(), Yex.DocServer.State.t()}
```

Handles the server's own messages: requests, timers, what the follower
reads, awareness from other servers, and exits, among them its tasks'.
Called by the default `handle_info/2`; other messages are ignored.

# `handle_update_v1`

```elixir
@spec handle_update_v1(Yex.Doc.t(), binary(), term(), Yex.DocServer.State.t()) ::
  {:noreply, Yex.DocServer.State.t()}
```

Broadcasts an update to the subscribers, and buffers it for appending
unless it was read from the store. Called by the default
`handle_update_v1/4`.

# `init`

```elixir
@spec init(keyword(), Yex.DocServer.State.t()) ::
  {:ok, Yex.DocServer.State.t()} | {:stop, term()}
```

Starts loading the document. Called by the default `init/2`.

# `subscribe`

```elixir
@spec subscribe(GenServer.server(), pid(), timeout()) :: :ok | {:error, term()}
```

Subscribes `pid` to updates after loading finishes. `timeout` bounds both waits.

# `sync`

```elixir
@spec sync(GenServer.server(), timeout()) :: :ok | {:error, term()}
```

Waits until the server has appended everything it had buffered, has read
the document's stream up to its tail as of the call (so it has applied
what other servers had stored), and has no compaction running.

# `terminate`

```elixir
@spec terminate(term(), Yex.DocServer.State.t()) :: :ok
```

Stops the follower; then, unless an append failed or the document was
deleted, appends what is buffered, waits for the appends and a running
compaction, and compacts if anything was read since the last snapshot.
Called by the default `terminate/2`.

# `unsubscribe`

```elixir
@spec unsubscribe(GenServer.server(), pid(), timeout()) :: :ok | {:error, term()}
```

Unsubscribes `pid`, and removes the awareness states it set.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
