persist document to database after 2 seconds of no updates, initialize with current state when joining genserver

This commit is contained in:
Silas 2023-11-24 20:24:40 -05:00
parent 52a7a64d23
commit 1ec5459a79
Signed by: silentsilas
GPG Key ID: 4199EFB7DAA34349
5 changed files with 50 additions and 12 deletions

View File

@ -28,6 +28,11 @@ export let TextEditor = {
range && this.quill.setSelection(range.index, range.length); range && this.quill.setSelection(range.index, range.length);
}) })
channel.on("saved", () => {
console.log('Saved');
// TODO: Show a saved message
})
this.quill.on('text-change', (delta, oldDelta, source) => { this.quill.on('text-change', (delta, oldDelta, source) => {
if (delta == oldDelta) return; if (delta == oldDelta) return;
if (source == 'api') { if (source == 'api') {

View File

@ -9,7 +9,7 @@ defmodule Poex.Pads.Document do
@primary_key {:id, Ecto.UUID, autogenerate: true} @primary_key {:id, Ecto.UUID, autogenerate: true}
schema "pad_documents" do schema "pad_documents" do
field :title, :string field :title, :string
field :state, :map, default: %{ops: []} field :contents, {:array, :map}, default: []
has_many :operations, Poex.Pads.Operation has_many :operations, Poex.Pads.Operation
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
@ -18,7 +18,7 @@ defmodule Poex.Pads.Document do
@doc false @doc false
def changeset(document, attrs) do def changeset(document, attrs) do
document document
|> cast(attrs, [:title, :state]) |> cast(attrs, [:title, :contents])
|> cast_assoc(:operations) |> cast_assoc(:operations)
end end
end end

View File

@ -1,8 +1,11 @@
defmodule Poex.Pads.DocumentServer do defmodule Poex.Pads.DocumentServer do
use GenServer use GenServer
alias Poex.Pads.Document
alias Poex.Pads
alias Poex.Utils.DeltaUtils alias Poex.Utils.DeltaUtils
@initial_state %{ @initial_state %{
save_scheduled: false,
# Number of changes made to the document so far # Number of changes made to the document so far
version: 0, version: 0,
@ -36,7 +39,8 @@ defmodule Poex.Pads.DocumentServer do
def init(args) do def init(args) do
id = args.id id = args.id
Registry.register(Poex.Pads.DocumentRegistry, id, []) Registry.register(Poex.Pads.DocumentRegistry, id, [])
initial_state = Map.put(@initial_state, :id, id) %Document{contents: current_state} = Pads.get_pad_document(id)
initial_state = Map.put(@initial_state, :id, id) |> Map.put(:contents, current_state)
{:ok, initial_state} {:ok, initial_state}
end end
@ -50,19 +54,28 @@ defmodule Poex.Pads.DocumentServer do
change = DeltaUtils.convert_ops(change_map) change = DeltaUtils.convert_ops(change_map)
inverted = Delta.invert(change, state.contents) inverted = Delta.invert(change, state.contents)
state = %{ save_scheduled =
if state.save_scheduled do
true
else
Process.send_after(self(), :save, 2000)
true
end
new_state = %{
id: state.id, id: state.id,
save_scheduled: save_scheduled,
version: state.version + 1, version: state.version + 1,
contents: Delta.compose(state.contents, change), contents: Delta.compose(state.contents, change),
inverted_changes: [inverted | state.inverted_changes] inverted_changes: [inverted | state.inverted_changes]
} }
PoexWeb.Endpoint.broadcast("pad:#{state.id}", "update", %{ PoexWeb.Endpoint.broadcast("pad:#{new_state.id}", "update", %{
change: change, change: change,
client_id: client_id client_id: client_id
}) })
{:reply, state.contents, state} {:reply, new_state.contents, new_state}
end end
# Fetch the current contents of the document # Fetch the current contents of the document
@ -100,6 +113,7 @@ defmodule Poex.Pads.DocumentServer do
state = %{ state = %{
id: state.id, id: state.id,
save_scheduled: state.save_scheduled,
version: state.version - 1, version: state.version - 1,
contents: Delta.compose(state.contents, last_change), contents: Delta.compose(state.contents, last_change),
inverted_changes: changes inverted_changes: changes
@ -108,5 +122,13 @@ defmodule Poex.Pads.DocumentServer do
{:reply, state.contents, state} {:reply, state.contents, state}
end end
@impl true
def handle_info(:save, state) do
state = Map.put(state, :save_scheduled, false)
Pads.update_pad_document(state.id, %{contents: state.contents})
PoexWeb.Endpoint.broadcast("pad:#{state.id}", "saved", %{})
{:noreply, state}
end
defp via_tuple(id), do: {:via, Registry, {Poex.Pads.DocumentRegistry, id}} defp via_tuple(id), do: {:via, Registry, {Poex.Pads.DocumentRegistry, id}}
end end

View File

@ -1,6 +1,5 @@
defmodule PoexWeb.PadLive do defmodule PoexWeb.PadLive do
alias Poex.Pads alias Poex.Pads.{Document, DocumentDynamicSupervisor}
alias Poex.Pads.Document
alias Poex.Repo alias Poex.Repo
alias Poex.Utils alias Poex.Utils
@ -12,7 +11,7 @@ defmodule PoexWeb.PadLive do
Document.changeset(%Document{}, %{title: "Untitled"}) Document.changeset(%Document{}, %{title: "Untitled"})
|> Repo.insert() |> Repo.insert()
Poex.Pads.DocumentDynamicSupervisor.start_document_supervisor(new_document) DocumentDynamicSupervisor.start_document_supervisor(new_document)
# Redirect to the new document with its ID # Redirect to the new document with its ID
{:ok, push_navigate(socket, to: ~p"/pad/#{new_document.id}", replace: true)} {:ok, push_navigate(socket, to: ~p"/pad/#{new_document.id}", replace: true)}
@ -22,11 +21,13 @@ defmodule PoexWeb.PadLive do
%{ %{
id: id, id: id,
title: title, title: title,
state: state contents: contents
} = Repo.get!(Document, id) } = document = Repo.get!(Document, id)
DocumentDynamicSupervisor.start_document_supervisor(document)
# init editor and assigns with latest state from doc # init editor and assigns with latest state from doc
{:ok, assign(socket, id: id, title: title, state: state |> Utils.atomize_keys())} {:ok, assign(socket, id: id, title: title, contents: contents |> Utils.atomize_keys())}
end end
def handle_info(:new_document, socket) do def handle_info(:new_document, socket) do

View File

@ -0,0 +1,10 @@
defmodule Poex.Repo.Migrations.RemoveStateAndAddContentsToDocument do
use Ecto.Migration
def change do
alter table(:pad_documents) do
remove :state
add :contents, {:array, :map}, default: []
end
end
end