Make sync offline friendly

This commit is contained in:
Adrian Gruntkowski 2024-11-28 23:23:29 +01:00
parent 17c158b6c1
commit d11d0b0cf1
5 changed files with 41 additions and 41 deletions

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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) => {

View file

@ -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);
}
}
}