Implement synchronization, offline data handling, and intelligent request routing
- Add services for offline data management, including `offline-data.service.ts`, ensuring data is saved to a local database and synced with the server when online. - Introduce bidirectional `SyncService` for managing data synchronization with conflict resolution and retry mechanisms. - Create `data.service.ts` to handle smart routing between local database and server API based on connectivity status. - Update models and logic to support comprehensive synchronization for books, chapters, characters, and conversations. - Implement event listeners for online/offline detection and automatic sync scheduling.
This commit is contained in:
366
lib/services/data.service.ts
Normal file
366
lib/services/data.service.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import System from '@/lib/models/System';
|
||||
import { BookProps, BookListProps } from '@/lib/models/Book';
|
||||
import { ChapterProps } from '@/lib/models/Chapter';
|
||||
import { CharacterProps } from '@/lib/models/Character';
|
||||
import { Conversation } from '@/lib/models/QuillSense';
|
||||
|
||||
/**
|
||||
* DataService - Smart routing layer between server API and local database
|
||||
* Automatically routes requests based on offline/online status
|
||||
*/
|
||||
export class DataService {
|
||||
private static isOffline: boolean = false;
|
||||
private static accessToken: string | null = null;
|
||||
|
||||
/**
|
||||
* Set offline mode status
|
||||
*/
|
||||
static setOfflineMode(offline: boolean): void {
|
||||
this.isOffline = offline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set access token for API requests
|
||||
*/
|
||||
static setAccessToken(token: string | null): void {
|
||||
this.accessToken = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently offline
|
||||
*/
|
||||
static isCurrentlyOffline(): boolean {
|
||||
return this.isOffline;
|
||||
}
|
||||
|
||||
// ========== BOOK OPERATIONS ==========
|
||||
|
||||
/**
|
||||
* Get all books
|
||||
*/
|
||||
static async getBooks(): Promise<BookListProps[]> {
|
||||
if (this.isOffline) {
|
||||
// Use local database
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
throw new Error('Electron API not available');
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbGetBooks();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to get books from local DB');
|
||||
}
|
||||
return result.data || [];
|
||||
} else {
|
||||
// Use server API
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const response = await System.authGetQueryToServer<BookListProps[]>(
|
||||
'books',
|
||||
this.accessToken
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single book with all data
|
||||
*/
|
||||
static async getBook(bookId: string): Promise<BookProps | null> {
|
||||
if (this.isOffline) {
|
||||
// Use local database
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
throw new Error('Electron API not available');
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbGetBook(bookId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to get book from local DB');
|
||||
}
|
||||
return result.data || null;
|
||||
} else {
|
||||
// Use server API
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const response = await System.authGetQueryToServer<BookProps>(
|
||||
`books/${bookId}`,
|
||||
this.accessToken
|
||||
);
|
||||
|
||||
return response.data || null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update a book
|
||||
*/
|
||||
static async saveBook(book: BookProps | BookListProps, authorId?: string): Promise<void> {
|
||||
if (this.isOffline) {
|
||||
// Save to local database
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
throw new Error('Electron API not available');
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbSaveBook(book, authorId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save book to local DB');
|
||||
}
|
||||
} else {
|
||||
// Save to server
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const isUpdate = 'bookId' in book && book.bookId;
|
||||
if (isUpdate) {
|
||||
await System.authPutToServer(`books/${book.bookId || (book as any).id}`, book, this.accessToken);
|
||||
} else {
|
||||
await System.authPostToServer('books', book, this.accessToken);
|
||||
}
|
||||
|
||||
// Also save to local DB for caching
|
||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
||||
await (window as any).electron.dbSaveBook(book, authorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a book
|
||||
*/
|
||||
static async deleteBook(bookId: string): Promise<void> {
|
||||
if (this.isOffline) {
|
||||
// Delete from local database
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
throw new Error('Electron API not available');
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbDeleteBook(bookId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to delete book from local DB');
|
||||
}
|
||||
} else {
|
||||
// Delete from server
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
await System.authDeleteToServer(`books/${bookId}`, {}, this.accessToken);
|
||||
|
||||
// Also delete from local DB
|
||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
||||
await (window as any).electron.dbDeleteBook(bookId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CHAPTER OPERATIONS ==========
|
||||
|
||||
/**
|
||||
* Save or update a chapter
|
||||
*/
|
||||
static async saveChapter(chapter: ChapterProps, bookId: string, contentId?: string): Promise<void> {
|
||||
if (this.isOffline) {
|
||||
// Save to local database
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
throw new Error('Electron API not available');
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbSaveChapter(chapter, bookId, contentId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save chapter to local DB');
|
||||
}
|
||||
} else {
|
||||
// Save to server
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const isUpdate = !!chapter.chapterId;
|
||||
if (isUpdate) {
|
||||
await System.authPutToServer(`chapters/${chapter.chapterId}`, chapter, this.accessToken);
|
||||
} else {
|
||||
await System.authPostToServer('chapters', { ...chapter, bookId }, this.accessToken);
|
||||
}
|
||||
|
||||
// Also save to local DB for caching
|
||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
||||
await (window as any).electron.dbSaveChapter(chapter, bookId, contentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CHARACTER OPERATIONS ==========
|
||||
|
||||
/**
|
||||
* Get all characters for a book
|
||||
*/
|
||||
static async getCharacters(bookId: string): Promise<CharacterProps[]> {
|
||||
if (this.isOffline) {
|
||||
// Use local database
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
throw new Error('Electron API not available');
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbGetCharacters(bookId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to get characters from local DB');
|
||||
}
|
||||
return result.data || [];
|
||||
} else {
|
||||
// Use server API
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const response = await System.authGetQueryToServer<CharacterProps[]>(
|
||||
`characters?bookId=${bookId}`,
|
||||
this.accessToken
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update a character
|
||||
*/
|
||||
static async saveCharacter(character: CharacterProps, bookId: string): Promise<void> {
|
||||
if (this.isOffline) {
|
||||
// Save to local database
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
throw new Error('Electron API not available');
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbSaveCharacter(character, bookId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to save character to local DB');
|
||||
}
|
||||
} else {
|
||||
// Save to server
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const isUpdate = !!character.id;
|
||||
if (isUpdate) {
|
||||
await System.authPutToServer(`characters/${character.id}`, character, this.accessToken);
|
||||
} else {
|
||||
await System.authPostToServer('characters', { ...character, bookId }, this.accessToken);
|
||||
}
|
||||
|
||||
// Also save to local DB for caching
|
||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
||||
await (window as any).electron.dbSaveCharacter(character, bookId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== AI CONVERSATION OPERATIONS ==========
|
||||
|
||||
/**
|
||||
* Get all AI conversations for a book
|
||||
*/
|
||||
static async getConversations(bookId: string): Promise<Conversation[]> {
|
||||
if (this.isOffline) {
|
||||
// Use local database
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
throw new Error('Electron API not available');
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbGetConversations(bookId);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to get conversations from local DB');
|
||||
}
|
||||
return result.data || [];
|
||||
} else {
|
||||
// Use server API
|
||||
if (!this.accessToken) {
|
||||
throw new Error('No access token available');
|
||||
}
|
||||
|
||||
const response = await System.authGetQueryToServer<Conversation[]>(
|
||||
`ai/conversations?bookId=${bookId}`,
|
||||
this.accessToken
|
||||
);
|
||||
|
||||
return response.data || [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an AI conversation (always saves locally when using AI)
|
||||
*/
|
||||
static async saveConversation(conversation: Conversation, bookId: string): Promise<void> {
|
||||
// Always save AI conversations to local DB first
|
||||
if (typeof window !== 'undefined' && (window as any).electron) {
|
||||
const result = await (window as any).electron.dbSaveConversation(conversation, bookId);
|
||||
if (!result.success) {
|
||||
console.error('Failed to save conversation to local DB:', result.error);
|
||||
}
|
||||
}
|
||||
|
||||
// If online, also sync to server
|
||||
if (!this.isOffline && this.accessToken) {
|
||||
try {
|
||||
const isUpdate = !!conversation.id;
|
||||
if (isUpdate) {
|
||||
await System.authPutToServer(
|
||||
`ai/conversations/${conversation.id}`,
|
||||
conversation,
|
||||
this.accessToken
|
||||
);
|
||||
} else {
|
||||
await System.authPostToServer(
|
||||
'ai/conversations',
|
||||
{ ...conversation, bookId },
|
||||
this.accessToken
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to sync conversation to server:', error);
|
||||
// Don't throw - local save succeeded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SYNC STATUS ==========
|
||||
|
||||
/**
|
||||
* Get sync status from local database
|
||||
*/
|
||||
static async getSyncStatus(): Promise<any[]> {
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbGetSyncStatus();
|
||||
if (!result.success) {
|
||||
console.error('Failed to get sync status:', result.error);
|
||||
return [];
|
||||
}
|
||||
return result.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending changes awaiting sync
|
||||
*/
|
||||
static async getPendingChanges(limit: number = 100): Promise<any[]> {
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbGetPendingChanges(limit);
|
||||
if (!result.success) {
|
||||
console.error('Failed to get pending changes:', result.error);
|
||||
return [];
|
||||
}
|
||||
return result.data || [];
|
||||
}
|
||||
}
|
||||
|
||||
export default DataService;
|
||||
Reference in New Issue
Block a user