poex/lib/poex/pads/document_server.ex

113 lines
3.2 KiB
Elixir

defmodule Poex.Pads.DocumentServer do
use GenServer
alias Poex.Utils.DeltaUtils
@initial_state %{
# Number of changes made to the document so far
version: 0,
# An up-to-date Delta with all changes applied, representing
# the current state of the document
contents: [],
# The `inverted` versions of all changes performed on the
# document (useful for viewing history or undo the changes)
inverted_changes: []
}
# Public API
# ----------
def start_link(args), do: GenServer.start_link(__MODULE__, args, name: via_tuple(args.id))
def stop(pid), do: GenServer.stop(pid)
def update(id, %{"change" => change, "client_id" => client_id}),
do: GenServer.call(via_tuple(id), {:update, change, client_id})
def get_contents(id), do: GenServer.call(via_tuple(id), :get_contents)
def get_history(id), do: GenServer.call(via_tuple(id), :get_history)
def undo(id), do: GenServer.call(via_tuple(id), :undo)
# GenServer Callbacks
# -------------------
# Initialize the document with the default state
@impl true
def init(args) do
id = args.id
Registry.register(Poex.Pads.DocumentRegistry, id, [])
initial_state = Map.put(@initial_state, :id, id)
{:ok, initial_state}
end
# Apply a given change to the document, updating its contents
# and incrementing the version
#
# We also keep track of the inverted version of the change
# which is useful for performing undo or viewing history
@impl true
def handle_call({:update, change_map, client_id}, _from, state) do
change = DeltaUtils.convert_ops(change_map)
inverted = Delta.invert(change, state.contents)
state = %{
id: state.id,
version: state.version + 1,
contents: Delta.compose(state.contents, change),
inverted_changes: [inverted | state.inverted_changes]
}
PoexWeb.Endpoint.broadcast("pad:#{state.id}", "update", %{
change: change,
client_id: client_id
})
{:reply, state.contents, state}
end
# Fetch the current contents of the document
@impl true
def handle_call(:get_contents, _from, state) do
{:reply, state.contents, state}
end
# Revert the applied changes one by one to see how the
# document transformed over time
@impl true
def handle_call(:get_history, _from, state) do
current = {state.version, state.contents}
history =
Enum.scan(state.inverted_changes, current, fn inverted, {version, contents} ->
contents = Delta.compose(contents, inverted)
{version - 1, contents}
end)
{:reply, [current | history], state}
end
# Don't undo when document is already empty
@impl true
def handle_call(:undo, _from, %{version: 0} = state) do
{:reply, state.contents, state}
end
# Revert the last change, removing it from our stack and
# updating the contents
@impl true
def handle_call(:undo, _from, state) do
[last_change | changes] = state.inverted_changes
state = %{
id: state.id,
version: state.version - 1,
contents: Delta.compose(state.contents, last_change),
inverted_changes: changes
}
{:reply, state.contents, state}
end
defp via_tuple(id), do: {:via, Registry, {Poex.Pads.DocumentRegistry, id}}
end