/** * Entities Store */ import { ref, computed, readonly } from 'vue' import { defineStore } from 'pinia' import { entityService } from '../services' import { EntityObject, MessageObject } from '../models' import type { EntityListStreamRequest, EntityTransmitRequest, EntityTransmitResponse, } from '../types/entity' import type { CollectionIdentifier, EntityIdentifier, ListFilter, ListRange, ListSort, } from '../types/common' import type { MessageInterface } from '@/types/message' 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(target: EntityIdentifier, retrieve: boolean = false): EntityObject | null { if (retrieve === true && !_entities.value[target]) { console.debug(`[Mail Manager][Store] - Force fetching entity "${target}"`) fetch([target]) } return _entities.value[target] || null } /** * 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(target: CollectionIdentifier, retrieve: boolean = false): EntityObject[] { const collectionEntities = Object.entries(_entities.value) .filter(([key]) => key.startsWith(target)) .map(([_, entity]) => entity) if (retrieve === true && collectionEntities.length === 0) { console.debug(`[Mail Manager][Store] - Force fetching entities for collection "${target}"`) list([target]) } return collectionEntities } // 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: CollectionIdentifier[], filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise> { transceiving.value = true try { const entities: Record = {} await entityService.listStream({ sources, filter, sort, range }, (entity: EntityObject) => { _entities.value[entity.identifier] = entity entities[entity.identifier] = entity }) console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(entities).length, 'entities') return entities } 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(targets: EntityIdentifier[]): Promise> { transceiving.value = true try { const response = await entityService.fetch({ targets }) // Merge fetched entities into state const entities: Record = {} Object.entries(response).forEach(([identifier, entity]) => { entities[identifier] = entity _entities.value[identifier] = entity }) 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 set of entity identifiers * * @param targets - array of entity identifiers to check availability for * * @returns Promise with entity availability status */ async function extant(targets: EntityIdentifier[]) { transceiving.value = true try { const response = await entityService.extant({ targets }) 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: CollectionIdentifier[]) { 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) => { delete _entities.value[identifier] }) } }) }) }) 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 empty entity object * * @returns New entity object instance */ function fresh(): EntityObject { return new EntityObject() } /** * Create a new entity with given collection identifier and properties * * @param target - collection identifier for the new entity * @param properties - entity properties for creation * * @returns Promise with created entity object */ async function create(target: CollectionIdentifier, properties: MessageInterface | MessageObject): Promise { transceiving.value = true try { if (properties instanceof MessageObject) { properties = properties.toJson() } const response = await entityService.create({ target, properties }) // Add created entity to state _entities.value[response.identifier] = response console.debug('[Mail Manager][Store] - Successfully created entity:', response.identifier) 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(target: EntityIdentifier, properties: MessageInterface | MessageObject): Promise { transceiving.value = true try { if (properties instanceof MessageObject) { properties = properties.toJson() } const response = await entityService.update({ target, properties }) // Update entity in state _entities.value[response.identifier] = response console.debug('[Mail Manager][Store] - Successfully updated entity:', response.identifier) 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 targets - entity identifiers to delete * * @returns Promise with deletion results keyed by target identifier */ async function remove(targets: EntityIdentifier[]): Promise<{successes: EntityIdentifier[], failures: EntityIdentifier[]}> { transceiving.value = true try { const response = await entityService.delete({ targets }) const successes: EntityIdentifier[] = [] const failures: EntityIdentifier[] = [] Object.entries(response).forEach(([targetIdentifier, result]) => { const originalIdentifier = targetIdentifier as EntityIdentifier if (!result.disposition || result.disposition === 'error') { console.warn(`[Mail Manager][Store] - Entity move on "${originalIdentifier}" returned an error: ${result.error})`) failures.push(originalIdentifier) return } if (!result.disposition || (result.disposition !== 'moved' && result.disposition !== 'deleted')) { console.warn(`[Mail Manager][Store] - Entity move on "${originalIdentifier}" returned invalid disposition: ${result.disposition})`) failures.push(originalIdentifier) return } const cachedEntity = _entities.value[originalIdentifier] if (!cachedEntity) { return } if (result.disposition === 'moved') { const movedEntity = cachedEntity.clone().fromJson({ ...cachedEntity.toJson(), collection: result.destination, identifier: result.mutation, }) _entities.value[result.mutation] = movedEntity } delete _entities.value[originalIdentifier] successes.push(originalIdentifier) }) console.debug('[Mail Manager][Store] - Successfully deleted', successes.length, 'entities') return { successes, failures } } catch (error: any) { console.error('[Mail Manager][Store] - Failed to delete entities:', error) throw error } finally { transceiving.value = false } } /** * Patch existing entities with new properties */ async function patch(properties: MessageInterface | MessageObject, targets: EntityIdentifier[]) { transceiving.value = true try { if (properties instanceof MessageObject) { properties = properties.toJson() } const response = await entityService.patch({ properties, targets }) const successes: EntityIdentifier[] = [] const failures: EntityIdentifier[] = [] Object.entries(response).forEach(([targetIdentifier, result]) => { const originalIdentifier = targetIdentifier as EntityIdentifier if (!result.disposition || result.disposition === 'error') { console.warn(`[Mail Manager][Store] - Entity patch on "${originalIdentifier}" returned an error: ${result.error})`) failures.push(originalIdentifier) return } const cachedEntity = _entities.value[originalIdentifier] if (!cachedEntity) { return } if (result.disposition === 'patched') { const cachedEntityJson = cachedEntity.toJson() const mutatedEntity = cachedEntity.clone().fromJson({ ...cachedEntityJson, properties: { ...cachedEntityJson.properties, ...properties, }, }) _entities.value[originalIdentifier] = mutatedEntity } successes.push(originalIdentifier) }) console.debug('[Mail Manager][Store] - Successfully patched', successes.length, 'entities') return { successes, failures } } catch (error: any) { console.error('[Mail Manager][Store] - Failed to patch 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<{successes: EntityIdentifier[], failures: EntityIdentifier[]}> { transceiving.value = true try { const response = await entityService.move({ target, sources }) const successes: EntityIdentifier[] = [] const failures: EntityIdentifier[] = [] Object.entries(response).forEach(([sourceIdentifier, result]) => { const originalIdentifier = sourceIdentifier as EntityIdentifier if (!result.disposition || result.disposition === 'error') { console.warn(`[Mail Manager][Store] - Entity move on "${originalIdentifier}" returned an error: ${result.error})`) failures.push(originalIdentifier) return } if (!result.disposition || result.disposition !== 'moved') { console.warn(`[Mail Manager][Store] - Entity move on "${originalIdentifier}" returned invalid disposition: ${result.disposition})`) failures.push(originalIdentifier) return } const cachedEntity = _entities.value[originalIdentifier] if (!cachedEntity) { return } const movedEntity = cachedEntity.clone().fromJson({ ...cachedEntity.toJson(), collection: result.destination, identifier: result.mutation, }) _entities.value[result.mutation] = movedEntity delete _entities.value[originalIdentifier] successes.push(originalIdentifier) }) console.debug('[Mail Manager][Store] - Successfully moved', successes.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 } } // Return public API return { // State (readonly) transceiving: readonly(transceiving), // Getters count, has, entities, entitiesForCollection, entity, list, fetch, extant, fresh, create, update, patch, delete: remove, delta, move, transmit, } })