Files
mail_manager/src/stores/servicesStore.ts
T
2026-06-20 00:00:28 -04:00

412 lines
13 KiB
TypeScript

/**
* 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<Record<string, ServiceObject>>({})
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<string, ServiceObject[]> = {}
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<Record<string, ServiceObject>> {
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<string, ServiceObject> = {}
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<ServiceObject> {
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<ServiceInterface>): Promise<ServiceObject> {
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<ServiceInterface>): Promise<ServiceObject> {
transceiving.value = true
try {
// convert ServiceObject to JSON if needed
let payload: Partial<ServiceInterface>
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<any> {
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<any> {
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,
}
})