Spaces:
Running
Running
Download src/services/sync-service.js from Luca448/APP-Backend: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/spaces/Luca448/APP-Backend/resolve/main/src/services/sync-service.js
- Command line
-
hf download hf://spaces/Luca448/APP-Backend/src/services/sync-service.js
-
curl -L -o sync-service.js https://huggingface.co/spaces/Luca448/APP-Backend/resolve/main/src/services/sync-service.js
14.4 kB
| import { supabase } from './supabase-client.js'; | |
| import { authService } from './auth-service.js'; | |
| import { cryptoService } from './crypto-service.js'; | |
| import { db } from '../db/database.js'; | |
| class SyncManager { | |
| constructor() { | |
| this.isSyncing = false; | |
| this.lastSyncTime = 0; | |
| this.lastUploadedSettings = {}; // Cache to prevent redundant uploads | |
| // Listen for auth state changes | |
| authService.onAuthStateChange(async (event, session) => { | |
| if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') { | |
| // If we log in and the crypto is unlocked, we can trigger sync | |
| if (authService.isCryptoUnlocked()) { | |
| this.sync(); | |
| } | |
| } | |
| }); | |
| } | |
| async initialize() { | |
| // Called once on app startup to trigger initial sync if already logged in | |
| if (authService.user && authService.isCryptoUnlocked()) { | |
| this.sync(); | |
| // Auto-Sync every 15 seconds | |
| setInterval(() => { | |
| if (authService.user && authService.isCryptoUnlocked()) { | |
| this.sync(); | |
| } | |
| }, 15000); | |
| } | |
| } | |
| async sync() { | |
| if (!authService.user || !authService.isCryptoUnlocked() || this.isSyncing) return; | |
| this.isSyncing = true; | |
| const userId = authService.user.id; | |
| console.log('[Sync] Starting Sync...'); | |
| try { | |
| let hasUpdates = false; | |
| if (await this.syncSettings(userId)) hasUpdates = true; | |
| if (await this.syncConversations(userId)) hasUpdates = true; | |
| if (await this.syncMessages(userId)) hasUpdates = true; | |
| if (await this.syncFlashcardDecks(userId)) hasUpdates = true; | |
| if (await this.syncFlashcards(userId)) hasUpdates = true; | |
| console.log('[Sync] Sync completed successfully.'); | |
| if (hasUpdates) { | |
| console.log('[Sync] New remote changes found, dispatching event.'); | |
| window.dispatchEvent(new CustomEvent('cloud_sync_updated')); | |
| } | |
| this.lastSyncTime = Date.now(); | |
| } catch (e) { | |
| console.error('[Sync] Error during sync:', e); | |
| } finally { | |
| this.isSyncing = false; | |
| } | |
| } | |
| async syncSettings(userId) { | |
| // Local to Remote | |
| const localSettings = db.exec("SELECT key, value FROM settings"); | |
| // Remote to Local | |
| const { data: remoteSettings, error } = await supabase | |
| .from('settings') | |
| .select('*') | |
| .eq('user_id', userId); | |
| if (error) throw error; | |
| const remoteSettingsMap = {}; | |
| if (remoteSettings) { | |
| for (const row of remoteSettings) { | |
| remoteSettingsMap[row.key] = await cryptoService.decrypt(row.value); | |
| } | |
| } | |
| // Check if local settings is effectively empty (only defaults, no api keys) | |
| const hasLocalApiKey = localSettings.some(r => r.key.includes('api_key') && r.value && r.value.trim().length > 0); | |
| const hasRemoteApiKey = Object.keys(remoteSettingsMap).some(k => k.includes('api_key') && remoteSettingsMap[k] && remoteSettingsMap[k].trim().length > 0); | |
| // If local has no API keys, but remote has some, we assume this is a NEW login on a new device. | |
| // In this case, we DO NOT push our local defaults to remote. We just pull from remote. | |
| if (!hasLocalApiKey && hasRemoteApiKey) { | |
| for (const key of Object.keys(remoteSettingsMap)) { | |
| if (remoteSettingsMap[key]) { | |
| db.setSetting(key, remoteSettingsMap[key]); | |
| } | |
| } | |
| return; // Skip pushing to remote | |
| } | |
| // Otherwise, push local to remote | |
| for (const row of localSettings) { | |
| if (this.lastUploadedSettings[row.key] === row.value) continue; // Skip redundant uploads | |
| const encryptedValue = await cryptoService.encrypt(row.value); | |
| await supabase.from('settings').upsert({ | |
| user_id: userId, | |
| key: row.key, | |
| value: encryptedValue, | |
| updated_at: new Date().toISOString() | |
| }, { onConflict: 'user_id, key' }); | |
| this.lastUploadedSettings[row.key] = row.value; // Cache it | |
| } | |
| let updated = false; | |
| // Pull any setting from remote that isn't locally set (e.g. from another device) | |
| if (remoteSettings) { | |
| for (const row of remoteSettings) { | |
| const localVal = db.getSetting(row.key); | |
| if (!localVal) { | |
| const decrypted = await cryptoService.decrypt(row.value); | |
| if (decrypted) { | |
| db.setSetting(row.key, decrypted); | |
| updated = true; | |
| } | |
| } | |
| } | |
| } | |
| return updated; | |
| } | |
| async syncConversations(userId) { | |
| const localConvs = db.getConversations(); | |
| const { data: remoteConvs, error } = await supabase | |
| .from('conversations') | |
| .select('*') | |
| .eq('user_id', userId); | |
| if (error) throw error; | |
| // Create map for fast lookup | |
| const remoteMap = {}; | |
| if (remoteConvs) { | |
| for (const c of remoteConvs) remoteMap[c.id] = c; | |
| } | |
| for (const local of localConvs) { | |
| const remote = remoteMap[local.id]; | |
| const localTime = new Date(local.updated_at).getTime(); | |
| const remoteTime = remote ? new Date(remote.updated_at).getTime() : 0; | |
| // Push to remote if local is newer or doesn't exist on remote | |
| if (!remote || localTime > remoteTime) { | |
| await supabase.from('conversations').upsert({ | |
| id: local.id, | |
| user_id: userId, | |
| title: await cryptoService.encrypt(local.title), | |
| emoji: await cryptoService.encrypt(local.emoji), | |
| preview: await cryptoService.encrypt(local.preview), | |
| model_used: local.model_used, | |
| created_at: local.created_at, | |
| updated_at: local.updated_at | |
| }, { onConflict: 'id, user_id' }); | |
| } | |
| } | |
| let updated = false; | |
| // Pull from remote if remote is newer or doesn't exist locally | |
| if (remoteConvs) { | |
| for (const remote of remoteConvs) { | |
| const local = localConvs.find(c => c.id === remote.id); | |
| const remoteTime = new Date(remote.updated_at).getTime(); | |
| const localTime = local ? new Date(local.updated_at).getTime() : 0; | |
| if (!local || remoteTime > localTime) { | |
| const title = await cryptoService.decrypt(remote.title); | |
| const emoji = await cryptoService.decrypt(remote.emoji); | |
| const preview = await cryptoService.decrypt(remote.preview); | |
| if (!local) { | |
| db.exec( | |
| "INSERT INTO conversations (id, title, emoji, preview, model_used, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)", | |
| [remote.id, title, emoji, preview, remote.model_used, remote.created_at, remote.updated_at] | |
| ); | |
| } else { | |
| db.exec( | |
| "UPDATE conversations SET title = ?, emoji = ?, preview = ?, model_used = ?, updated_at = ? WHERE id = ?", | |
| [title, emoji, preview, remote.model_used, remote.updated_at, remote.id] | |
| ); | |
| } | |
| updated = true; | |
| } | |
| } | |
| } | |
| return updated; | |
| } | |
| async syncMessages(userId) { | |
| const localMsgs = db.exec("SELECT * FROM messages"); | |
| // Because messages can be large, we might only want to fetch IDs to see what's missing | |
| const { data: remoteMsgs, error } = await supabase | |
| .from('messages') | |
| .select('id, created_at') | |
| .eq('user_id', userId); | |
| if (error) throw error; | |
| const remoteIds = new Set((remoteMsgs || []).map(m => m.id)); | |
| const localIds = new Set(localMsgs.map(m => m.id)); | |
| // Push local to remote | |
| for (const local of localMsgs) { | |
| if (!remoteIds.has(local.id)) { | |
| await supabase.from('messages').upsert({ | |
| id: local.id, | |
| conversation_id: local.conversation_id, | |
| user_id: userId, | |
| role: local.role, | |
| content: await cryptoService.encrypt(local.content), | |
| model_used: local.model_used, | |
| style_used: local.style_used, | |
| created_at: local.created_at | |
| }, { onConflict: 'id, user_id' }); | |
| } | |
| } | |
| let updated = false; | |
| // Pull remote to local | |
| for (const remote of (remoteMsgs || [])) { | |
| if (!localIds.has(remote.id)) { | |
| const { data: remoteFull } = await supabase | |
| .from('messages') | |
| .select('*') | |
| .eq('id', remote.id) | |
| .single(); | |
| if (remoteFull) { | |
| const content = await cryptoService.decrypt(remoteFull.content); | |
| db.exec( | |
| "INSERT INTO messages (id, conversation_id, role, content, model_used, style_used, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", | |
| [remoteFull.id, remoteFull.conversation_id, remoteFull.role, content, remoteFull.model_used, remoteFull.style_used, remoteFull.created_at] | |
| ); | |
| updated = true; | |
| } | |
| } | |
| } | |
| return updated; | |
| } | |
| async syncFlashcardDecks(userId) { | |
| const localDecks = db.getDecks(); | |
| const { data: remoteDecks, error } = await supabase | |
| .from('flashcard_decks') | |
| .select('*') | |
| .eq('user_id', userId); | |
| if (error) { | |
| console.error("[Sync] Error fetching decks", error); | |
| throw error; | |
| } | |
| const remoteMap = {}; | |
| if (remoteDecks) { | |
| for (const c of remoteDecks) remoteMap[c.id] = c; | |
| } | |
| for (const local of localDecks) { | |
| const remote = remoteMap[local.id]; | |
| const localTime = new Date(local.updated_at).getTime(); | |
| const remoteTime = remote ? new Date(remote.updated_at).getTime() : 0; | |
| if (!remote || localTime > remoteTime) { | |
| await supabase.from('flashcard_decks').upsert({ | |
| id: local.id, | |
| user_id: userId, | |
| title: await cryptoService.encrypt(local.title), | |
| description: await cryptoService.encrypt(local.description || ''), | |
| color: local.color, | |
| source_lang: local.source_lang, | |
| target_lang: local.target_lang, | |
| created_at: local.created_at, | |
| updated_at: local.updated_at | |
| }, { onConflict: 'id, user_id' }); | |
| } | |
| } | |
| let updated = false; | |
| if (remoteDecks) { | |
| for (const remote of remoteDecks) { | |
| const local = localDecks.find(c => c.id === remote.id); | |
| const remoteTime = new Date(remote.updated_at).getTime(); | |
| const localTime = local ? new Date(local.updated_at).getTime() : 0; | |
| if (!local || remoteTime > localTime) { | |
| const title = await cryptoService.decrypt(remote.title); | |
| const description = await cryptoService.decrypt(remote.description); | |
| if (!local) { | |
| db.exec( | |
| "INSERT INTO flashcard_decks (id, title, description, color, source_lang, target_lang, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", | |
| [remote.id, title, description, remote.color, remote.source_lang, remote.target_lang, remote.created_at, remote.updated_at] | |
| ); | |
| } else { | |
| db.exec( | |
| "UPDATE flashcard_decks SET title = ?, description = ?, color = ?, source_lang = ?, target_lang = ?, updated_at = ? WHERE id = ?", | |
| [title, description, remote.color, remote.source_lang, remote.target_lang, remote.updated_at, remote.id] | |
| ); | |
| } | |
| updated = true; | |
| } | |
| } | |
| } | |
| return updated; | |
| } | |
| async syncFlashcards(userId) { | |
| const localCards = db.exec("SELECT * FROM flashcards"); | |
| const { data: remoteCardsRaw, error } = await supabase | |
| .from('flashcards') | |
| .select('id, updated_at') | |
| .eq('user_id', userId); | |
| if (error) { | |
| console.error("[Sync] Error fetching flashcards", error); | |
| throw error; | |
| } | |
| const remoteMapRaw = {}; | |
| for (const r of (remoteCardsRaw || [])) { | |
| remoteMapRaw[r.id] = r; | |
| } | |
| for (const local of localCards) { | |
| const remoteRaw = remoteMapRaw[local.id]; | |
| const localTime = new Date(local.updated_at).getTime(); | |
| const remoteTime = remoteRaw ? new Date(remoteRaw.updated_at).getTime() : 0; | |
| if (!remoteRaw || localTime > remoteTime) { | |
| await supabase.from('flashcards').upsert({ | |
| id: local.id, | |
| user_id: userId, | |
| deck_id: local.deck_id, | |
| front: await cryptoService.encrypt(local.front), | |
| back: await cryptoService.encrypt(local.back), | |
| image_url: local.image_url ? await cryptoService.encrypt(local.image_url) : null, | |
| next_review: local.next_review, | |
| ease_factor: local.ease_factor, | |
| interval: local.interval, | |
| repetitions: local.repetitions, | |
| created_at: local.created_at, | |
| updated_at: local.updated_at | |
| }, { onConflict: 'id, user_id' }); | |
| } | |
| } | |
| let updated = false; | |
| for (const remoteRaw of (remoteCardsRaw || [])) { | |
| const local = localCards.find(c => c.id === remoteRaw.id); | |
| const remoteTime = new Date(remoteRaw.updated_at).getTime(); | |
| const localTime = local ? new Date(local.updated_at).getTime() : 0; | |
| if (!local || remoteTime > localTime) { | |
| const { data: remoteFull } = await supabase | |
| .from('flashcards') | |
| .select('*') | |
| .eq('id', remoteRaw.id) | |
| .single(); | |
| if (remoteFull) { | |
| const front = await cryptoService.decrypt(remoteFull.front); | |
| const back = await cryptoService.decrypt(remoteFull.back); | |
| const image_url = remoteFull.image_url ? await cryptoService.decrypt(remoteFull.image_url) : null; | |
| if (!local) { | |
| db.exec( | |
| "INSERT INTO flashcards (id, deck_id, front, back, image_url, next_review, ease_factor, interval, repetitions, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | |
| [remoteFull.id, remoteFull.deck_id, front, back, image_url, remoteFull.next_review, remoteFull.ease_factor, remoteFull.interval, remoteFull.repetitions, remoteFull.created_at, remoteFull.updated_at] | |
| ); | |
| } else { | |
| db.exec( | |
| "UPDATE flashcards SET front = ?, back = ?, image_url = ?, next_review = ?, ease_factor = ?, interval = ?, repetitions = ?, updated_at = ? WHERE id = ?", | |
| [front, back, image_url, remoteFull.next_review, remoteFull.ease_factor, remoteFull.interval, remoteFull.repetitions, remoteFull.updated_at, remoteFull.id] | |
| ); | |
| } | |
| updated = true; | |
| } | |
| } | |
| } | |
| return updated; | |
| } | |
| } | |
| export const syncManager = new SyncManager(); | |