Initial commit
This commit is contained in:
245
src/composables/useMailSync.ts
Normal file
245
src/composables/useMailSync.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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<Date | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
const sources = ref<SyncSource[]>([]);
|
||||
|
||||
let syncInterval: ReturnType<typeof setInterval> | 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 entities store first (updated by delta), fallback to collections store
|
||||
let signature = entitiesStore.signatures[source.provider]?.[String(source.service)]?.[String(collection)];
|
||||
|
||||
// Fallback to collection signature if not yet synced
|
||||
if (!signature) {
|
||||
const collectionData = collectionsStore.collections[source.provider]?.[String(source.service)]?.[String(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.getDelta(deltaSources);
|
||||
// If fetchDetails is enabled, fetch full entity data for additions and modifications
|
||||
if (fetchDetails) {
|
||||
const fetchPromises: Promise<any>[] = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.getMessages(
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user