Skip to content

Commit 858d24d

Browse files
Merge pull request #726 from diksha78dev/fix/issue-650
Complete offline library sync queue(#650)
2 parents 11f3836 + 3644f57 commit 858d24d

4 files changed

Lines changed: 317 additions & 41 deletions

File tree

‎frontend/js/app.js‎

Lines changed: 210 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,17 +228,16 @@ const CollectionAPI = {
228228
}
229229
};
230230
window.CollectionAPI = CollectionAPI;
231-
import { saveBookOffline, removeOfflineBook, db } from './db.js';
232231

233232
// Example click handler for your custom "Save for Offline" icon
234233
async function handleDownloadToggle(bookCard, bookData) {
235-
const isAlreadyDownloaded = await db.downloadedBooks.get(bookData.id);
234+
const isAlreadyDownloaded = await window.db.downloadedBooks.get(bookData.id);
236235

237236
if (isAlreadyDownloaded) {
238-
const success = await removeOfflineBook(bookData.id);
237+
const success = await window.removeOfflineBook(bookData.id);
239238
if (success) bookCard.classList.remove('is-downloaded');
240239
} else {
241-
const success = await saveBookOffline(bookData);
240+
const success = await window.saveBookOffline(bookData);
242241
if (success) bookCard.classList.add('is-downloaded');
243242
}
244243
}
@@ -1864,6 +1863,10 @@ class LibraryManager {
18641863

18651864
// 4. Sync with backend if available (Full Refresh)
18661865
await this.syncWithBackend();
1866+
if (navigator.onLine) {
1867+
await this.flushPendingLibraryMutations();
1868+
}
1869+
await this.updateSyncStatus();
18671870
}
18681871

