refactor: mail manager changes
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -8,12 +8,10 @@ import { ref, onMounted, onUnmounted } from 'vue';
|
|||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { useEntitiesStore } from '../stores/entitiesStore';
|
import { useEntitiesStore } from '../stores/entitiesStore';
|
||||||
import { useCollectionsStore } from '../stores/collectionsStore';
|
import { useCollectionsStore } from '../stores/collectionsStore';
|
||||||
|
import type { CollectionIdentifier, EntityIdentifier } from '../types/common';
|
||||||
|
|
||||||
export interface SyncSource {
|
/** A sync source is a collection identifier (provider:service:collection) to monitor */
|
||||||
provider: string;
|
export type SyncSource = CollectionIdentifier;
|
||||||
service: string | number;
|
|
||||||
collections: (string | number)[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SyncOptions {
|
interface SyncOptions {
|
||||||
/** Polling interval in milliseconds (default: 30000 = 30 seconds) */
|
/** Polling interval in milliseconds (default: 30000 = 30 seconds) */
|
||||||
@@ -28,9 +26,9 @@ export interface MailSyncController {
|
|||||||
isRunning: Ref<boolean>;
|
isRunning: Ref<boolean>;
|
||||||
lastSync: Ref<Date | null>;
|
lastSync: Ref<Date | null>;
|
||||||
error: Ref<string | null>;
|
error: Ref<string | null>;
|
||||||
sources: Ref<SyncSource[]>;
|
sources: Ref<CollectionIdentifier[]>;
|
||||||
addSource: (source: SyncSource) => void;
|
addSource: (source: CollectionIdentifier) => void;
|
||||||
removeSource: (source: SyncSource) => void;
|
removeSource: (source: CollectionIdentifier) => void;
|
||||||
clearSources: () => void;
|
clearSources: () => void;
|
||||||
sync: () => Promise<void>;
|
sync: () => Promise<void>;
|
||||||
start: () => void;
|
start: () => void;
|
||||||
@@ -51,22 +49,17 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
const isRunning = ref(false);
|
const isRunning = ref(false);
|
||||||
const lastSync = ref<Date | null>(null);
|
const lastSync = ref<Date | null>(null);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const sources = ref<SyncSource[]>([]);
|
const sources = ref<CollectionIdentifier[]>([]);
|
||||||
const signatures = ref<Record<string, Record<string, Record<string, string>>>>({});
|
// Last known signature per collection identifier (updated by delta)
|
||||||
|
const signatures = ref<Record<string, string>>({});
|
||||||
|
|
||||||
let syncInterval: ReturnType<typeof setInterval> | null = null;
|
let syncInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a source to sync (mailbox to monitor)
|
* Add a source to sync (mailbox to monitor)
|
||||||
*/
|
*/
|
||||||
function addSource(source: SyncSource) {
|
function addSource(source: CollectionIdentifier) {
|
||||||
const exists = sources.value.some(
|
if (!sources.value.includes(source)) {
|
||||||
s => s.provider === source.provider
|
|
||||||
&& s.service === source.service
|
|
||||||
&& JSON.stringify(s.collections) === JSON.stringify(source.collections)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
sources.value.push(source);
|
sources.value.push(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,13 +67,8 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
/**
|
/**
|
||||||
* Remove a source from sync
|
* Remove a source from sync
|
||||||
*/
|
*/
|
||||||
function removeSource(source: SyncSource) {
|
function removeSource(source: CollectionIdentifier) {
|
||||||
const index = sources.value.findIndex(
|
const index = sources.value.indexOf(source);
|
||||||
s => s.provider === source.provider
|
|
||||||
&& s.service === source.service
|
|
||||||
&& JSON.stringify(s.collections) === JSON.stringify(source.collections)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
sources.value.splice(index, 1);
|
sources.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -104,86 +92,69 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
try {
|
try {
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
// Build sources structure for delta request
|
// Build flat identifier list for the delta request, embedding the last known
|
||||||
const deltaSources: any = {};
|
// signature for each collection as the entity slot (provider:service:collection:signature)
|
||||||
|
const requestSignatures: Record<string, string> = {};
|
||||||
|
const targets: (CollectionIdentifier | EntityIdentifier)[] = [];
|
||||||
|
|
||||||
sources.value.forEach(source => {
|
sources.value.forEach(collectionId => {
|
||||||
if (!deltaSources[source.provider]) {
|
// Look up signature from local tracking (updated by delta), falling back to the
|
||||||
deltaSources[source.provider] = {};
|
// collection's own signature if it has not been synced yet
|
||||||
}
|
let signature = signatures.value[collectionId];
|
||||||
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) {
|
if (!signature) {
|
||||||
const collectionData = collectionsStore.collection(source.provider, source.service, collection);
|
const collectionData = collectionsStore.collection(collectionId);
|
||||||
signature = collectionData?.signature || '';
|
signature = collectionData?.signature || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Sync] Collection ${source.provider}/${source.service}/${collection} signature: "${signature}"`);
|
requestSignatures[collectionId] = signature;
|
||||||
|
targets.push(signature ? `${collectionId}:${signature}` as EntityIdentifier : collectionId);
|
||||||
// Map collection identifier to signature string
|
|
||||||
deltaSources[source.provider][source.service][collection] = signature || '';
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get delta changes
|
// 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 is enabled, fetch full entity data for additions and modifications
|
||||||
if (fetchDetails) {
|
if (fetchDetails) {
|
||||||
const fetchPromises: Promise<any>[] = [];
|
const fetchPromises: Promise<any>[] = [];
|
||||||
|
|
||||||
Object.entries(deltaResponse).forEach(([provider, providerData]: [string, any]) => {
|
Object.entries(deltaResponse).forEach(([provider, providerData]: [string, any]) => {
|
||||||
|
if (providerData === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Object.entries(providerData).forEach(([service, serviceData]: [string, any]) => {
|
Object.entries(providerData).forEach(([service, serviceData]: [string, any]) => {
|
||||||
|
if (serviceData === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Object.entries(serviceData).forEach(([collection, collectionData]: [string, any]) => {
|
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') {
|
if (collectionData === false || typeof collectionData === 'string') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collectionId = `${provider}:${service}:${collection}` as CollectionIdentifier;
|
||||||
|
|
||||||
// Update signature tracking
|
// Update signature tracking
|
||||||
if (collectionData.signature) {
|
if (collectionData.signature) {
|
||||||
if (!signatures.value[provider]) {
|
signatures.value[collectionId] = collectionData.signature;
|
||||||
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)
|
// Skip fetching when the signature did not actually change
|
||||||
const oldSignature = deltaSources[provider]?.[service]?.[collection];
|
const oldSignature = requestSignatures[collectionId];
|
||||||
const newSignature = collectionData.signature;
|
const newSignature = collectionData.signature;
|
||||||
|
|
||||||
if (oldSignature && newSignature && oldSignature === newSignature) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const identifiersToFetch = [
|
const changedIds = [
|
||||||
...(collectionData.additions || []),
|
...(collectionData.additions || []),
|
||||||
...(collectionData.modifications || []),
|
...(collectionData.modifications || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (identifiersToFetch.length > 0) {
|
if (changedIds.length > 0) {
|
||||||
console.log(`[Sync] Fetching ${identifiersToFetch.length} entities for ${provider}/${service}/${collection}`);
|
const entityTargets = changedIds.map(
|
||||||
fetchPromises.push(
|
(id: string | number) => `${collectionId}:${id}` as EntityIdentifier
|
||||||
entitiesStore.fetch(
|
|
||||||
provider,
|
|
||||||
service,
|
|
||||||
collection,
|
|
||||||
identifiersToFetch
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
fetchPromises.push(entitiesStore.fetch(entityTargets));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -173,14 +173,16 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve delta changes for entities
|
* 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)
|
* @returns Promise with delta changes (additions, modifications, deletions)
|
||||||
*
|
*
|
||||||
* Note: Delta returns only identifiers, not full entities.
|
* Note: Delta returns only identifiers, not full entities.
|
||||||
* Caller should fetch full entities for additions/modifications separately.
|
* Caller should fetch full entities for additions/modifications separately.
|
||||||
*/
|
*/
|
||||||
async function delta(targets: CollectionIdentifier[]) {
|
async function delta(targets: (CollectionIdentifier | EntityIdentifier)[]) {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.delta({ targets })
|
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.
|
* 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 ProviderIdentifier = `${string}`;
|
||||||
export type ServiceIdentifier = `${string}:${string}`;
|
export type ServiceIdentifier = `${string}:${string}`;
|
||||||
export type CollectionIdentifier = `${string}:${string}:${string | number}`;
|
export type CollectionIdentifier = `${string}:${string}:${string | number}`;
|
||||||
|
|||||||
+3
-1
@@ -92,7 +92,9 @@ export interface EntityExtantResponse {
|
|||||||
* Entity delta
|
* Entity delta
|
||||||
*/
|
*/
|
||||||
export interface EntityDeltaRequest {
|
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 {
|
export interface EntityDeltaResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user