/** * Services Store */ import { ref, computed, readonly } from 'vue' import { defineStore } from 'pinia' import { serviceService } from '../services' import { ServiceObject } from '../models/service' import type { CollectionIdentifier, ServiceIdentifier, ServiceLocation, ServiceIdentity, ServiceInterface, } from '../types' import { Location, Identity } from '@/models' export const useServicesStore = defineStore('mailServicesStore', () => { // State const _services = ref>({}) const transceiving = ref(false) /** * Get count of services in store */ const count = computed(() => Object.keys(_services.value).length) /** * Check if any services are present in store */ const has = computed(() => count.value > 0) /** * Get all services present in store */ const services = computed(() => Object.values(_services.value)) /** * Get all enabled services present in store */ const servicesEnabled = computed(() => services.value.filter(service => service.enabled)) /** * Get all services present in store grouped by provider */ const servicesByProvider = computed(() => { const groups: Record = {} Object.values(_services.value).forEach((service) => { if (!groups[service.provider]) { groups[service.provider] = [] } groups[service.provider].push(service) }) return groups }) /** * Get a specific service from store, with optional retrieval * * @param provider - provider identifier * @param identifier - service identifier * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only * * @returns Service object or null */ function service(provider: string, identifier: string | number, retrieve: boolean = false): ServiceObject | null { return serviceByIdentifier(identifierKey(provider, identifier), retrieve) } /** * Get a service from store by its unique identifier, with optional retrieval * * @param identifier - unique service identifier * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only * @returns Service object or null */ function serviceByIdentifier(identifier: ServiceIdentifier, retrieve: boolean = false): ServiceObject | null { if (retrieve === true && !_services.value[identifier]) { console.debug(`[Mail Manager][Store] - Force fetching service "${identifier}"`) const separatorIndex = identifier.indexOf(':') const provider = identifier.slice(0, separatorIndex) const serviceIdentifier = identifier.slice(separatorIndex + 1) void fetch(provider, serviceIdentifier) } return _services.value[identifier] ?? null } /** * Get a service from store matching a given address, with optional retrieval * * @param address - email address to match against primary and secondary addresses * @param retrieve - Retrieve behavior: true = fetch if missing, false = cache only * * @returns Service object or null */ function serviceForAddress(address: string, retrieve: boolean = false): ServiceObject | null { const service = Object.values(_services.value).find(s => s.primaryAddress?.matches(address) || s.secondaryAddresses.some(candidate => candidate.matches(address)), ) if (retrieve === true && !service) { console.debug(`[Mail Manager][Store] - No service found for address "${address}", discovery may be needed`) // TODO: Implement retrieving service by address } return service || null } /** * Unique key for a service */ function identifierKey(provider: string, identifier: string | number | null): ServiceIdentifier { return `${provider}:${identifier ?? ''}` as ServiceIdentifier } // Actions /** * Retrieve all or specific services, optionally filtered by provider/service identifiers * * @param targets - optional array of provider:service (or provider:service:collection) identifiers * * @returns Promise with service object list keyed by provider and service identifier */ async function list(targets?: ServiceIdentifier[] | CollectionIdentifier[]): Promise> { transceiving.value = true try { const response = await serviceService.list({ targets }) // Flatten nested structure: provider-id: { service-id: object } -> "provider-id:service-id": object const services: Record = {} Object.entries(response).forEach(([_providerId, providerServices]) => { Object.entries(providerServices).forEach(([_serviceId, serviceObj]) => { const key = identifierKey(serviceObj.provider, serviceObj.identifier) services[key] = serviceObj }) }) // Merge retrieved services into state _services.value = { ..._services.value, ...services } console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(services).length, 'services') return services } catch (error: any) { console.error('[Mail Manager][Store] - Failed to retrieve services:', error) throw error } finally { transceiving.value = false } } /** * Retrieve a specific service by provider and identifier * * @param provider - provider identifier * @param identifier - service identifier * * @returns Promise with service object */ async function fetch(provider: string, identifier: string | number): Promise { transceiving.value = true try { const service = await serviceService.fetch({ provider, identifier }) // Merge fetched service into state const key = identifierKey(service.provider, service.identifier) _services.value[key] = service console.debug('[Mail Manager][Store] - Successfully fetched service:', key) return service } catch (error: any) { console.error('[Mail Manager][Store] - Failed to fetch service:', error) throw error } finally { transceiving.value = false } } /** * Retrieve service availability status for the given service identifiers * * @param targets - array of provider:service identifiers to check availability for * * @returns Promise with service availability status */ async function extant(targets: ServiceIdentifier[]) { transceiving.value = true try { const response = await serviceService.extant({ targets }) console.debug('[Mail Manager][Store] - Successfully checked', targets?.length ?? 0, 'services') return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to check services:', error) throw error } finally { transceiving.value = false } } /** * Create a new service with given provider and data * * @param provider - provider identifier for the new service * @param data - partial service data for creation * * @returns Promise with created service object */ async function create(provider: string, data: Partial): Promise { transceiving.value = true try { const service = await serviceService.create({ provider, data }) // Merge created service into state const key = identifierKey(service.provider, service.identifier) _services.value[key] = service console.debug('[Mail Manager][Store] - Successfully created service:', key) return service } catch (error: any) { console.error('[Mail Manager][Store] - Failed to create service:', error) throw error } finally { transceiving.value = false } } /** * Update an existing service with given provider, identifier, and data * * @param provider - provider identifier for the service to update * @param identifier - service identifier for the service to update * @param delta - whether the update is a delta (partial) update or a full replacement * @param data - service data for update * * @returns Promise with updated service object */ async function update(provider: string, identifier: string | number, delta: boolean, data: ServiceObject | Partial): Promise { transceiving.value = true try { // convert ServiceObject to JSON if needed let payload: Partial if (data instanceof ServiceObject) { payload = data.toJson(delta) } else { payload = data } const service = await serviceService.update({ provider, identifier, delta, data: payload }) // Merge updated service into state const key = identifierKey(service.provider, service.identifier) _services.value[key] = service console.debug('[Mail Manager][Store] - Successfully updated service:', key) return service } catch (error: any) { console.error('[Mail Manager][Store] - Failed to update service:', error) throw error } finally { transceiving.value = false } } /** * Delete a service by provider and identifier * * @param provider - provider identifier for the service to delete * @param identifier - service identifier for the service to delete * * @returns Promise with deletion result */ async function remove(provider: string, identifier: string | number): Promise { transceiving.value = true try { await serviceService.delete({ provider, identifier }) // Remove deleted service from state const key = identifierKey(provider, identifier) delete _services.value[key] console.debug('[Mail Manager][Store] - Successfully deleted service:', key) } catch (error: any) { console.error('[Mail Manager][Store] - Failed to delete service:', error) throw error } finally { transceiving.value = false } } /** * Discover services based on provided parameters * * @param identity - optional service identity for discovery * @param secret - optional secret for discovery * @param location - optional location for discovery * @param provider - optional provider identifier for discovery * @param onService - called for each discovered service as it arrives * * @returns Promise resolving to { total } when the stream completes */ async function discover( identity: string, secret: string | undefined, location: string | undefined, provider: string | undefined, onService?: (service: ServiceObject) => void, ): Promise<{ total: number }> { transceiving.value = true try { const result = await serviceService.discover( { identity, secret, location, provider }, (service: ServiceObject) => { onService?.(service) } ) console.debug('[Mail Manager][Store] - Successfully discovered', result.total, 'services') return result } catch (error: any) { console.error('[Mail Manager][Store] - Failed to discover service:', error) throw error } finally { transceiving.value = false } } /** * Test service connectivity and configuration * * @param provider - provider identifier for the service to test * @param identifier - optional service identifier for testing existing service * @param location - optional service location for testing new configuration * @param identity - optional service identity for testing new configuration * * @return Promise with test results */ async function test( provider: string, identifier?: string | number | null, location?: ServiceLocation | Location | null, identity?: ServiceIdentity | Identity | null, ): Promise { transceiving.value = true try { if (provider === undefined || provider === null) { console.error('[Mail Manager][Store] - Provider is required for testing service') throw new Error('Provider is required for testing service') } if (identifier === undefined && (location === undefined || location === null) && (identity === undefined || identity === null)) { console.error('[Mail Manager][Store] - Either identifier or location/identity is required for testing service') throw new Error('Either identifier or location/identity is required for testing service') } if (location && location instanceof Location) { location = location.toJson() } if (identity && identity instanceof Identity) { identity = identity.toJson() } const response = await serviceService.test({ provider, identifier, location, identity }) console.debug('[Mail Manager][Store] - Successfully tested service:', provider, identifier || location) return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to test service:', error) throw error } finally { transceiving.value = false } } // Return public API return { // State (readonly) transceiving: readonly(transceiving), // Getters count, has, services, servicesEnabled, servicesByProvider, // Actions service, serviceByIdentifier, serviceForAddress, list, fetch, extant, create, update, delete: remove, discover, test, } })