18691872
getUser() {
@@ -1883,6 +1886,194 @@ class LibraryManager {
18831886
return new Headers(headers);
18841887
}
18851888

1889+
async _getPendingSyncCount() {
1890+
const user = this.getUser();
1891+
if (!user || !window.db?.syncQueue) return 0;
1892+
return await window.db.syncQueue.where('userId').equals(user.id).count();
1893+
}
1894+
1895+
async updateSyncStatus() {
1896+
const statusEl = document.getElementById('library-sync-status');
1897+
if (!statusEl) return;
1898+
1899+
const pendingCount = await this._getPendingSyncCount();
1900+
statusEl.hidden = false;
1901+
statusEl.textContent = pendingCount > 0
1902+
? `${pendingCount} pending sync${pendingCount === 1 ? '' : 's'}`
1903+
: 'Synced';
1904+
statusEl.dataset.state = pendingCount > 0 ? 'pending' : 'synced';
1905+
}
1906+
1907+
async _queueMutation(action, book, extra = {}) {
1908+
if (typeof window.enqueueLibraryMutation !== 'function') return;
1909+
1910+
const user = this.getUser();
1911+
if (!user || !window.db?.syncQueue) return;
1912+
1913+
const snapshot = JSON.parse(JSON.stringify(book));
1914+
1915+
const existingMutations = (await window.db.syncQueue.where('userId').equals(user.id).toArray())
1916+
.filter((mutation) => mutation.bookId === snapshot.id);
1917+
1918+
if (action === 'remove') {
1919+
await Promise.all(existingMutations.map((mutation) => window.db.syncQueue.delete(mutation.id)));
1920+
await this.updateSyncStatus();
1921+
return;
1922+
}
1923+
1924+
const shelf = extra.shelf || this.findBookShelf(snapshot.id) || null;
1925+
const mergedMutation = {
1926+
userId: user.id,
1927+
action,
1928+
bookId: snapshot.id,
1929+
db_id: snapshot.db_id || null,
1930+
shelf,
1931+
payload: extra,
1932+
book: snapshot
1933+
};
1934+
1935+
const pendingAdd = existingMutations.find((mutation) => mutation.action === 'add');
1936+
if (pendingAdd && (action === 'move' || action === 'update')) {
1937+
await window.db.syncQueue.put({
1938+
...pendingAdd,
1939+
db_id: mergedMutation.db_id || pendingAdd.db_id || null,
1940+
shelf: action === 'move' ? extra.toShelf || pendingAdd.shelf : pendingAdd.shelf,
1941+
payload: {
1942+
...(pendingAdd.payload || {}),
1943+
...extra
1944+
},
1945+
book: snapshot,
1946+
createdAt: pendingAdd.createdAt || new Date().toISOString()
1947+
});
1948+
await this.updateSyncStatus();
1949+
return;
1950+
}
1951+
1952+
if (action === 'add') {
1953+
await Promise.all(existingMutations.map((mutation) => window.db.syncQueue.delete(mutation.id)));
1954+
}
1955+
1956+
await window.enqueueLibraryMutation(mergedMutation);
1957+
await this.updateSyncStatus();
1958+
}
1959+
1960+
async _applyQueuedMutation(mutation) {
1961+
const user = this.getUser();
1962+
if (!user) return;
1963+
1964+
const localBookResult = this.findBookInShelf(mutation.bookId);
1965+
const localBook = localBookResult?.book || mutation.book;
1966+
const dbId = localBook?.db_id || mutation.db_id;
1967+
1968+
if (mutation.action === 'add') {
1969+
if (!localBook) return;
1970+
1971+
const payload = {
1972+
user_id: user.id,
1973+
google_books_id: localBook.id,
1974+
title: localBook.volumeInfo?.title || localBook.title || '',
1975+
authors: localBook.volumeInfo?.authors ? localBook.volumeInfo.authors.join(', ') : '',
1976+
thumbnail: localBook.volumeInfo?.imageLinks?.thumbnail || '',
1977+
shelf_type: mutation.shelf || mutation.payload?.shelf || 'want'
1978+
};
1979+
1980+
const res = await fetch(`${this.apiBase}/library`, {
1981+
method: 'POST',
1982+
headers: this.getAuthHeaders(),
1983+
credentials: 'include',
1984+
body: JSON.stringify(payload)
1985+
});
1986+
1987+
if (!res.ok) {
1988+
const data = await res.json().catch(() => ({}));
1989+
throw new Error(data.error || `HTTP ${res.status}`);
1990+
}
1991+
1992+
const data = await res.json();
1993+
if (localBook) {
1994+
localBook.db_id = data.item.id;
1995+
localBook.version = data.item.version;
1996+
this.saveLocally();
1997+
}
1998+
return;
1999+
}
2000+
2001+
if (mutation.action === 'remove') {
2002+
if (!dbId) return;
2003+
2004+
const res = await fetch(`${this.apiBase}/library/${dbId}`, {
2005+
method: 'DELETE',
2006+
headers: this.getAuthHeaders(),
2007+
credentials: 'include'
2008+
});
2009+
2010+
if (!res.ok) {
2011+
const data = await res.json().catch(() => ({}));
2012+
throw new Error(data.error || `HTTP ${res.status}`);
2013+
}
2014+
return;
2015+
}
2016+
2017+
if (mutation.action === 'move' || mutation.action === 'update') {
2018+
if (!dbId || !localBook) return;
2019+
2020+
const body = mutation.action === 'move'
2021+
? {
2022+
shelf_type: mutation.payload?.toShelf,
2023+
progress: localBook.progress,
2024+
version: localBook.version
2025+
}
2026+
: {
2027+
...mutation.payload?.updates,
2028+
version: localBook.version
2029+
};
2030+
2031+
const res = await fetch(`${this.apiBase}/library/${dbId}`, {
2032+
method: 'PUT',
2033+
headers: this.getAuthHeaders(),
2034+
credentials: 'include',
2035+
body: JSON.stringify(body)
2036+
});
2037+
2038+
if (!res.ok) {
2039+
const data = await res.json().catch(() => ({}));
2040+
throw new Error(data.error || `HTTP ${res.status}`);
2041+
}
2042+
2043+
const data = await res.json();
2044+
localBook.version = data.item.version;
2045+
this.saveLocally();
2046+
}
2047+
}
2048+
2049+
async flushPendingLibraryMutations() {
2050+
const user = this.getUser();
2051+
if (!user || !window.db?.syncQueue) {
2052+
await this.updateSyncStatus();
2053+
return 0;
2054+
}
2055+
2056+
const pendingMutations = await window.db.syncQueue.where('userId').equals(user.id).sortBy('createdAt');
2057+
if (pendingMutations.length === 0) {
2058+
await this.updateSyncStatus();
2059+
return 0;
2060+
}
2061+
2062+
let processed = 0;
2063+
for (const mutation of pendingMutations) {
2064+
await this._applyQueuedMutation(mutation);
2065+
await window.db.syncQueue.delete(mutation.id);
2066+
processed += 1;
2067+
}
2068+
2069+
if (processed > 0) {
2070+
await this.syncWithBackend();
2071+
}
2072+
2073+
await this.updateSyncStatus();
2074+
return processed;
2075+
}
2076+
18862077
async syncWithBackend() {
18872078
const user = this.getUser();
18882079
if (!user) return;
@@ -1975,6 +2166,7 @@ class LibraryManager {
19752166
this.renderShelf('finished', 'shelf-finished');
19762167
}
19772168
}
2169+
await this.updateSyncStatus();
19782170
}
19792171
} catch (e) {
19802172
console.error("Sync failed", e);
@@ -2032,6 +2224,7 @@ class LibraryManager {
20322224

20332225
// After upload, pull fresh state from backend to get the new DB IDs and versions
20342226
await this.syncWithBackend();
2227+
await this.updateSyncStatus();
20352228
} else {
20362229
const data = await res.json();
20372230
console.error("Backend refused sync", data);
@@ -2201,10 +2394,12 @@ class LibraryManager {
22012394
enrichedBook.db_id = data.item.id;
22022395
enrichedBook.version = data.item.version;
22032396
this.saveLocally();
2397+
await this.updateSyncStatus();
22042398
}
22052399
} catch (e) {
22062400
console.error("Failed to save to backend", e);
2207-
showToast("Saved locally (Sync failed)", "info");
2401+
await this._queueMutation('add', enrichedBook, { shelf });
2402+
showToast("Saved locally; sync queued", "info");
22082403
}
22092404
}
22102405
}
@@ -2250,6 +2445,7 @@ class LibraryManager {
22502445
const data = await res.json();
22512446
book.version = data.item.version;
22522447
this.saveLocally();
2448+
await this.updateSyncStatus();
22532449
} else if (res.status === 409) {
22542450
const data = await res.json();
22552451
showToast("Conflict detected! Syncing with server...", "error");
@@ -2261,7 +2457,8 @@ class LibraryManager {
22612457
}
22622458
} catch (e) {
22632459
console.error("Failed to update backend", e);
2264-
showToast("Saved locally (Sync failed)", "info");
2460+
await this._queueMutation('update', book, { updates });
2461+
showToast("Saved locally; sync queued", "info");
22652462
}
22662463
}
22672464
}
@@ -2325,9 +2522,11 @@ class LibraryManager {
23252522
headers: this.getAuthHeaders(),
23262523
credentials: 'include'
23272524
});
2525+
await this.updateSyncStatus();
23282526
} catch (e) {
23292527
console.error("Failed to delete from backend", e);
2330-
showToast("Removed locally (Backend sync failed)", "info");
2528+
await this._queueMutation('remove', book, { shelf });
2529+
showToast("Removed locally; sync queued", "info");
23312530
}
23322531
} else if (user) {
23332532
// Fallback: If we don't have db_id locally (maybe added before login logic),
@@ -2379,6 +2578,7 @@ class LibraryManager {
23792578
const data = await res.json();
23802579
book.version = data.item.version;
23812580
this.saveLocally();
2581+
await this.updateSyncStatus();
23822582
} else if (res.status === 409) {
23832583
showToast("Conflict detected! Syncing with server...", "error");
23842584
await this.syncWithBackend();
@@ -2389,10 +2589,12 @@ class LibraryManager {
23892589
}
23902590
} catch (e) {
23912591
console.error("Failed to update backend during move", e);
2392-
showToast("Moved locally (Sync failed)", "info");
2592+
await this._queueMutation('move', book, { fromShelf, toShelf });
2593+
showToast("Moved locally; sync queued", "info");
23932594
}
23942595
}
23952596

2597+
await this.updateSyncStatus();
23962598
return true;
23972599
}
23982600

0 commit comments

Comments
 (0)