| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | import type { Message, Contact, Group } from '@/lib/types'; |
| | import { openDB, IDBPDatabase, DBSchema } from 'idb'; |
| |
|
| | const DB_NAME = 'WhisperLinkDB_v2'; |
| | const DB_VERSION = 4; |
| | const MESSAGES_STORE = 'messages'; |
| | const CONTACTS_STORE = 'contacts'; |
| | const GROUPS_STORE = 'groups'; |
| | const SYNC_STORE = 'syncInfo'; |
| | const MEDIA_CACHE_STORE = 'media-cache'; |
| | const DATA_CACHE_STORE = 'data-cache'; |
| |
|
| | |
| | interface WhisperLinkDBSchema extends DBSchema { |
| | [MESSAGES_STORE]: { |
| | key: string; |
| | value: Message & { chatId: string }; |
| | indexes: { 'chatId': string }; |
| | }; |
| | [CONTACTS_STORE]: { |
| | key: string; |
| | value: Contact; |
| | }; |
| | [GROUPS_STORE]: { |
| | key: string; |
| | value: Group; |
| | }; |
| | [SYNC_STORE]: { |
| | key: string; |
| | value: { chatId: string; timestamp: number }; |
| | }; |
| | [MEDIA_CACHE_STORE]: { |
| | key: string; |
| | value: { |
| | fileKey: string; |
| | blob: Blob; |
| | timestamp: number; |
| | }; |
| | }; |
| | [DATA_CACHE_STORE]: { |
| | key: string; |
| | value: any; |
| | } |
| | } |
| |
|
| | class StorageService { |
| | private isBrowser: boolean; |
| | private dbPromise: Promise<IDBPDatabase<WhisperLinkDBSchema>> | null = null; |
| |
|
| | constructor() { |
| | this.isBrowser = typeof window !== 'undefined'; |
| | } |
| |
|
| | private getDB(): Promise<IDBPDatabase<WhisperLinkDBSchema>> { |
| | if (!this.isBrowser) { |
| | return Promise.reject(new Error("IndexedDB is not available in this environment.")); |
| | } |
| | if (!this.dbPromise) { |
| | this.dbPromise = openDB<WhisperLinkDBSchema>(DB_NAME, DB_VERSION, { |
| | upgrade(db, oldVersion, newVersion, transaction) { |
| | console.log(`Upgrading IndexedDB from version ${oldVersion} to ${newVersion}...`); |
| | |
| | if (!db.objectStoreNames.contains(MESSAGES_STORE)) { |
| | const messageStore = db.createObjectStore(MESSAGES_STORE, { keyPath: 'id' }); |
| | messageStore.createIndex('chatId', 'chatId', { unique: false }); |
| | console.log(`Created object store: ${MESSAGES_STORE} with index 'chatId'.`); |
| | } |
| | if (!db.objectStoreNames.contains(CONTACTS_STORE)) { |
| | db.createObjectStore(CONTACTS_STORE, { keyPath: 'uid' }); |
| | console.log(`Created object store: ${CONTACTS_STORE}.`); |
| | } |
| | if (!db.objectStoreNames.contains(GROUPS_STORE)) { |
| | db.createObjectStore(GROUPS_STORE, { keyPath: 'id' }); |
| | console.log(`Created object store: ${GROUPS_STORE}.`); |
| | } |
| | if (!db.objectStoreNames.contains(SYNC_STORE)) { |
| | db.createObjectStore(SYNC_STORE, { keyPath: 'chatId' }); |
| | console.log(`Created object store: ${SYNC_STORE}.`); |
| | } |
| | if (!db.objectStoreNames.contains(MEDIA_CACHE_STORE)) { |
| | db.createObjectStore(MEDIA_CACHE_STORE, { keyPath: 'fileKey' }); |
| | console.log(`Created object store: ${MEDIA_CACHE_STORE}.`); |
| | } |
| | if (!db.objectStoreNames.contains(DATA_CACHE_STORE)) { |
| | db.createObjectStore(DATA_CACHE_STORE, { keyPath: 'key' }); |
| | console.log(`Created object store: ${DATA_CACHE_STORE}.`); |
| | } |
| | }, |
| | }); |
| | } |
| | return this.dbPromise; |
| | } |
| |
|
| | |
| |
|
| | setItem<T>(key: string, value: T): void { |
| | if (!this.isBrowser) return; |
| | try { |
| | window.localStorage.setItem(key, JSON.stringify(value)); |
| | } catch (error) { |
| | console.error(`Error setting localStorage key "${key}":`, error); |
| | } |
| | } |
| |
|
| | getItem<T>(key: string): T | null { |
| | if (!this.isBrowser) return null; |
| | try { |
| | const item = window.localStorage.getItem(key); |
| | return item ? JSON.parse(item) : null; |
| | } catch (error) { |
| | console.error(`Error reading localStorage key "${key}":`, error); |
| | return null; |
| | } |
| | } |
| |
|
| | removeItem(key: string): void { |
| | if (!this.isBrowser) return; |
| | try { |
| | window.localStorage.removeItem(key); |
| | } catch (error) { |
| | console.error(`Error removing localStorage key "${key}":`, error); |
| | } |
| | } |
| |
|
| | |
| |
|
| | async saveMessages(chatId: string, messages: Message[]): Promise<void> { |
| | if (!this.isBrowser) return; |
| | try { |
| | const db = await this.getDB(); |
| | const tx = db.transaction(MESSAGES_STORE, 'readwrite'); |
| | await Promise.all(messages.map(message => tx.store.put({ ...message, chatId }))); |
| | await tx.done; |
| | } catch (error) { |
| | console.error("Failed to save messages to IndexedDB:", error); |
| | } |
| | } |
| |
|
| | async getMessages(chatId: string): Promise<Message[]> { |
| | if (!this.isBrowser) return []; |
| | const db = await this.getDB(); |
| | const messages = await db.getAllFromIndex(MESSAGES_STORE, 'chatId', chatId); |
| | return messages.sort((a, b) => a.timestamp - b.timestamp); |
| | } |
| |
|
| | async updateMessage(chatId: string, messageId: string, updates: Partial<Message>): Promise<void> { |
| | if (!this.isBrowser) return; |
| | const db = await this.getDB(); |
| | const message = await db.get(MESSAGES_STORE, messageId); |
| | if (message) { |
| | const updatedMessage = { ...message, ...updates }; |
| | await db.put(MESSAGES_STORE, updatedMessage); |
| | } |
| | } |
| | |
| | async deleteMessage(chatId: string, messageId: string): Promise<void> { |
| | if (!this.isBrowser) return; |
| | const db = await this.getDB(); |
| | await db.delete(MESSAGES_STORE, messageId); |
| | } |
| |
|
| |
|
| | async clearMessages(chatId: string): Promise<void> { |
| | if (!this.isBrowser) return; |
| | const db = await this.getDB(); |
| | const tx = db.transaction(MESSAGES_STORE, 'readwrite'); |
| | const index = tx.store.index('chatId'); |
| | for await (const cursor of index.iterate(chatId)) { |
| | await cursor.delete(); |
| | } |
| | await tx.done; |
| | } |
| | |
| | async saveContacts(contacts: Contact[]): Promise<void> { |
| | if (!this.isBrowser) return; |
| | const db = await this.getDB(); |
| | const tx = db.transaction(CONTACTS_STORE, 'readwrite'); |
| | await Promise.all(contacts.map(contact => tx.store.put(contact))); |
| | await tx.done; |
| | } |
| |
|
| | async getContacts(): Promise<Contact[]> { |
| | if (!this.isBrowser) return []; |
| | const db = await this.getDB(); |
| | return await db.getAll(CONTACTS_STORE); |
| | } |
| |
|
| | async saveGroups(groups: Group[]): Promise<void> { |
| | if (!this.isBrowser) return; |
| | const db = await this.getDB(); |
| | const tx = db.transaction(GROUPS_STORE, 'readwrite'); |
| | await Promise.all(groups.map(group => tx.store.put(group))); |
| | await tx.done; |
| | } |
| |
|
| | async getGroups(): Promise<Group[]> { |
| | if (!this.isBrowser) return []; |
| | const db = await this.getDB(); |
| | return await db.getAll(GROUPS_STORE); |
| | } |
| |
|
| |
|
| | |
| |
|
| | async cacheMedia(fileKey: string, blob: Blob): Promise<void> { |
| | if (!this.isBrowser) return; |
| | const db = await this.getDB(); |
| | await db.put(MEDIA_CACHE_STORE, { fileKey, blob, timestamp: Date.now() }); |
| | } |
| |
|
| | async getCachedMedia(fileKey: string): Promise<Blob | null> { |
| | if (!this.isBrowser) return null; |
| | try { |
| | const db = await this.getDB(); |
| | const record = await db.get(MEDIA_CACHE_STORE, fileKey); |
| | return record?.blob || null; |
| | } catch (error) { |
| | console.error(`Failed to get cached media for key ${fileKey}:`, error); |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | |
| | async saveCachedData(key: string, data: any): Promise<void> { |
| | if (!this.isBrowser) return; |
| | try { |
| | const db = await this.getDB(); |
| | await db.put(DATA_CACHE_STORE, { key, data }); |
| | } catch (error) { |
| | console.error(`Failed to save data to cache with key ${key}:`, error); |
| | } |
| | } |
| |
|
| | async getCachedData<T>(key: string): Promise<T | null> { |
| | if (!this.isBrowser) return null; |
| | try { |
| | const db = await this.getDB(); |
| | const result = await db.get(DATA_CACHE_STORE, key); |
| | return result?.data || null; |
| | } catch (error) { |
| | console.error(`Failed to retrieve cached data with key ${key}:`, error); |
| | return null; |
| | } |
| | } |
| | |
| | |
| |
|
| | async updateLastSync(chatId: string, timestamp: number): Promise<void> { |
| | if (!this.isBrowser) return; |
| | const db = await this.getDB(); |
| | await db.put(SYNC_STORE, { chatId, timestamp }); |
| | } |
| |
|
| | async getLastSync(chatId: string): Promise<number> { |
| | if (!this.isBrowser) return 0; |
| | const db = await this.getDB(); |
| | const syncInfo = await db.get(SYNC_STORE, chatId); |
| | return syncInfo?.timestamp || 0; |
| | } |
| | } |
| |
|
| | |
| | export const storageService = new StorageService(); |
| |
|