/** * Background mail synchronization composable * * Periodically checks for changes in mailboxes using the delta method */ import { ref, onMounted, onUnmounted } from 'vue'; import { useEntitiesStore } from '../stores/entitiesStore'; import { useCollectionsStore } from '../stores/collectionsStore'; interface SyncSource { provider: string; service: string | number; collections: (string | number)[]; } interface SyncOptions { /** Polling interval in milliseconds (default: 30000 = 30 seconds) */ interval?: number; /** Auto-start sync on mount (default: true) */ autoStart?: boolean; /** Fetch full entity details after delta (default: true) */ fetchDetails?: boolean; } export function useMailSync(options: SyncOptions = {}) { const { interval = 30000, autoStart = true, fetchDetails = true, } = options; const entitiesStore = useEntitiesStore(); const collectionsStore = useCollectionsStore(); const isRunning = ref(false); const lastSync = ref(null); const error = ref(null); const sources = ref([]); const signatures = ref>>>({}); let syncInterval: ReturnType | null = null; /** * Add a source to sync (mailbox to monitor) */ function addSource(source: SyncSource) { const exists = sources.value.some( s => s.provider === source.provider && s.service === source.service && JSON.stringify(s.collections) === JSON.stringify(source.collections) ); if (!exists) { sources.value.push(source); } } /** * Remove a source from sync */ function removeSource(source: SyncSource) { const index = sources.value.findIndex( s => s.provider === source.provider && s.service === source.service && JSON.stringify(s.collections) === JSON.stringify(source.collections) ); if (index !== -1) { sources.value.splice(index, 1); } } /** * Clear all sources */ function clearSources() { sources.value = []; } /** * Perform a single sync check */ async function sync() { if (sources.value.length === 0) { return; } try { error.value = null; // Build sources structure for delta request const deltaSources: any = {}; sources.value.forEach(source => { if (!deltaSources[source.provider]) { deltaSources[source.provider] = {}; } if (!deltaSources[source.provider][source.service]) { deltaSources[source.provider][source.service] = {}; } // Add collections to check with their signatures source.collections.forEach(collection => { // Look up signature from local tracking (updated by delta) let signature = signatures.value[source.provider]?.[String(source.service)]?.[String(collection)]; // Fallback to collection signature if not yet synced if (!signature) { const collectionData = collectionsStore.collection(source.provider, source.service, collection); signature = collectionData?.signature || ''; } console.log(`[Sync] Collection ${source.provider}/${source.service}/${collection} signature: "${signature}"`); // Map collection identifier to signature string deltaSources[source.provider][source.service][collection] = signature || ''; }); }); // Get delta changes const deltaResponse = await entitiesStore.delta(deltaSources); // If fetchDetails is enabled, fetch full entity data for additions and modifications if (fetchDetails) { const fetchPromises: Promise[] = []; Object.entries(deltaResponse).forEach(([provider, providerData]: [string, any]) => { Object.entries(providerData).forEach(([service, serviceData]: [string, any]) => { Object.entries(serviceData).forEach(([collection, collectionData]: [string, any]) => { // Skip if no changes (server returns false or string signature) if (collectionData === false || typeof collectionData === 'string') { return; } // Update signature tracking if (collectionData.signature) { if (!signatures.value[provider]) { signatures.value[provider] = {}; } if (!signatures.value[provider][service]) { signatures.value[provider][service] = {}; } signatures.value[provider][service][collection] = collectionData.signature; console.log(`[Sync] Updated signature for ${provider}/${service}/${collection}: "${collectionData.signature}"`); } // Check if signature actually changed (if not, skip fetching) const oldSignature = deltaSources[provider]?.[service]?.[collection]; const newSignature = collectionData.signature; if (oldSignature && newSignature && oldSignature === newSignature) { // Signature unchanged - server bug returning additions anyway, skip fetch console.log(`[Sync] Skipping fetch for ${provider}/${service}/${collection} - signature unchanged (${newSignature})`); return; } const identifiersToFetch = [ ...(collectionData.additions || []), ...(collectionData.modifications || []), ]; if (identifiersToFetch.length > 0) { console.log(`[Sync] Fetching ${identifiersToFetch.length} entities for ${provider}/${service}/${collection}`); fetchPromises.push( entitiesStore.fetch( provider, service, collection, identifiersToFetch ) ); } }); }); }); // Fetch all in parallel await Promise.allSettled(fetchPromises); } lastSync.value = new Date(); } catch (err: any) { error.value = err.message || 'Sync failed'; console.error('Mail sync error:', err); } } /** * Start the background sync worker */ function start() { if (isRunning.value) { return; } isRunning.value = true; // Do initial sync sync(); // Set up periodic sync syncInterval = setInterval(() => { sync(); }, interval); } /** * Stop the background sync worker */ function stop() { if (!isRunning.value) { return; } isRunning.value = false; if (syncInterval) { clearInterval(syncInterval); syncInterval = null; } } /** * Restart the sync worker */ function restart() { stop(); start(); } // Auto-start/stop on component lifecycle onMounted(() => { if (autoStart && sources.value.length > 0) { start(); } }); onUnmounted(() => { stop(); }); return { // State isRunning, lastSync, error, sources, // Methods addSource, removeSource, clearSources, sync, start, stop, restart, }; }