feat: streaming entities

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-02-21 10:06:07 -05:00
parent e5eeeeb546
commit f520b8e5ac
6 changed files with 323 additions and 24 deletions

View File

@@ -6,7 +6,7 @@ import { ref, computed, readonly } from 'vue'
import { defineStore } from 'pinia'
import { entityService } from '../services'
import { EntityObject } from '../models'
import type { EntityTransmitRequest, EntityTransmitResponse } from '../types/entity'
import type { EntityTransmitRequest, EntityTransmitResponse, EntityStreamRequest } from '../types/entity'
import type { SourceSelector, ListFilter, ListSort, ListRange } from '../types/common'
export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
@@ -103,26 +103,16 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
async function list(sources?: SourceSelector, filter?: ListFilter, sort?: ListSort, range?: ListRange): Promise<Record<string, EntityObject>> {
transceiving.value = true
try {
const response = await entityService.list({ sources, filter, sort, range })
const added: Record<string, EntityObject> = {}
// Flatten nested structure: provider:service:collection:entity -> "provider:service:collection:entity": object
const entities: Record<string, EntityObject> = {}
Object.entries(response).forEach(([providerId, providerServices]) => {
Object.entries(providerServices).forEach(([serviceId, serviceCollections]) => {
Object.entries(serviceCollections).forEach(([collectionId, collectionEntities]) => {
Object.entries(collectionEntities).forEach(([entityId, entityData]) => {
const key = identifierKey(providerId, serviceId, collectionId, entityId)
entities[key] = entityData
})
})
})
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
})
// Merge retrieved entities into state
_entities.value = { ..._entities.value, ...entities }
console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(entities).length, 'entities')
return entities
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
@@ -346,6 +336,42 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
}
}
/**
* 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)
@@ -365,5 +391,6 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
delete: remove,
delta,
transmit,
stream,
}
})