113 lines
3.2 KiB
Elixir
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
|