/** * Entities Store */ import { ref, computed, readonly } from 'vue' import { defineStore } from 'pinia' import { entityService } from '../services' import { EntityObject } from '../models' import type { EntityDeleteResponse, EntityMoveResponse, EntityStreamRequest, EntityTransmitRequest, EntityTransmitResponse, } from '../types/entity' import type { CollectionIdentifier, EntityIdentifier, ListFilter, ListRange, ListSort, SourceSelector, } from '../types/common' export const useEntitiesStore = defineStore('mailEntitiesStore', () => { // State const _entities = ref>({}) const transceiving = ref(false) /** * Get count of entities in store */ const count = computed(() => Object.keys(_entities.value).length) /** * Check if any entities are present in store */ const has = computed(() => count.value > 0) /** * Get all entities present in store */ const entities = computed(() => Object.values(_entities.value)) /** * Get a specific entity from store, with optional retrieval * * @param provider - provider identifier * @param service - service identifier * @param collection - collection identifier * @param identifier - entity identifier * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only * * @returns Entity object or null */ function entity(provider: string, service: string | number, collection: string | number, identifier: string | number, retrieve: boolean = false): EntityObject | null { const key = identifierKey(provider, service, collection, identifier) if (retrieve === true && !_entities.value[key]) { console.debug(`[Mail Manager][Store] - Force fetching entity "${key}"`) fetch(provider, service, collection, [identifier]) } return _entities.value[key] || null } /** * Resolve an entity from cache by full entity identifier. */ function entityByIdentifier(identifier: EntityIdentifier, retrieve: boolean = false): EntityObject | null { if (retrieve === true && !_entities.value[identifier]) { console.debug(`[Mail Manager][Store] - Force fetching entity "${identifier}"`) const { provider, service, collection, identifier: id } = parseEntityIdentifier(identifier) fetch(provider, service, collection, [id]) } return _entities.value[identifier] || null } /** * Resolve multiple entities from cache by full entity identifiers. */ function entitiesByIdentifiers(identifiers: EntityIdentifier[], retrieve: boolean = false): Record { const resolved: Record = {} as Record Array.from(new Set(identifiers)).forEach(identifier => { const entity = entityByIdentifier(identifier, retrieve) if (entity) { resolved[identifier] = entity } }) return resolved } /** * Get all entities for a specific collection * * @param provider - provider identifier * @param service - service identifier * @param collection - collection identifier * @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only * * @returns Array of entity objects */ function entitiesForCollection(provider: string, service: string | number, collection: string | number, retrieve: boolean = false): EntityObject[] { const collectionKeyPrefix = `${provider}:${service}:${collection}:` const collectionEntities = Object.entries(_entities.value) .filter(([key]) => key.startsWith(collectionKeyPrefix)) .map(([_, entity]) => entity) if (retrieve === true && collectionEntities.length === 0) { console.debug(`[Mail Manager][Store] - Force fetching entities for collection "${provider}:${service}:${collection}"`) const sources: SourceSelector = { [provider]: { [String(service)]: { [String(collection)]: true } } } list(sources) } return collectionEntities } /** * Create unique key for an entity */ function identifierKey(provider: string, service: string | number, collection: string | number, identifier: string | number): string { return `${provider}:${service}:${collection}:${identifier}` } /** * Parse a full entity identifier into its components. */ function parseEntityIdentifier(identifier: EntityIdentifier): { provider: string service: string collection: string identifier: string } { const [provider, service, collection, entity] = identifier.split(':', 4) return { provider, service, collection, identifier: entity, } } // Actions /** * Retrieve all or specific entities, optionally filtered by source selector * * @param sources - optional source selector * @param filter - optional list filter * @param sort - optional list sort * @param range - optional list range * * @returns Promise with entity object list keyed by identifier */ async function list(sources?: SourceSelector, filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise> { transceiving.value = true try { const added: Record = {} await entityService.stream({ sources, filter, sort, range }, (entity: EntityObject) => { const key = identifierKey(entity.provider, entity.service, entity.collection, entity.identifier) _entities.value[key] = entity added[key] = entity }) console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(added).length, 'entities') return added } catch (error: any) { console.error('[Mail Manager][Store] - Failed to retrieve entities:', error) throw error } finally { transceiving.value = false } } /** * Retrieve specific entities by provider, service, collection, and identifiers * * @param provider - provider identifier * @param service - service identifier * @param collection - collection identifier * @param identifiers - array of entity identifiers to fetch * * @returns Promise with entity objects keyed by identifier */ async function fetch(provider: string, service: string | number, collection: string | number, identifiers: (string | number)[]): Promise> { transceiving.value = true try { const response = await entityService.fetch({ provider, service, collection, identifiers }) // Merge fetched entities into state const entities: Record = {} Object.entries(response).forEach(([identifier, entityData]) => { const key = identifierKey(provider, service, collection, identifier) entities[key] = entityData _entities.value[key] = entityData }) console.debug('[Mail Manager][Store] - Successfully fetched', Object.keys(entities).length, 'entities') return entities } catch (error: any) { console.error('[Mail Manager][Store] - Failed to fetch entities:', error) throw error } finally { transceiving.value = false } } /** * Retrieve entity availability status for a given source selector * * @param sources - source selector to check availability for * * @returns Promise with entity availability status */ async function extant(sources: SourceSelector) { transceiving.value = true try { const response = await entityService.extant({ sources }) console.debug('[Mail Manager][Store] - Successfully checked entity availability') return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to check entity availability:', error) throw error } finally { transceiving.value = false } } /** * Retrieve delta changes for entities * * @param sources - source selector for delta check * * @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(sources: SourceSelector) { transceiving.value = true try { const response = await entityService.delta({ sources }) // Process delta and update store Object.entries(response).forEach(([provider, providerData]) => { // Skip if no changes for provider if (providerData === false) return Object.entries(providerData).forEach(([service, serviceData]) => { // Skip if no changes for service if (serviceData === false) return Object.entries(serviceData).forEach(([collection, collectionData]) => { // Skip if no changes for collection if (collectionData === false) return // Process deletions (remove from store) if (collectionData.deletions && collectionData.deletions.length > 0) { collectionData.deletions.forEach((identifier) => { const key = identifierKey(provider, service, collection, identifier) delete _entities.value[key] }) } // Note: additions and modifications contain only identifiers // The caller should fetch full entities using the fetch() method }) }) }) console.debug('[Mail Manager][Store] - Successfully processed delta changes') return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to process delta:', error) throw error } finally { transceiving.value = false } } /** * Create a new entity with given provider, service, collection, and data * * @param provider - provider identifier for the new entity * @param service - service identifier for the new entity * @param collection - collection identifier for the new entity * @param data - entity properties for creation * * @returns Promise with created entity object */ async function create(provider: string, service: string | number, collection: string | number, data: any): Promise { transceiving.value = true try { const response = await entityService.create({ provider, service, collection, properties: data }) // Add created entity to state const key = identifierKey(response.provider, response.service, response.collection, response.identifier) _entities.value[key] = response console.debug('[Mail Manager][Store] - Successfully created entity:', key) return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to create entity:', error) throw error } finally { transceiving.value = false } } /** * Update an existing entity with given provider, service, collection, identifier, and data * * @param provider - provider identifier for the entity to update * @param service - service identifier for the entity to update * @param collection - collection identifier for the entity to update * @param identifier - entity identifier for the entity to update * @param data - entity properties for update * * @returns Promise with updated entity object */ async function update(provider: string, service: string | number, collection: string | number, identifier: string | number, data: any): Promise { transceiving.value = true try { const response = await entityService.update({ provider, service, collection, identifier, properties: data }) // Update entity in state const key = identifierKey(response.provider, response.service, response.collection, response.identifier) _entities.value[key] = response console.debug('[Mail Manager][Store] - Successfully updated entity:', key) return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to update entity:', error) throw error } finally { transceiving.value = false } } /** * Delete entities by their identifiers. * * Removes successfully deleted entities from the local store. * * @param sources - entity identifiers to delete * * @returns Promise with deletion results keyed by source identifier */ async function remove(sources: EntityIdentifier[]): Promise { transceiving.value = true try { const response = await entityService.delete({ sources }) const successes: EntityIdentifier[] = [] const failures: EntityIdentifier[] = [] Object.entries(response).forEach(([sourceIdentifier, result]) => { if (!result.disposition || result.disposition === 'error') { console.warn(`[Mail Manager][Store] - Entity move on "${sourceIdentifier}" returned an error: ${result.error})`) failures.push(sourceIdentifier) return } if (!result.disposition || (result.disposition !== 'moved' && result.disposition !== 'deleted')) { console.warn(`[Mail Manager][Store] - Entity move on "${sourceIdentifier}" returned invalid disposition: ${result.disposition})`) failures.push(sourceIdentifier) return } const cachedEntity = _entities.value[sourceIdentifier] if (!cachedEntity) { return } if (result.disposition === 'moved') { const mutation = parseEntityIdentifier(result.mutation?.identifier || sourceIdentifier) const movedEntity = cachedEntity.clone().fromJson({ ...cachedEntity.toJson(), provider: mutation.provider, service: mutation.service, collection: mutation.collection, identifier: mutation.identifier, }) const key = identifierKey(mutation.provider, mutation.service, mutation.collection, mutation.identifier) _entities.value[key] = movedEntity } delete _entities.value[sourceIdentifier] successes.push(sourceIdentifier) }) console.debug('[Mail Manager][Store] - Successfully deleted', Object.keys(response).length, 'entities') return [successes, failures] } catch (error: any) { console.error('[Mail Manager][Store] - Failed to delete entities:', error) throw error } finally { transceiving.value = false } } /** * Move entities to another collection. * * Updates local store keys for successfully moved entities when they are * already present in cache. * * @param target - target collection identifier * @param sources - source entity identifiers * * @returns Promise with move results keyed by source identifier */ async function move(target: CollectionIdentifier, sources: EntityIdentifier[]): Promise { transceiving.value = true try { const response = await entityService.move({ target, sources }) const successes: EntityIdentifier[] = [] const failures: EntityIdentifier[] = [] Object.entries(response).forEach(([sourceIdentifier, result]) => { if (!result.disposition || result.disposition === 'error') { console.warn(`[Mail Manager][Store] - Entity move on "${sourceIdentifier}" returned an error: ${result.error})`) failures.push(sourceIdentifier) return } if (!result.disposition || result.disposition !== 'moved') { console.warn(`[Mail Manager][Store] - Entity move on "${sourceIdentifier}" returned invalid disposition: ${result.disposition})`) failures.push(sourceIdentifier) return } const cachedEntity = _entities.value[sourceIdentifier] if (!cachedEntity) { return } const mutation = parseEntityIdentifier(result.mutation?.identifier || sourceIdentifier) const movedEntity = cachedEntity.clone().fromJson({ ...cachedEntity.toJson(), provider: mutation.provider, service: mutation.service, collection: mutation.collection, identifier: mutation.identifier, }) const movedKey = identifierKey(mutation.provider, mutation.service, mutation.collection, mutation.identifier) _entities.value[movedKey] = movedEntity delete _entities.value[sourceIdentifier] successes.push(sourceIdentifier) }) console.debug('[Mail Manager][Store] - Successfully moved', Object.keys(response).length, 'entities') return [successes, failures] } catch (error: any) { console.error('[Mail Manager][Store] - Failed to move entities:', error) throw error } finally { transceiving.value = false } } /** * Send/transmit an entity * * @param request - transmit request parameters * * @returns Promise with transmission result */ async function transmit(request: EntityTransmitRequest): Promise { transceiving.value = true try { const response = await entityService.transmit(request) console.debug('[Mail Manager][Store] - Successfully transmitted entity') return response } catch (error: any) { console.error('[Mail Manager][Store] - Failed to transmit entity:', error) throw error } finally { transceiving.value = false } } /** * Stream entities progressively, merging each entity into the store as it arrives. * * Unlike list(), which waits for the full response before updating the store, * stream() updates reactive state entity-by-entity so UI renders incrementally. * * @param sources - optional source selector * @param filter - optional list filter * @param sort - optional list sort * @param range - optional list range * * @returns Promise resolving to { total } when the stream completes */ async function stream( sources?: SourceSelector, filter?: ListFilter, sort?: ListSort, range?: ListRange ): Promise<{ total: number }> { transceiving.value = true try { const request: EntityStreamRequest = { sources, filter, sort, range } const result = await entityService.stream(request, (entity: EntityObject) => { const key = identifierKey(entity.provider, entity.service, entity.collection, entity.identifier) _entities.value[key] = entity }) console.debug('[Mail Manager][Store] - Successfully streamed', result.total, 'entities') return result } catch (error: any) { console.error('[Mail Manager][Store] - Failed to stream entities:', error) throw error } finally { transceiving.value = false } } // Return public API return { // State (readonly) transceiving: readonly(transceiving), // Getters count, has, entities, entitiesForCollection, entitiesByIdentifiers, entity, entityByIdentifier, list, fetch, extant, create, update, delete: remove, delta, move, transmit, stream, } })