From 3aef72b05c1116945e5fdd6e9b731f9ed9f4318d Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Sat, 20 Jun 2026 00:50:34 -0400 Subject: [PATCH] refactor: mail manager changes Signed-off-by: Sebastian Krupinski --- src/composables/useMailSync.ts | 149 +++++++++++++-------------------- src/stores/entitiesStore.ts | 10 ++- src/types/common.ts | 4 +- src/types/entity.ts | 4 +- 4 files changed, 72 insertions(+), 95 deletions(-) diff --git a/src/composables/useMailSync.ts b/src/composables/useMailSync.ts index 7af5163..3e6ff54 100644 --- a/src/composables/useMailSync.ts +++ b/src/composables/useMailSync.ts @@ -1,6 +1,6 @@ /** * Background mail synchronization composable - * + * * Periodically checks for changes in mailboxes using the delta method */ @@ -8,12 +8,10 @@ import { ref, onMounted, onUnmounted } from 'vue'; import type { Ref } from 'vue'; import { useEntitiesStore } from '../stores/entitiesStore'; import { useCollectionsStore } from '../stores/collectionsStore'; +import type { CollectionIdentifier, EntityIdentifier } from '../types/common'; -export interface SyncSource { - provider: string; - service: string | number; - collections: (string | number)[]; -} +/** A sync source is a collection identifier (provider:service:collection) to monitor */ +export type SyncSource = CollectionIdentifier; interface SyncOptions { /** Polling interval in milliseconds (default: 30000 = 30 seconds) */ @@ -28,9 +26,9 @@ export interface MailSyncController { isRunning: Ref; lastSync: Ref; error: Ref; - sources: Ref; - addSource: (source: SyncSource) => void; - removeSource: (source: SyncSource) => void; + sources: Ref; + addSource: (source: CollectionIdentifier) => void; + removeSource: (source: CollectionIdentifier) => void; clearSources: () => void; sync: () => Promise; start: () => void; @@ -47,26 +45,21 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController { const entitiesStore = useEntitiesStore(); const collectionsStore = useCollectionsStore(); - + const isRunning = ref(false); const lastSync = ref(null); const error = ref(null); - const sources = ref([]); - const signatures = ref>>>({}); - + const sources = ref([]); + // Last known signature per collection identifier (updated by delta) + 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) { + function addSource(source: CollectionIdentifier) { + if (!sources.value.includes(source)) { sources.value.push(source); } } @@ -74,13 +67,8 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController { /** * 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) - ); - + function removeSource(source: CollectionIdentifier) { + const index = sources.value.indexOf(source); if (index !== -1) { sources.value.splice(index, 1); } @@ -104,91 +92,74 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController { 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 || ''; - } + // Build flat identifier list for the delta request, embedding the last known + // signature for each collection as the entity slot (provider:service:collection:signature) + const requestSignatures: Record = {}; + const targets: (CollectionIdentifier | EntityIdentifier)[] = []; - console.log(`[Sync] Collection ${source.provider}/${source.service}/${collection} signature: "${signature}"`); - - // Map collection identifier to signature string - deltaSources[source.provider][source.service][collection] = signature || ''; - }); + sources.value.forEach(collectionId => { + // Look up signature from local tracking (updated by delta), falling back to the + // collection's own signature if it has not been synced yet + let signature = signatures.value[collectionId]; + if (!signature) { + const collectionData = collectionsStore.collection(collectionId); + signature = collectionData?.signature || ''; + } + + requestSignatures[collectionId] = signature; + targets.push(signature ? `${collectionId}:${signature}` as EntityIdentifier : collectionId); }); // Get delta changes - const deltaResponse = await entitiesStore.delta(deltaSources); + const deltaResponse = await entitiesStore.delta(targets); + // 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]) => { + if (providerData === false) { + return; + } Object.entries(providerData).forEach(([service, serviceData]: [string, any]) => { + if (serviceData === false) { + return; + } Object.entries(serviceData).forEach(([collection, collectionData]: [string, any]) => { - // Skip if no changes (server returns false or string signature) + // Skip if no changes (server returns false or a bare signature string) if (collectionData === false || typeof collectionData === 'string') { return; } - + + const collectionId = `${provider}:${service}:${collection}` as CollectionIdentifier; + // 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}"`); + signatures.value[collectionId] = collectionData.signature; } - - // Check if signature actually changed (if not, skip fetching) - const oldSignature = deltaSources[provider]?.[service]?.[collection]; + + // Skip fetching when the signature did not actually change + const oldSignature = requestSignatures[collectionId]; 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 = [ + + const changedIds = [ ...(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 - ) + + if (changedIds.length > 0) { + const entityTargets = changedIds.map( + (id: string | number) => `${collectionId}:${id}` as EntityIdentifier ); + fetchPromises.push(entitiesStore.fetch(entityTargets)); } }); }); }); - + // Fetch all in parallel await Promise.allSettled(fetchPromises); } @@ -209,10 +180,10 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController { } isRunning.value = true; - + // Do initial sync sync(); - + // Set up periodic sync syncInterval = setInterval(() => { sync(); @@ -228,7 +199,7 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController { } isRunning.value = false; - + if (syncInterval) { clearInterval(syncInterval); syncInterval = null; @@ -260,7 +231,7 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController { lastSync, error, sources, - + // Methods addSource, removeSource, diff --git a/src/stores/entitiesStore.ts b/src/stores/entitiesStore.ts index 7462287..e78f9e2 100644 --- a/src/stores/entitiesStore.ts +++ b/src/stores/entitiesStore.ts @@ -173,14 +173,16 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => { /** * Retrieve delta changes for entities * - * @param sources - source selector for delta check - * + * @param targets - collection identifiers (provider:service:collection), optionally + * suffixed with a known signature (provider:service:collection:signature) + * to request a delta relative to that signature + * * @returns Promise with delta changes (additions, modifications, deletions) - * + * * Note: Delta returns only identifiers, not full entities. * Caller should fetch full entities for additions/modifications separately. */ - async function delta(targets: CollectionIdentifier[]) { + async function delta(targets: (CollectionIdentifier | EntityIdentifier)[]) { transceiving.value = true try { const response = await entityService.delta({ targets }) diff --git a/src/types/common.ts b/src/types/common.ts index 1691d7e..1894974 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -87,7 +87,9 @@ export type ApiStreamResponse = /** * Identifiers for targeting specific providers, services, collections, or entities in list or extant operations. * - + * Operations accept flat arrays of colon-separated identifier strings, e.g. + * ["imap:account1:INBOX", "imap:account1:Sent:1001"]. + */ export type ProviderIdentifier = `${string}`; export type ServiceIdentifier = `${string}:${string}`; export type CollectionIdentifier = `${string}:${string}:${string | number}`; diff --git a/src/types/entity.ts b/src/types/entity.ts index f234c94..3cdadd7 100644 --- a/src/types/entity.ts +++ b/src/types/entity.ts @@ -92,7 +92,9 @@ export interface EntityExtantResponse { * Entity delta */ export interface EntityDeltaRequest { - targets: CollectionIdentifier[]; + // Each target is provider:service:collection, or provider:service:collection:signature + // to request a delta relative to a known signature (the signature is the entity slot). + targets: (CollectionIdentifier | EntityIdentifier)[]; } export interface EntityDeltaResponse {