diff --git a/src/composables/useMailSync.ts b/src/composables/useMailSync.ts index 3e086d9..7af5163 100644 --- a/src/composables/useMailSync.ts +++ b/src/composables/useMailSync.ts @@ -5,10 +5,11 @@ */ import { ref, onMounted, onUnmounted } from 'vue'; +import type { Ref } from 'vue'; import { useEntitiesStore } from '../stores/entitiesStore'; import { useCollectionsStore } from '../stores/collectionsStore'; -interface SyncSource { +export interface SyncSource { provider: string; service: string | number; collections: (string | number)[]; @@ -23,7 +24,21 @@ interface SyncOptions { fetchDetails?: boolean; } -export function useMailSync(options: SyncOptions = {}) { +export interface MailSyncController { + isRunning: Ref; + lastSync: Ref; + error: Ref; + sources: Ref; + addSource: (source: SyncSource) => void; + removeSource: (source: SyncSource) => void; + clearSources: () => void; + sync: () => Promise; + start: () => void; + stop: () => void; + restart: () => void; +} + +export function useMailSync(options: SyncOptions = {}): MailSyncController { const { interval = 30000, autoStart = true, diff --git a/src/stores/collectionsStore.ts b/src/stores/collectionsStore.ts index 4071292..8d361d9 100644 --- a/src/stores/collectionsStore.ts +++ b/src/stores/collectionsStore.ts @@ -9,8 +9,13 @@ import { CollectionObject, CollectionPropertiesObject } from '../models/collecti import type { SourceSelector, ListFilter, ListSort } from '../types' export const useCollectionsStore = defineStore('mailCollectionsStore', () => { + const ROOT_IDENTIFIER = '__root__' + const SERVICE_INDEX_IDENTIFIER = '__service__' + // State const _collections = ref>({}) + const _collectionsByServiceIndex = ref>({}) + const _collectionsByParentIndex = ref>({}) const transceiving = ref(false) /** @@ -33,13 +38,20 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { */ const collectionsByService = computed(() => { const groups: Record = {} - - Object.values(_collections.value).forEach((collection) => { - const serviceKey = `${collection.provider}:${collection.service}` - if (!groups[serviceKey]) { - groups[serviceKey] = [] + + Object.keys(_collectionsByServiceIndex.value).forEach(serviceIndexKey => { + const collectionKeys = _collectionsByServiceIndex.value[serviceIndexKey] ?? [] + const collectionsForKey = collectionKeys + .map(collectionKey => _collections.value[collectionKey]) + .filter((collection): collection is CollectionObject => collection !== undefined) + + if (collectionsForKey.length === 0) { + return } - groups[serviceKey].push(collection) + + const firstCollection = collectionsForKey[0] + const serviceKey = `${firstCollection.provider}:${firstCollection.service}` + groups[serviceKey] = collectionsForKey }) return groups @@ -75,10 +87,9 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { * @returns Array of collection objects */ function collectionsForService(provider: string, service: string | number, retrieve: boolean = false): CollectionObject[] { - const serviceKeyPrefix = `${provider}:${service}:` - const serviceCollections = Object.entries(_collections.value) - .filter(([key]) => key.startsWith(serviceKeyPrefix)) - .map(([_, collection]) => collection) + const serviceCollections = collectionObjectsForKeys( + _collectionsByServiceIndex.value[identifierKey(provider, service, SERVICE_INDEX_IDENTIFIER)] ?? [], + ) if (retrieve === true && serviceCollections.length === 0) { console.debug(`[Mail Manager][Store] - Force fetching collections for service "${provider}:${service}"`) @@ -93,19 +104,26 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { return serviceCollections } - function collectionsInCollection(provider: string, service: string | number, collectionId: string | number, retrieve: boolean = false): CollectionObject[] { - const collectionKeyPrefix = `${provider}:${service}:${collectionId}:` - const nestedCollections = Object.entries(_collections.value) - .filter(([key]) => key.startsWith(collectionKeyPrefix)) - .map(([_, collection]) => collection) + /** + * Get direct child collections for a parent collection, or root collections when parent is null. + * + * @param provider - provider identifier + * @param service - service identifier + * @param collectionId - parent collection identifier, or null for root-level collections + * @param retrieve - Retrieve behavior: true = fetch service collections if missing, false = cache only + * + * @returns Array of direct child collection objects + */ + function collectionsInCollection(provider: string, service: string | number, collectionId: string | number | null, retrieve: boolean = false): CollectionObject[] { + const nestedCollections = collectionObjectsForKeys( + _collectionsByParentIndex.value[identifierKey(provider, service, collectionId)] ?? [], + ) if (retrieve === true && nestedCollections.length === 0) { console.debug(`[Mail Manager][Store] - Force fetching collections in collection "${provider}:${service}:${collectionId}"`) const sources: SourceSelector = { [provider]: { - [String(service)]: { - [String(collectionId)]: true - } + [String(service)]: true } } list(sources) @@ -114,11 +132,66 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { return nestedCollections } + function hasChildrenInCollection(provider: string, service: string | number, collectionId: string | number | null): boolean { + return (_collectionsByParentIndex.value[identifierKey(provider, service, collectionId)]?.length ?? 0) > 0 + } + /** * Create unique key for a collection */ function identifierKey(provider: string, service: string | number | null, identifier: string | number | null): string { - return `${provider}:${service ?? ''}:${identifier ?? ''}` + return `${provider}:${String(service ?? ROOT_IDENTIFIER)}:${String(identifier ?? ROOT_IDENTIFIER)}` + } + + function collectionObjectsForKeys(collectionKeys: string[]): CollectionObject[] { + return collectionKeys + .map(collectionKey => _collections.value[collectionKey]) + .filter((collection): collection is CollectionObject => collection !== undefined) + } + + function addIndexEntry(index: Record, indexKey: string, collectionKey: string) { + const existing = index[indexKey] ?? [] + + if (existing.includes(collectionKey)) { + return + } + + index[indexKey] = [...existing, collectionKey] + } + + function removeIndexEntry(index: Record, indexKey: string, collectionKey: string) { + const existing = index[indexKey] + + if (!existing) { + return + } + + const filtered = existing.filter(existingKey => existingKey !== collectionKey) + + if (filtered.length === 0) { + delete index[indexKey] + return + } + + index[indexKey] = filtered + } + + function indexCollection(collection: CollectionObject) { + const collectionKey = identifierKey(collection.provider, collection.service, collection.identifier) + const serviceIndexKey = identifierKey(collection.provider, collection.service, SERVICE_INDEX_IDENTIFIER) + const parentIndexKey = identifierKey(collection.provider, collection.service, collection.collection) + + addIndexEntry(_collectionsByServiceIndex.value, serviceIndexKey, collectionKey) + addIndexEntry(_collectionsByParentIndex.value, parentIndexKey, collectionKey) + } + + function deindexCollection(collection: CollectionObject) { + const collectionKey = identifierKey(collection.provider, collection.service, collection.identifier) + const serviceIndexKey = identifierKey(collection.provider, collection.service, SERVICE_INDEX_IDENTIFIER) + const parentIndexKey = identifierKey(collection.provider, collection.service, collection.collection) + + removeIndexEntry(_collectionsByServiceIndex.value, serviceIndexKey, collectionKey) + removeIndexEntry(_collectionsByParentIndex.value, parentIndexKey, collectionKey) } // Actions @@ -143,6 +216,12 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { Object.entries(providerServices).forEach(([_serviceId, serviceCollections]) => { Object.entries(serviceCollections).forEach(([_collectionId, collectionObj]) => { const key = identifierKey(collectionObj.provider, collectionObj.service, collectionObj.identifier) + const previousCollection = _collections.value[key] + + if (previousCollection) { + deindexCollection(previousCollection) + } + collections[key] = collectionObj }) }) @@ -150,6 +229,9 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { // Merge retrieved collections into state _collections.value = { ..._collections.value, ...collections } + Object.values(collections).forEach(collectionObj => { + indexCollection(collectionObj) + }) console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(collections).length, 'collections') return collections @@ -177,7 +259,14 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { // Merge fetched collection into state const key = identifierKey(response.provider, response.service, response.identifier) + const previousCollection = _collections.value[key] + + if (previousCollection) { + deindexCollection(previousCollection) + } + _collections.value[key] = response + indexCollection(response) console.debug('[Mail Manager][Store] - Successfully fetched collection:', key) return response @@ -234,6 +323,7 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { // Merge created collection into state const key = identifierKey(response.provider, response.service, response.identifier) _collections.value[key] = response + indexCollection(response) console.debug('[Mail Manager][Store] - Successfully created collection:', key) return response @@ -267,7 +357,14 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { // Merge updated collection into state const key = identifierKey(response.provider, response.service, response.identifier) + const previousCollection = _collections.value[key] + + if (previousCollection) { + deindexCollection(previousCollection) + } + _collections.value[key] = response + indexCollection(response) console.debug('[Mail Manager][Store] - Successfully updated collection:', key) return response @@ -295,6 +392,12 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { // Remove deleted collection from state const key = identifierKey(provider, service, identifier) + const previousCollection = _collections.value[key] + + if (previousCollection) { + deindexCollection(previousCollection) + } + delete _collections.value[key] console.debug('[Mail Manager][Store] - Successfully deleted collection:', key) @@ -317,6 +420,7 @@ export const useCollectionsStore = defineStore('mailCollectionsStore', () => { collectionsByService, collectionsForService, collectionsInCollection, + hasChildrenInCollection, // Actions collection, list,