Remove DataService and OfflineDataService, refactor book and character operations to use streamlined handlers in LocalSystem

- Delete `data.service.ts` and `offline-data.service.ts`, consolidating functionality into `LocalSystem`.
- Refactor book, character, and conversation operations to adopt unified, multilingual, and session-enabled IPC handlers in `LocalSystem`.
- Simplify redundant legacy methods, enhancing maintainability and consistency.
This commit is contained in:
natreex
2025-11-18 21:02:38 -05:00
parent d46aecc80d
commit d018e75be4
11 changed files with 222 additions and 1111 deletions

View File

@@ -1,366 +0,0 @@
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;

View File

@@ -1,395 +0,0 @@
import { BookProps, BookListProps } from '@/lib/models/Book';
import { CharacterProps } from '@/lib/models/Character';
import { Conversation } from '@/lib/models/QuillSense';
import { WorldProps } from '@/lib/models/World';
import { ChapterProps } from '@/lib/models/Chapter';
/**
* OfflineDataService - Manages data retrieval with offline/online mode
* Automatically caches to local DB when online, serves from local DB when offline
*/
export class OfflineDataService {
private isOffline: boolean = false;
/**
* Set offline status (called by OfflineProvider)
*/
setOfflineStatus(offline: boolean): void {
this.isOffline = offline;
}
/**
* Get current offline status
*/
getOfflineStatus(): boolean {
return this.isOffline;
}
/**
* Get all books - from local DB if offline, from server otherwise
*/
async getBooks(fetchFromServer: () => Promise<BookListProps[]>): Promise<BookListProps[]> {
if (!window.electron) {
return await fetchFromServer();
}
if (this.isOffline) {
// Fetch from local DB
const result = await window.electron.dbGetBooks();
if (result.success && result.data) {
const titles = result.data.map(b => b.title).join(', ');
console.log(`📚 Books from LOCAL DB: ${titles}`);
return result.data;
}
throw new Error(result.error || 'Failed to get books from local DB');
} else {
// Fetch from server and save to local DB
const books = await fetchFromServer();
const titles = books.map(b => b.title).join(', ');
console.log(`📚 Books from SERVER: ${titles}`);
// Save to local DB in background
for (const book of books) {
try {
await window.electron.dbSaveBook(book);
} catch (error) {
console.error('Failed to save book to local DB:', error);
}
}
return books;
}
}
/**
* Get single book - from local DB if offline, from server otherwise
*/
async getBook(
bookId: string,
fetchFromServer: () => Promise<BookProps>
): Promise<BookProps> {
if (!window.electron) {
return await fetchFromServer();
}
if (this.isOffline) {
const result = await window.electron.dbGetBook(bookId);
if (result.success && result.data) {
console.log(`📖 "${result.data.title}" from LOCAL DB`);
return result.data;
}
throw new Error(result.error || 'Book not found in local DB');
} else {
const book = await fetchFromServer();
console.log(`📖 "${book.title}" from SERVER`);
// Save to local DB
try {
await window.electron.dbSaveBook(book);
} catch (error) {
console.error('Failed to save book to local DB:', error);
}
return book;
}
}
/**
* Create new book - creates on server if online, local UUID if offline
*/
async createBook(
bookData: Omit<BookProps, 'bookId'>,
authorId: string,
createOnServer: () => Promise<string>
): Promise<string> {
if (!window.electron) {
return await createOnServer();
}
if (this.isOffline) {
// Generate local UUID and save to local DB
const localBookId = crypto.randomUUID();
const book: BookProps = { ...bookData, bookId: localBookId };
await window.electron.dbSaveBook(book, authorId);
console.log(`💾 Book "${book.title}" created locally (offline mode)`);
return localBookId;
} else {
// Create on server and save to local DB
const serverBookId = await createOnServer();
const book: BookProps = { ...bookData, bookId: serverBookId };
await window.electron.dbSaveBook(book, authorId);
return serverBookId;
}
}
/**
* Save book - save to local DB and sync to server later if offline
*/
async saveBook(
book: BookProps,
authorId: string | undefined,
saveToServer: () => Promise<void>
): Promise<void> {
if (!window.electron) {
return await saveToServer();
}
// Always save to local DB first
await window.electron.dbSaveBook(book, authorId);
if (!this.isOffline) {
// Also save to server
try {
await saveToServer();
} catch (error) {
console.error('Failed to save to server, will sync later:', error);
// Data is already in local DB, will be synced later
}
} else {
console.log(`💾 Book queued for sync (offline mode)`);
}
}
/**
* Get characters for a book
*/
async getCharacters(
bookId: string,
fetchFromServer: () => Promise<CharacterProps[]>
): Promise<CharacterProps[]> {
if (!window.electron) {
return await fetchFromServer();
}
if (this.isOffline) {
const result = await window.electron.dbGetCharacters(bookId);
if (result.success && result.data) {
const names = result.data.map(c => c.name).join(', ');
console.log(`👤 Characters from LOCAL DB: ${names}`);
return result.data;
}
throw new Error(result.error || 'Failed to get characters from local DB');
} else {
const characters = await fetchFromServer();
const names = characters.map(c => c.name).join(', ');
console.log(`👤 Characters from SERVER: ${names}`);
// Save to local DB
for (const character of characters) {
try {
await window.electron.dbSaveCharacter(character, bookId);
} catch (error) {
console.error('Failed to save character to local DB:', error);
}
}
return characters;
}
}
/**
* Create character
*/
async createCharacter(
characterData: Omit<CharacterProps, 'id'>,
bookId: string,
createOnServer: () => Promise<string>
): Promise<string> {
if (!window.electron) {
return await createOnServer();
}
if (this.isOffline) {
const localId = crypto.randomUUID();
const character = { ...characterData, id: localId };
await window.electron.dbSaveCharacter(character, bookId);
console.log(`💾 Character "${character.name}" created locally (offline mode)`);
return localId;
} else {
const serverId = await createOnServer();
const character = { ...characterData, id: serverId };
await window.electron.dbSaveCharacter(character, bookId);
return serverId;
}
}
/**
* Save character
*/
async saveCharacter(
character: CharacterProps,
bookId: string,
saveToServer: () => Promise<void>
): Promise<void> {
if (!window.electron) {
return await saveToServer();
}
// Always save to local DB first
await window.electron.dbSaveCharacter(character, bookId);
if (!this.isOffline) {
try {
await saveToServer();
} catch (error) {
console.error('Failed to save to server, will sync later:', error);
}
} else {
console.log(`💾 Character queued for sync (offline mode)`);
}
}
/**
* Get conversations for a book
*/
async getConversations(
bookId: string,
fetchFromServer: () => Promise<Conversation[]>
): Promise<Conversation[]> {
if (!window.electron) {
return await fetchFromServer();
}
if (this.isOffline) {
const result = await window.electron.dbGetConversations(bookId);
if (result.success) {
const titles = result.data?.map((c: any) => c.title).join(', ') || 'none';
console.log(`💬 Conversations from LOCAL DB: ${titles}`);
return result.data || [];
}
throw new Error(result.error || 'Failed to get conversations from local DB');
} else {
const conversations = await fetchFromServer();
const titles = conversations.map(c => c.title).join(', ');
console.log(`💬 Conversations from SERVER: ${titles}`);
// Save to local DB
for (const conversation of conversations) {
try {
await window.electron.dbSaveConversation(conversation, bookId);
} catch (error) {
console.error('Failed to save conversation to local DB:', error);
}
}
return conversations;
}
}
/**
* Create conversation
*/
async createConversation(
conversationData: Omit<Conversation, 'id'>,
bookId: string,
createOnServer: () => Promise<string>
): Promise<string> {
if (!window.electron) {
return await createOnServer();
}
if (this.isOffline) {
const localId = crypto.randomUUID();
const conversation = { ...conversationData, id: localId };
await window.electron.dbSaveConversation(conversation, bookId);
console.log(`💾 Conversation "${conversation.title}" created locally (offline mode)`);
return localId;
} else {
const serverId = await createOnServer();
const conversation = { ...conversationData, id: serverId };
await window.electron.dbSaveConversation(conversation, bookId);
return serverId;
}
}
/**
* Save conversation
*/
async saveConversation(
conversation: Conversation,
bookId: string,
saveToServer: () => Promise<void>
): Promise<void> {
if (!window.electron) {
return await saveToServer();
}
// Always save to local DB first
await window.electron.dbSaveConversation(conversation, bookId);
if (!this.isOffline) {
try {
await saveToServer();
} catch (error) {
console.error('Failed to save to server, will sync later:', error);
}
} else {
console.log(`💾 Conversation queued for sync (offline mode)`);
}
}
/**
* Create chapter
*/
async createChapter(
chapterData: Omit<ChapterProps, 'chapterId'>,
bookId: string,
createOnServer: () => Promise<string>
): Promise<string> {
if (!window.electron) {
return await createOnServer();
}
if (this.isOffline) {
const localId = crypto.randomUUID();
const chapter = { ...chapterData, chapterId: localId };
await window.electron.dbSaveChapter(chapter, bookId);
console.log(`💾 Chapter "${chapter.title}" created locally (offline mode)`);
return localId;
} else {
const serverId = await createOnServer();
const chapter = { ...chapterData, chapterId: serverId };
await window.electron.dbSaveChapter(chapter, bookId);
return serverId;
}
}
/**
* Save chapter content
*/
async saveChapter(
chapter: ChapterProps,
bookId: string,
saveToServer: () => Promise<void>
): Promise<void> {
if (!window.electron) {
return await saveToServer();
}
// Always save to local DB first
await window.electron.dbSaveChapter(chapter, bookId);
if (!this.isOffline) {
try {
await saveToServer();
} catch (error) {
console.error('Failed to save to server, will sync later:', error);
}
} else {
console.log(`💾 Chapter "${chapter.title}" queued for sync (offline mode)`);
}
}
}
// Singleton instance
let offlineDataServiceInstance: OfflineDataService | null = null;
/**
* Get OfflineDataService singleton instance
*/
export function getOfflineDataService(): OfflineDataService {
if (!offlineDataServiceInstance) {
offlineDataServiceInstance = new OfflineDataService();
}
return offlineDataServiceInstance;
}