refactor: mail manager changes
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -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<boolean>;
|
||||
lastSync: Ref<Date | null>;
|
||||
error: Ref<string | null>;
|
||||
sources: Ref<SyncSource[]>;
|
||||
addSource: (source: SyncSource) => void;
|
||||
removeSource: (source: SyncSource) => void;
|
||||
sources: Ref<CollectionIdentifier[]>;
|
||||
addSource: (source: CollectionIdentifier) => void;
|
||||
removeSource: (source: CollectionIdentifier) => void;
|
||||
clearSources: () => void;
|
||||
sync: () => Promise<void>;
|
||||
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<Date | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
const sources = ref<SyncSource[]>([]);
|
||||
const signatures = ref<Record<string, Record<string, Record<string, string>>>>({});
|
||||
|
||||
const sources = ref<CollectionIdentifier[]>([]);
|
||||
// Last known signature per collection identifier (updated by delta)
|
||||
const signatures = ref<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) {
|
||||
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<string, string> = {};
|
||||
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<any>[] = [];
|
||||
|
||||
|
||||
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,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
+3
-1
@@ -87,7 +87,9 @@ export type ApiStreamResponse<T = any> =
|
||||
/**
|
||||
* 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}`;
|
||||
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user