- Updated delete methods across hooks and components to include `deletedAt: System.timeStampInSeconds()`. - Refactored synchronized delete logic to pass `deletedAt` for both offline and online states. - Improved synchronization workflows to include `deletedAt` in server and IPC requests. - Enhanced destructuring patterns for cleaner and more consistent request data.
267 lines
11 KiB
TypeScript
267 lines
11 KiB
TypeScript
import {useContext} from 'react';
|
|
import System from '@/lib/models/System';
|
|
import {SessionContext} from '@/context/SessionContext';
|
|
import {LangContext} from '@/context/LangContext';
|
|
import {AlertContext} from '@/context/AlertContext';
|
|
import OfflineContext from '@/context/OfflineContext';
|
|
import {BooksSyncContext} from '@/context/BooksSyncContext';
|
|
import {CompleteBook} from '@/lib/models/Book';
|
|
import {BookSyncCompare, SyncedBook} from '@/lib/models/SyncedBook';
|
|
import {useTranslations} from 'next-intl';
|
|
|
|
interface RemovedItemRecord {
|
|
removal_id: string;
|
|
table_name: string;
|
|
entity_id: string;
|
|
book_id: string | null;
|
|
user_id: string;
|
|
deleted_at: number;
|
|
}
|
|
|
|
interface SyncedBooksResponse {
|
|
books: SyncedBook[];
|
|
tombstones: RemovedItemRecord[];
|
|
}
|
|
|
|
export default function useSyncBooks() {
|
|
const t = useTranslations();
|
|
const {session} = useContext(SessionContext);
|
|
const {lang} = useContext(LangContext);
|
|
const {errorMessage} = useContext(AlertContext);
|
|
const {isCurrentlyOffline, offlineMode} = useContext(OfflineContext);
|
|
const {
|
|
booksToSyncToServer,
|
|
booksToSyncFromServer,
|
|
localOnlyBooks,
|
|
serverOnlyBooks,
|
|
setLocalOnlyBooks,
|
|
setServerOnlyBooks,
|
|
setServerSyncedBooks,
|
|
setLocalSyncedBooks,
|
|
setBooksToSyncFromServer,
|
|
setBooksToSyncToServer
|
|
} = useContext(BooksSyncContext);
|
|
|
|
async function upload(bookId: string): Promise<boolean> {
|
|
if (isCurrentlyOffline()) return false;
|
|
|
|
try {
|
|
const bookToSync: CompleteBook = await window.electron.invoke<CompleteBook>('db:book:uploadToServer', bookId);
|
|
if (!bookToSync) {
|
|
errorMessage(t('bookCard.uploadError'));
|
|
return false;
|
|
}
|
|
const response: boolean = await System.authPostToServer('book/sync/upload', {
|
|
book: bookToSync
|
|
}, session.accessToken, lang);
|
|
if (!response) {
|
|
errorMessage(t('bookCard.uploadError'));
|
|
return false;
|
|
}
|
|
const uploadedBook: SyncedBook | undefined = localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId);
|
|
setLocalOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => {
|
|
return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId);
|
|
});
|
|
if (uploadedBook) {
|
|
setLocalSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, uploadedBook]);
|
|
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, uploadedBook]);
|
|
}
|
|
return true;
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('bookCard.uploadError'));
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function download(bookId: string): Promise<boolean> {
|
|
if (isCurrentlyOffline()) return false;
|
|
|
|
try {
|
|
const response: CompleteBook = await System.authGetQueryToServer('book/sync/download', session.accessToken, lang, {bookId});
|
|
if (!response) {
|
|
errorMessage(t('bookCard.downloadError'));
|
|
return false;
|
|
}
|
|
const syncStatus: boolean = await window.electron.invoke<boolean>('db:book:syncSave', response);
|
|
if (!syncStatus) {
|
|
errorMessage(t('bookCard.downloadError'));
|
|
return false;
|
|
}
|
|
const downloadedBook: SyncedBook | undefined = serverOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId);
|
|
setServerOnlyBooks((prevBooks: SyncedBook[]): SyncedBook[] => {
|
|
return prevBooks.filter((book: SyncedBook): boolean => book.id !== bookId);
|
|
});
|
|
if (downloadedBook) {
|
|
setLocalSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, downloadedBook]);
|
|
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] => [...prev, downloadedBook]);
|
|
}
|
|
return true;
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('bookCard.downloadError'));
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function syncFromServer(bookId: string): Promise<boolean> {
|
|
if (isCurrentlyOffline()) return false;
|
|
|
|
try {
|
|
const bookToFetch: BookSyncCompare | undefined = booksToSyncFromServer.find((book: BookSyncCompare): boolean => book.id === bookId);
|
|
if (!bookToFetch) {
|
|
errorMessage(t('bookCard.syncFromServerError'));
|
|
return false;
|
|
}
|
|
const response: CompleteBook = await System.authPostToServer('book/sync/server-to-client', {
|
|
bookToSync: bookToFetch
|
|
}, session.accessToken, lang);
|
|
if (!response) {
|
|
errorMessage(t('bookCard.syncFromServerError'));
|
|
return false;
|
|
}
|
|
const syncStatus: boolean = await window.electron.invoke<boolean>('db:book:sync:toClient', response);
|
|
if (!syncStatus) {
|
|
errorMessage(t('bookCard.syncFromServerError'));
|
|
return false;
|
|
}
|
|
setBooksToSyncFromServer((prev: BookSyncCompare[]): BookSyncCompare[] =>
|
|
prev.filter((book: BookSyncCompare): boolean => book.id !== bookId)
|
|
);
|
|
return true;
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('bookCard.syncFromServerError'));
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function syncToServer(bookId: string): Promise<boolean> {
|
|
if (isCurrentlyOffline()) return false;
|
|
|
|
try {
|
|
const bookToFetch: BookSyncCompare | undefined = booksToSyncToServer.find((book: BookSyncCompare): boolean => book.id === bookId);
|
|
if (!bookToFetch) {
|
|
errorMessage(t('bookCard.syncToServerError'));
|
|
return false;
|
|
}
|
|
const bookToSync: CompleteBook = await window.electron.invoke<CompleteBook>('db:book:sync:toServer', bookToFetch);
|
|
if (!bookToSync) {
|
|
errorMessage(t('bookCard.syncToServerError'));
|
|
return false;
|
|
}
|
|
const response: boolean = await System.authPatchToServer('book/sync/client-to-server', {
|
|
book: bookToSync
|
|
}, session.accessToken, lang);
|
|
if (!response) {
|
|
errorMessage(t('bookCard.syncToServerError'));
|
|
return false;
|
|
}
|
|
setBooksToSyncToServer((prev: BookSyncCompare[]): BookSyncCompare[] =>
|
|
prev.filter((book: BookSyncCompare): boolean => book.id !== bookId)
|
|
);
|
|
return true;
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('bookCard.syncToServerError'));
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function syncAllToServer(): Promise<void> {
|
|
for (const diff of booksToSyncToServer) {
|
|
await syncToServer(diff.id);
|
|
}
|
|
}
|
|
|
|
async function syncAllFromServer(): Promise<void> {
|
|
for (const diff of booksToSyncFromServer) {
|
|
await syncFromServer(diff.id);
|
|
}
|
|
}
|
|
|
|
async function refreshBooks(): Promise<void> {
|
|
try {
|
|
let localBooksResponse: SyncedBook[] = [];
|
|
let serverBooksResponse: SyncedBook[] = [];
|
|
|
|
if (!isCurrentlyOffline()) {
|
|
if (offlineMode.isDatabaseInitialized) {
|
|
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
|
|
|
// Get lastOnlineTimestamp from localStorage (or 0 if not set)
|
|
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
|
|
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
|
|
|
|
// Get local tombstones since lastOnlineTimestamp via IPC
|
|
const localTombstones: RemovedItemRecord[] = await window.electron.invoke<RemovedItemRecord[]>(
|
|
'db:tombstones:since',
|
|
lastOnlineTimestamp
|
|
);
|
|
|
|
// Call server with POST and tombstones
|
|
const serverResponse: SyncedBooksResponse = await System.authPostToServer<SyncedBooksResponse>(
|
|
'books/synced',
|
|
{ lastOnlineTimestamp, tombstones: localTombstones },
|
|
session.accessToken,
|
|
lang
|
|
);
|
|
|
|
serverBooksResponse = serverResponse.books;
|
|
|
|
// Apply server tombstones locally via IPC
|
|
await window.electron.invoke<void>('db:tombstones:apply:books', serverResponse.tombstones);
|
|
} else {
|
|
// No local DB but online - just get server books without tombstones
|
|
const serverResponse: SyncedBooksResponse = await System.authPostToServer<SyncedBooksResponse>(
|
|
'books/synced',
|
|
{ lastOnlineTimestamp: 0, tombstones: [] },
|
|
session.accessToken,
|
|
lang
|
|
);
|
|
serverBooksResponse = serverResponse.books;
|
|
}
|
|
} else {
|
|
if (offlineMode.isDatabaseInitialized) {
|
|
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
|
}
|
|
}
|
|
|
|
setServerSyncedBooks(serverBooksResponse);
|
|
setLocalSyncedBooks(localBooksResponse);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('bookCard.refreshError'));
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
upload,
|
|
download,
|
|
syncFromServer,
|
|
syncToServer,
|
|
syncAllToServer,
|
|
syncAllFromServer,
|
|
refreshBooks,
|
|
localOnlyBooks,
|
|
serverOnlyBooks,
|
|
booksToSyncToServer,
|
|
booksToSyncFromServer
|
|
};
|
|
}
|