From 001d7f8296055be0ba8bb5a9d4a756796877ab04 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Sun, 24 Nov 2024 12:06:39 +0100 Subject: [PATCH] Implement very rudimentary sync capability --- lib/jenot/account.ex | 2 +- lib/jenot/note.ex | 18 +- lib/jenot/notes.ex | 116 +++++++- lib/jenot/reminder.ex | 3 +- lib/jenot/subscription.ex | 2 +- lib/jenot/web.ex | 4 +- .../20241117180924_initial_schema.exs | 3 + priv/static/js/caching-worker.js | 42 --- priv/static/js/db-store.js | 143 ---------- priv/static/js/jenot.js | 20 +- priv/static/js/{store.js => local-store.js} | 8 +- priv/static/js/service-worker-init.js | 24 ++ priv/static/js/service-worker.js | 61 ++-- priv/static/js/synced-store.js | 261 ++++++++++++++++++ priv/static/js/web-store.js | 86 ------ 15 files changed, 473 insertions(+), 320 deletions(-) delete mode 100644 priv/static/js/caching-worker.js delete mode 100644 priv/static/js/db-store.js rename priv/static/js/{store.js => local-store.js} (94%) create mode 100644 priv/static/js/service-worker-init.js create mode 100644 priv/static/js/synced-store.js delete mode 100644 priv/static/js/web-store.js diff --git a/lib/jenot/account.ex b/lib/jenot/account.ex index 57e2ce7..9a409e1 100644 --- a/lib/jenot/account.ex +++ b/lib/jenot/account.ex @@ -5,7 +5,7 @@ defmodule Jenot.Account do @primary_key {:id, :binary_id, autogenerate: true} schema "accounts" do - timestamps(type: :utc_datetime) + timestamps(type: :utc_datetime_usec) end def new() do diff --git a/lib/jenot/note.ex b/lib/jenot/note.ex index c382ea9..9fe30b4 100644 --- a/lib/jenot/note.ex +++ b/lib/jenot/note.ex @@ -9,22 +9,32 @@ defmodule Jenot.Note do field(:type, Ecto.Enum, values: [:note, :tasklist], default: :note) field(:title, :string, default: "") field(:content, :string) + field(:deleted_at, :utc_datetime_usec) belongs_to(:account, Jenot.Account, type: :binary_id) - timestamps(type: :utc_datetime) + timestamps(type: :utc_datetime_usec) end def new(account, params) do %__MODULE__{} - |> cast(params, [:internal_id, :type, :title, :content]) + |> cast(params, [ + :internal_id, + :type, + :title, + :content, + :deleted_at, + :inserted_at, + :updated_at + ]) |> put_change(:id, Ecto.UUID.generate()) - |> validate_required([:internal_id, :type]) + |> validate_required([:internal_id, :type, :inserted_at, :updated_at]) |> put_assoc(:account, account) end def update(note, params) do note - |> cast(params, [:type, :title, :content]) + |> cast(params, [:type, :title, :content, :deleted_at, :updated_at]) + |> validate_required([:internal_id, :type, :updated_at]) end end diff --git a/lib/jenot/notes.ex b/lib/jenot/notes.ex index 71a6afe..7b5b5bf 100644 --- a/lib/jenot/notes.ex +++ b/lib/jenot/notes.ex @@ -11,13 +11,15 @@ defmodule Jenot.Notes do def latest_change(account) do Note |> where(account_id: ^account.id) - |> select([a], max(a.updated_at)) + |> where([n], is_nil(n.deleted_at)) + |> select([n], max(n.updated_at)) |> Repo.one() end - def all(account, since \\ nil) do + def all(account, since \\ nil, include_deleted? \\ false) do Note |> where(account_id: ^account.id) + |> maybe_include_deleted(include_deleted?) |> maybe_filter_since(since) |> Repo.all() end @@ -33,7 +35,11 @@ defmodule Jenot.Notes do end def note_by_internal_id(account, internal_id) do - note = Repo.get_by(Note, account_id: account.id, internal_id: internal_id) + note = + Note + |> where(account_id: ^account.id) + |> where(internal_id: ^internal_id) + |> Repo.one() if note do {:ok, note} @@ -58,7 +64,7 @@ defmodule Jenot.Notes do def delete(account, internal_id) do Note |> where(account_id: ^account.id, internal_id: ^internal_id) - |> Repo.delete_all() + |> Repo.update_all(set: [deleted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()]) :ok end @@ -79,14 +85,20 @@ defmodule Jenot.Notes do |> Map.delete("id") |> Map.delete(:id) |> Map.put("internal_id", internal_id) + |> deserialize_timestamp(:deleted, :deleted_at) + |> deserialize_timestamp(:created, :inserted_at) + |> deserialize_timestamp(:updated, :updated_at) end end defp serialize_note(note) do note = note - |> Map.take([:title, :type, :content, :inserted_at, :updated_at]) + |> Map.take([:title, :type, :content, :inserted_at, :updated_at, :deleted_at]) |> Map.put(:id, note.internal_id) + |> serialize_timestamp(:deleted_at, :deleted) + |> serialize_timestamp(:inserted_at, :created) + |> serialize_timestamp(:updated_at, :updated) if note.type == :tasklist and not is_nil(note.content) do %{note | content: Jason.decode!(note.content)} @@ -95,9 +107,47 @@ defmodule Jenot.Notes do end end + defp deserialize_timestamp(params, src_key, dst_key) do + str_src_key = to_string(src_key) + + value = + case Map.get(params, str_src_key, Map.get(params, src_key)) do + nil -> nil + unix_time -> DateTime.from_unix!(unix_time, :millisecond) + end + + params + |> Map.delete(src_key) + |> Map.delete(str_src_key) + |> Map.put(to_string(dst_key), value) + end + + defp serialize_timestamp(data, src_key, dst_key) do + value = + case data[src_key] do + nil -> nil + dt -> DateTime.to_unix(dt, :millisecond) + end + + data + |> Map.delete(src_key) + |> Map.put(dst_key, value) + end + + defp maybe_include_deleted(q, true), do: q + + defp maybe_include_deleted(q, false) do + where(q, [n], is_nil(n.deleted_at)) + end + defp maybe_filter_since(query, nil), do: query - defp maybe_filter_since(query, datetime) do + defp maybe_filter_since(query, unix_time) do + datetime = + unix_time + |> String.to_integer() + |> DateTime.from_unix!(:millisecond) + where(query, [n], n.updated_at > ^datetime) end @@ -106,11 +156,59 @@ defmodule Jenot.Notes do type = Ecto.Changeset.get_field(changeset, :type) || :note title = Ecto.Changeset.get_field(changeset, :title) || "" content = Ecto.Changeset.get_field(changeset, :content) || "" + deleted_at = Ecto.Changeset.get_field(changeset, :deleted_at) + updated_at = Ecto.Changeset.get_field(changeset, :updated_at) + + conflict_query = + from(n in Note, + update: [ + set: [ + type: + fragment( + "CASE WHEN ? > ? THEN ? ELSE ? END", + ^updated_at, + n.updated_at, + ^type, + n.type + ), + title: + fragment( + "CASE WHEN ? > ? THEN ? ELSE ? END", + ^updated_at, + n.updated_at, + ^title, + n.title + ), + content: + fragment( + "CASE WHEN ? > ? THEN ? ELSE ? END", + ^updated_at, + n.updated_at, + ^content, + n.content + ), + deleted_at: + fragment( + "CASE WHEN ? > ? THEN ? ELSE ? END", + ^updated_at, + n.updated_at, + ^deleted_at, + n.deleted_at + ), + updated_at: + fragment( + "CASE WHEN ? > ? THEN ? ELSE ? END", + ^updated_at, + n.updated_at, + ^updated_at, + n.updated_at + ) + ] + ] + ) Repo.insert(changeset, - on_conflict: [ - set: [type: type, title: title, content: content, updated_at: DateTime.utc_now()] - ], + on_conflict: conflict_query, conflict_target: conflict_target, returning: true ) diff --git a/lib/jenot/reminder.ex b/lib/jenot/reminder.ex index d1e6d1d..00dfb15 100644 --- a/lib/jenot/reminder.ex +++ b/lib/jenot/reminder.ex @@ -8,9 +8,10 @@ defmodule Jenot.Reminder do field(:day_of_week, :integer) field(:repeat_period, Ecto.Enum, values: [:day, :week, :month, :year]) field(:repeat_count, :integer) + field(:deleted_at, :utc_datetime_usec) belongs_to(:note, Jenot.Note) - timestamps(type: :utc_datetime) + timestamps(type: :utc_datetime_usec) end end diff --git a/lib/jenot/subscription.ex b/lib/jenot/subscription.ex index 270a599..0aed63f 100644 --- a/lib/jenot/subscription.ex +++ b/lib/jenot/subscription.ex @@ -11,6 +11,6 @@ defmodule Jenot.Subscription do belongs_to(:account, Jenot.Account) - timestamps(type: :utc_datetime) + timestamps(type: :utc_datetime_usec) end end diff --git a/lib/jenot/web.ex b/lib/jenot/web.ex index c0aa724..34e9e32 100644 --- a/lib/jenot/web.ex +++ b/lib/jenot/web.ex @@ -57,7 +57,7 @@ defmodule Jenot.Web do notes = account - |> Notes.all(conn.params["since"]) + |> Notes.all(conn.params["since"], conn.params["deleted"] == "true") |> Enum.map(&Notes.serialize/1) send_resp(conn, 200, Jason.encode!(notes)) @@ -108,7 +108,7 @@ defmodule Jenot.Web do :ok = Notes.delete(account, internal_id) - send_resp(conn, 204, "") + send_resp(conn, 200, Jason.encode!(%{deleted: true})) end get "/api/push/public-key" do diff --git a/priv/repo/migrations/20241117180924_initial_schema.exs b/priv/repo/migrations/20241117180924_initial_schema.exs index c826eff..e80cc2e 100644 --- a/priv/repo/migrations/20241117180924_initial_schema.exs +++ b/priv/repo/migrations/20241117180924_initial_schema.exs @@ -14,6 +14,7 @@ defmodule Jenot.Repo.Migrations.InitialSchema do add :type, :text, null: false add :title, :text, null: false, default: "" add :content, :text, null: false, default: "" + add :deleted_at, :datetime_usec add :account_id, references(:accounts, on_delete: :delete_all), null: false @@ -49,6 +50,8 @@ defmodule Jenot.Repo.Migrations.InitialSchema do add :repeat_period, :text add :repeat_count, :integer + add :deleted_at, :datetime_usec + add :note_id, references(:note, on_delete: :delete_all), null: false timestamps(type: :datetime_usec) diff --git a/priv/static/js/caching-worker.js b/priv/static/js/caching-worker.js deleted file mode 100644 index 89486c3..0000000 --- a/priv/static/js/caching-worker.js +++ /dev/null @@ -1,42 +0,0 @@ -const putInCache = async (request, response) => { - const cache = await caches.open("v1"); - await cache.put(request, response); -}; - -const cacheFirst = async ({ request, fallbackUrl }) => { - const responseFromCache = await caches.match(request); - if (responseFromCache) { - return responseFromCache; - } - - try { - const responseFromNetwork = await fetch(request); - // Cloning is needed because a response can only be consumed once. - putInCache(request, responseFromNetwork.clone()); - return responseFromNetwork; - } catch (error) { - const fallbackResponse = await caches.match(fallbackUrl); - if (fallbackResponse) { - return fallbackResponse; - } - - return new Response("Network error happened", { - status: 408, - headers: { "Content-Type": "text/plain" }, - }); - } -}; - -self.addEventListener("fetch", (event) => { - const requestUrl = URL.parse(event.request.url); - - // We don't cache API requests - if (!requestUrl.pathname.startsWith("/api")) { - event.respondWith( - cacheFirst({ - request: event.request, - fallbackUrl: "/index.html", - }), - ); - } -}); diff --git a/priv/static/js/db-store.js b/priv/static/js/db-store.js deleted file mode 100644 index 96cba29..0000000 --- a/priv/static/js/db-store.js +++ /dev/null @@ -1,143 +0,0 @@ -export class DBNoteStore extends EventTarget { - constructor(dbName, storeName) { - super(); - this.dbName = dbName; - this.storeName = storeName; - this.db = null; - } - - async all() { - const that = this; - return this.#connect().then( - (db) => - new Promise((resolve, reject) => { - db - .transaction([that.storeName], "readonly") - .objectStore(that.storeName) - .getAll().onsuccess = (data) => resolve(data.target.result); - }), - ); - } - - async get(id) { - const that = this; - let result; - - return this.#connect().then( - (db) => - new Promise( - (resolve, reject) => - (db - .transaction([that.storeName], "readonly") - .objectStore(that.storeName) - .get(id).onsuccess = (data) => resolve(data.target.result)), - ), - ); - } - - async add(note) { - const that = this; - const now = Date.now(); - - const entry = { - id: "id_" + now, - type: note.type, - content: note.content, - created: now, - updated: now, - }; - - return this.#connect().then( - (db) => - new Promise( - (resolve, reject) => - (db - .transaction([that.storeName], "readwrite") - .objectStore(that.storeName) - .add(entry).onsuccess = () => resolve(null)), - ), - ); - } - - async reset() { - const that = this; - - return this.#connect().then( - (db) => - new Promise( - (resolve, reject) => - (db - .transaction([that.storeName], "readwrite") - .objectStore(that.storeName) - .clear().onsuccess = () => resolve(null)), - ), - ); - } - - async remove({ id }) { - const that = this; - - return this.#connect().then( - (db) => - new Promise( - (resolve, reject) => - (db - .transaction([that.storeName], "readwrite") - .objectStore(that.storeName) - .delete(id).onsuccess = () => resolve(null)), - ), - ); - } - - async update(note) { - const that = this; - - note.updated = Date.now(); - - return this.#connect().then( - (db) => - new Promise( - (resolve, reject) => - (db - .transaction([that.storeName], "readwrite") - .objectStore(that.storeName) - .put(note).onsuccess = () => resolve(null)), - ), - ); - } - - saveStorage() { - this.dispatchEvent(new CustomEvent("save")); - } - - async #connect() { - if (!this.db) { - this.db = await this.#dbConnect(); - } - - return this.db; - } - - #dbConnect() { - const that = this; - - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.dbName, 1); - - request.onsuccess = (e) => { - resolve(e.target.result); - }; - - request.onerror = (e) => { - console.error(`indexedDB error: ${e.target.errorCode}`); - }; - - request.onupgradeneeded = (e) => { - const db = e.target.result; - db.createObjectStore(that.storeName, { - keyPath: "id", - }); - }; - }); - } -} diff --git a/priv/static/js/jenot.js b/priv/static/js/jenot.js index 0b7218b..1fd3886 100644 --- a/priv/static/js/jenot.js +++ b/priv/static/js/jenot.js @@ -1,8 +1,7 @@ -import "./service-worker.js"; +import "./service-worker-init.js"; import { renderText } from "./dom.js"; -import { NoteStore } from "./store.js"; -import { DBNoteStore } from "./db-store.js"; -import { WebNoteStore } from "./web-store.js"; +import { LocalNoteStore } from "./local-store.js"; +import { SyncedNoteStore } from "./synced-store.js"; import { authorizeNotifications, notificationsEnabled, @@ -29,8 +28,16 @@ notificationsTestButton.addEventListener("click", () => { const urlParams = new URLSearchParams(window.location.search); const Notes = urlParams.has("localStorage") - ? new NoteStore("jenot-app") - : new WebNoteStore("jenot-app", "notes"); + ? new LocalNoteStore("jenot-app") + : new SyncedNoteStore("jenot-app", "notes", "/"); + +const sync = async () => { + await Notes.sync(); + Notes.saveStorage(); +}; + +sync(); +setInterval(sync, 5000); const newNote = document.querySelector("#new-note"); const editNote = document.querySelector("#edit-note"); @@ -94,7 +101,6 @@ async function render() { newNote.classList.add("hidden"); editNote.classList.remove("hidden"); const note = await Notes.get(container.id); - console.log("edited note", note); editNote.load(note); }); }); diff --git a/priv/static/js/store.js b/priv/static/js/local-store.js similarity index 94% rename from priv/static/js/store.js rename to priv/static/js/local-store.js index b473aff..ca81a03 100644 --- a/priv/static/js/store.js +++ b/priv/static/js/local-store.js @@ -1,4 +1,4 @@ -export class NoteStore extends EventTarget { +export class LocalNoteStore extends EventTarget { localStorageKey; notes = []; @@ -44,10 +44,6 @@ export class NoteStore extends EventTarget { }); } - reset() { - this.notes = []; - } - remove({ id }) { this.notes = this.notes.filter((note) => note.id !== id); } @@ -57,6 +53,8 @@ export class NoteStore extends EventTarget { this.notes = this.notes.map((n) => (n.id === note.id ? note : n)); } + sync() {} + saveStorage() { window.localStorage.setItem( this.localStorageKey + "_notes", diff --git a/priv/static/js/service-worker-init.js b/priv/static/js/service-worker-init.js new file mode 100644 index 0000000..fb73632 --- /dev/null +++ b/priv/static/js/service-worker-init.js @@ -0,0 +1,24 @@ +const registerServiceWorker = async () => { + if ("serviceWorker" in navigator) { + try { + const registration = await navigator.serviceWorker.register( + "/js/service-worker.js", + { + type: "module", + scope: "/", + }, + ); + if (registration.installing) { + console.log("Service worker installing"); + } else if (registration.waiting) { + console.log("Service worker installed"); + } else if (registration.active) { + console.log("Service worker active"); + } + } catch (error) { + console.error(`Registration failed with ${error}`, error); + } + } +}; + +registerServiceWorker(); diff --git a/priv/static/js/service-worker.js b/priv/static/js/service-worker.js index 48c77d5..511eba0 100644 --- a/priv/static/js/service-worker.js +++ b/priv/static/js/service-worker.js @@ -1,23 +1,46 @@ -const registerServiceWorker = async () => { - if ("serviceWorker" in navigator) { - try { - const registration = await navigator.serviceWorker.register( - "/js/caching-worker.js", - { - scope: "/", - }, - ); - if (registration.installing) { - console.log("Service worker installing"); - } else if (registration.waiting) { - console.log("Service worker installed"); - } else if (registration.active) { - console.log("Service worker active"); - } - } catch (error) { - console.error(`Registration failed with ${error}`); +// caching + +const putInCache = async (request, response) => { + const cache = await caches.open("v1"); + await cache.put(request, response); +}; + +const cacheFirst = async ({ request, fallbackUrl }) => { + const responseFromCache = await caches.match(request); + if (responseFromCache) { + return responseFromCache; + } + + try { + const responseFromNetwork = await fetch(request); + // Cloning is needed because a response can only be consumed once. + putInCache(request, responseFromNetwork.clone()); + return responseFromNetwork; + } catch (error) { + const fallbackResponse = await caches.match(fallbackUrl); + if (fallbackResponse) { + return fallbackResponse; } + + return new Response("Network error happened", { + status: 408, + headers: { "Content-Type": "text/plain" }, + }); } }; -registerServiceWorker(); +self.addEventListener("fetch", (event) => { + // We don't cache API requests + if (!event.request.url?.pathname) { + return true; + } + + if (!event.request.url.pathname.startsWith("/api")) { + event.respondWith( + cacheFirst({ + request: event.request, + fallbackUrl: "/index.html", + }), + ); + } +}); diff --git a/priv/static/js/synced-store.js b/priv/static/js/synced-store.js new file mode 100644 index 0000000..476d1e5 --- /dev/null +++ b/priv/static/js/synced-store.js @@ -0,0 +1,261 @@ +class WebNoteStore { + constructor(endpoint) { + this.endpoint = endpoint || "/"; + } + + async all(lastSync, includeDeleted) { + const params = new URLSearchParams(); + if (lastSync > 0) { + params.append("since", lastSync); + } + if (includeDeleted) { + params.append("deleted", "true"); + } + const suffix = params.size > 0 ? `?${params.toString()}` : ""; + return this.#request(`${this.endpoint}api/notes${suffix}`, {}, () => []); + } + + async get(id) { + return this.#request(`${this.endpoint}api/notes/${id}`, {}, () => null); + } + + async add(note) { + return this.#request( + `${this.endpoint}api/notes`, + { + method: "POST", + body: JSON.stringify(note), + }, + () => null, + ); + } + + async update(note) { + return this.#request( + `${this.endpoint}api/notes/${note.id}`, + { + method: "PUT", + body: JSON.stringify(note), + }, + () => null, + ); + } + + async #request(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); + } + } +} + +export class SyncedNoteStore extends EventTarget { + constructor(dbName, storeName, endpoint) { + super(); + this.dbName = dbName; + this.storeName = storeName; + this.db = null; + this.webStore = endpoint && new WebNoteStore(endpoint); + } + + async all(since, includeDeleted) { + const that = this; + return this.#connect().then( + (db) => + new Promise((resolve, reject) => { + db + .transaction([that.storeName], "readonly") + .objectStore(that.storeName) + .getAll().onsuccess = (data) => { + const results = data.target.result.filter( + (n) => (includeDeleted || !n.deleted) && n.id !== "meta", + ); + + if (since > 0) { + return resolve(results.filter((n) => n.updated > since)); + } + + return resolve(results); + }; + }), + ); + } + + async get(id) { + const that = this; + let result; + + return this.#connect().then( + (db) => + new Promise( + (resolve, reject) => + (db + .transaction([that.storeName], "readonly") + .objectStore(that.storeName) + .get(id).onsuccess = (data) => resolve(data.target.result)), + ), + ); + } + + async getMeta() { + return this.get("meta").then((meta) => meta || { lastSync: null }); + } + + async setMeta(meta) { + meta.id = "meta"; + this.update(meta, true); + } + + async add(note) { + const that = this; + const now = Date.now(); + + const entry = { + id: "id_" + now, + type: note.type, + content: note.content, + created: now, + updated: now, + deleted: null, + }; + return this.#connect() + .then( + (db) => + new Promise( + (resolve, reject) => + (db + .transaction([that.storeName], "readwrite") + .objectStore(that.storeName) + .add(entry).onsuccess = () => resolve(null)), + ), + ) + .then(() => { + (async () => this.webStore?.add(entry))(); + return null; + }); + } + + async remove(note) { + const that = this; + + note.deleted = Date.now(); + + return this.update(note); + } + + async update(note, skipNetwork) { + const that = this; + + if (!skipNetwork) { + note.updated = Date.now(); + } + + return this.#connect() + .then( + (db) => + new Promise( + (resolve, reject) => + (db + .transaction([that.storeName], "readwrite") + .objectStore(that.storeName) + .put(note).onsuccess = () => resolve(null)), + ), + ) + .then(() => { + (async () => (skipNetwork ? null : this.webStore?.update(note)))(); + return null; + }); + } + + async sync() { + const that = this; + const meta = await this.getMeta(); + const lastSync = meta?.lastSync; + + this.all(lastSync, true) + .then((notes) => { + notes.forEach(async (n) => await that.webStore.add(n)); + }) + .then(() => that.webStore.all(lastSync, true)) + .then((notes) => { + notes.forEach(async (n) => await that.update(n, true)); + }) + .then(() => { + meta.lastSync = Date.now(); + + that.setMeta(meta); + }); + } + + saveStorage() { + this.dispatchEvent(new CustomEvent("save")); + } + + async #connect() { + if (!this.db) { + this.db = await this.#dbConnect(); + } + + return this.db; + } + + #dbConnect() { + const that = this; + + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, 1); + + request.onsuccess = (e) => { + resolve(e.target.result); + }; + + request.onerror = (e) => { + console.error(`indexedDB error: ${e.target.errorCode}`); + }; + + request.onupgradeneeded = (e) => { + const db = e.target.result; + db.createObjectStore(that.storeName, { + keyPath: "id", + }); + }; + }); + } + + #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); + } + } +} diff --git a/priv/static/js/web-store.js b/priv/static/js/web-store.js deleted file mode 100644 index 7423a4c..0000000 --- a/priv/static/js/web-store.js +++ /dev/null @@ -1,86 +0,0 @@ -export class WebNoteStore extends EventTarget { - constructor(dbName, storeName, endpoint) { - super(); - this.dbName = dbName; - this.storeName = storeName; - this.db = null; - this.endpoint = endpoint || "/"; - } - - async all() { - return this.#request(`${this.endpoint}api/notes`, {}, () => []); - } - - async get(id) { - console.log("get", id); - return this.#request(`${this.endpoint}api/notes/${id}`, {}, () => null); - } - - async add(note) { - const now = Date.now(); - - const entry = { - id: "id_" + now, - type: note.type, - content: note.content, - }; - - return this.#request( - `${this.endpoint}api/notes`, - { - method: "POST", - body: JSON.stringify(entry), - }, - () => null, - ); - } - - async reset() { - // NOOP for now - return null; - } - - async remove({ id }) { - return this.#request( - `${this.endpoint}api/notes/${id}`, - { - method: "DELETE", - }, - () => null, - ); - } - - async update(note) { - return this.#request( - `${this.endpoint}api/notes/${note.id}`, - { - method: "PUT", - body: JSON.stringify(note), - }, - () => null, - ); - } - - saveStorage() { - this.dispatchEvent(new CustomEvent("save")); - } - - async #request(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); - } - } -}