259 lines
7.3 KiB
TypeScript
259 lines
7.3 KiB
TypeScript
/**
|
|
* 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[]>([]);
|
|
const signatures = ref<Record<string, Record<string, Record<string, string>>>>({});
|
|
|
|
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 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<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;
|
|
}
|
|
|
|
// 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,
|
|
};
|
|
}
|