Initial commit

This commit is contained in:
root
2025-12-21 09:55:58 -05:00
committed by Sebastian Krupinski
commit 169b7b4c91
57 changed files with 10105 additions and 0 deletions

View 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,
};
}