From d11d0b0cf1f860bea0bc9df14145ff698312b474 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 28 Nov 2024 23:23:29 +0100 Subject: [PATCH] Make sync offline friendly --- lib/jenot/note.ex | 2 + lib/jenot/notes.ex | 6 +- .../20241117180924_initial_schema.exs | 1 + priv/static/js/jenot.js | 4 +- priv/static/js/synced-store.js | 69 +++++++++---------- 5 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/jenot/note.ex b/lib/jenot/note.ex index 9fe30b4..8b77831 100644 --- a/lib/jenot/note.ex +++ b/lib/jenot/note.ex @@ -13,6 +13,7 @@ defmodule Jenot.Note do belongs_to(:account, Jenot.Account, type: :binary_id) + field(:server_updated_at, :utc_datetime_usec) timestamps(type: :utc_datetime_usec) end @@ -28,6 +29,7 @@ defmodule Jenot.Note do :updated_at ]) |> put_change(:id, Ecto.UUID.generate()) + |> put_change(:server_updated_at, DateTime.utc_now()) |> validate_required([:internal_id, :type, :inserted_at, :updated_at]) |> put_assoc(:account, account) end diff --git a/lib/jenot/notes.ex b/lib/jenot/notes.ex index 7b5b5bf..15b7327 100644 --- a/lib/jenot/notes.ex +++ b/lib/jenot/notes.ex @@ -11,8 +11,7 @@ defmodule Jenot.Notes do def latest_change(account) do Note |> where(account_id: ^account.id) - |> where([n], is_nil(n.deleted_at)) - |> select([n], max(n.updated_at)) + |> select([n], max(n.server_updated_at)) |> Repo.one() end @@ -148,7 +147,7 @@ defmodule Jenot.Notes do |> String.to_integer() |> DateTime.from_unix!(:millisecond) - where(query, [n], n.updated_at > ^datetime) + where(query, [n], n.server_updated_at > ^datetime) end defp upsert(changeset, opts) do @@ -195,6 +194,7 @@ defmodule Jenot.Notes do ^deleted_at, n.deleted_at ), + server_updated_at: ^DateTime.utc_now(), updated_at: fragment( "CASE WHEN ? > ? THEN ? ELSE ? END", diff --git a/priv/repo/migrations/20241117180924_initial_schema.exs b/priv/repo/migrations/20241117180924_initial_schema.exs index e80cc2e..ea746d4 100644 --- a/priv/repo/migrations/20241117180924_initial_schema.exs +++ b/priv/repo/migrations/20241117180924_initial_schema.exs @@ -18,6 +18,7 @@ defmodule Jenot.Repo.Migrations.InitialSchema do add :account_id, references(:accounts, on_delete: :delete_all), null: false + add :server_updated_at, :datetime_usec, null: false timestamps(type: :datetime_usec) end diff --git a/priv/static/js/jenot.js b/priv/static/js/jenot.js index b5bed68..a6a301c 100644 --- a/priv/static/js/jenot.js +++ b/priv/static/js/jenot.js @@ -26,7 +26,6 @@ const sync = async () => { Notes.saveStorage(); }; -sync(); setInterval(sync, 5000); // Notifications API test - to be reused for push notifications later on @@ -57,8 +56,9 @@ const editNote = document.querySelector("#edit-note"); // of notes list. Notes.addEventListener("save", render.bind(this)); -// Initial notes render. +// Initial notes render and initial sync. render(); +sync(); // note-form component specific event handlers newNote.addEventListener("addNote", async (e) => { diff --git a/priv/static/js/synced-store.js b/priv/static/js/synced-store.js index e538a8b..0e04e3d 100644 --- a/priv/static/js/synced-store.js +++ b/priv/static/js/synced-store.js @@ -12,11 +12,19 @@ class WebNoteStore { params.append("deleted", "true"); } const suffix = params.size > 0 ? `?${params.toString()}` : ""; - return this.#request(`${this.endpoint}api/notes${suffix}`, {}, () => []); + return this.#request( + `${this.endpoint}api/notes${suffix}`, + {}, + () => "no_network", + ); } async get(id) { - return this.#request(`${this.endpoint}api/notes/${id}`, {}, () => null); + return this.#request( + `${this.endpoint}api/notes/${id}`, + {}, + () => "no_network", + ); } async add(note) { @@ -26,7 +34,7 @@ class WebNoteStore { method: "POST", body: JSON.stringify(note), }, - () => null, + () => "no_network", ); } @@ -37,7 +45,7 @@ class WebNoteStore { method: "PUT", body: JSON.stringify(note), }, - () => null, + () => "no_network", ); } @@ -174,7 +182,7 @@ export class SyncedNoteStore extends EventTarget { ), ) .then(() => { - (async () => (skipNetwork ? null : this.webStore?.update(note)))(); + (async () => (skipNetwork ? null : this.webStore?.add(note)))(); return null; }); } @@ -183,19 +191,33 @@ export class SyncedNoteStore extends EventTarget { const that = this; const meta = await this.getMeta(); const lastSync = meta?.lastSync; + const currentSync = Date.now(); this.all(lastSync, true) .then((notes) => { - notes.forEach(async (n) => await that.webStore.add(n)); + return Promise.all(notes.map((n) => that.webStore.add(n))); + }) + .then((results) => { + if (results.indexOf("no_network") < 0) { + return that.webStore.all(lastSync, true); + } else { + return "no_network"; + } }) - .then(() => that.webStore.all(lastSync, true)) .then((notes) => { - notes.forEach(async (n) => await that.update(n, true)); + if (notes !== "no_network") { + notes.forEach(async (n) => await that.update(n, true)); + return null; + } else { + return "no_network"; + } }) - .then(() => { - meta.lastSync = Date.now(); + .then((result) => { + if (result !== "no_network") { + meta.lastSync = currentSync; - that.setMeta(meta); + that.setMeta(meta); + } }); } @@ -233,29 +255,4 @@ export class SyncedNoteStore extends EventTarget { }; }); } - - #request(url, opts, emptyValue) { - new Promise((resolve) => { - return resolve(this.#runRequest(url, opts, () => emptyValue)); - }); - } - - async #runRequest(url, opts, errorCallback) { - opts.headers = { - ...(opts.headers || {}), - "Content-Type": "application/json", - }; - - try { - const response = await fetch(url, opts); - if (!response.ok) { - console.error("Request failed", response); - return errorCallback(response); - } - return response.json(); - } catch (error) { - console.error("Request error", error); - return errorCallback(error); - } - } }