feat: streaming entities
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -11,6 +11,8 @@ namespace KTXM\MailManager\Controllers;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Http\Response\StreamedNdJsonResponse;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
@@ -44,6 +46,8 @@ class DefaultController extends ControllerAbstract {
|
||||
private const ERR_INVALID_IDENTIFIERS = 'Invalid parameter: identifiers must be an array';
|
||||
private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array';
|
||||
|
||||
private const STREAM_FLUSH_INTERVAL = 1;
|
||||
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
@@ -62,7 +66,7 @@ class DefaultController extends ControllerAbstract {
|
||||
* "data": {...}
|
||||
* }
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @return Response
|
||||
*/
|
||||
#[AuthenticatedRoute('/v1', name: 'mail.manager.v1', methods: ['POST'])]
|
||||
public function index(
|
||||
@@ -71,7 +75,7 @@ class DefaultController extends ControllerAbstract {
|
||||
string|null $operation = null,
|
||||
array|null $data = null,
|
||||
string|null $user = null
|
||||
): JsonResponse {
|
||||
): Response {
|
||||
|
||||
// authorize request
|
||||
$tenantId = $this->tenantIdentity->identifier();
|
||||
@@ -80,7 +84,12 @@ class DefaultController extends ControllerAbstract {
|
||||
try {
|
||||
|
||||
if ($operation !== null) {
|
||||
$result = $this->processOperation($tenantId, $userId, $operation, $data ?? [], []);
|
||||
$result = $this->processOperation($tenantId, $userId, $operation, $data ?? [], $version, $transaction);
|
||||
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
@@ -110,7 +119,7 @@ class DefaultController extends ControllerAbstract {
|
||||
/**
|
||||
* Process a single operation
|
||||
*/
|
||||
private function processOperation(string $tenantId, string $userId, string $operation, array $data): mixed {
|
||||
private function processOperation(string $tenantId, string $userId, string $operation, array $data, int $version = 1, string $transaction = ''): mixed {
|
||||
return match ($operation) {
|
||||
// Provider operations
|
||||
'provider.list' => $this->providerList($tenantId, $userId, $data),
|
||||
@@ -144,6 +153,7 @@ class DefaultController extends ControllerAbstract {
|
||||
'entity.create' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||
'entity.update' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||
'entity.delete' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||
'entity.stream' => $this->entityStream($tenantId, $userId, $data, $version, $transaction),
|
||||
'entity.delta' => $this->entityDelta($tenantId, $userId, $data),
|
||||
'entity.move' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||
'entity.copy' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||
@@ -617,4 +627,48 @@ class DefaultController extends ControllerAbstract {
|
||||
return ['jobId' => $jobId];
|
||||
}
|
||||
|
||||
// ==================== Entity Stream ====================
|
||||
|
||||
private function entityStream(string $tenantId, string $userId, array $data, int $version, string $transaction): StreamedNdJsonResponse {
|
||||
if (!isset($data['sources'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
||||
}
|
||||
if (!is_array($data['sources'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
||||
}
|
||||
|
||||
$sources = new SourceSelector();
|
||||
$sources->jsonDeserialize($data['sources']);
|
||||
|
||||
$filter = $data['filter'] ?? null;
|
||||
$sort = $data['sort'] ?? null;
|
||||
$range = $data['range'] ?? null;
|
||||
|
||||
$entityGenerator = $this->mailManager->entityStream($tenantId, $userId, $sources, $filter, $sort, $range);
|
||||
$logger = $this->logger;
|
||||
|
||||
$lines = (function () use ($entityGenerator, $version, $transaction, $logger): \Generator {
|
||||
yield ['type' => 'meta', 'version' => $version, 'transaction' => $transaction, 'status' => 'success'];
|
||||
|
||||
$total = 0;
|
||||
try {
|
||||
foreach ($entityGenerator as $entity) {
|
||||
$entityData = is_array($entity)
|
||||
? $entity
|
||||
: (method_exists($entity, 'jsonSerialize') ? $entity->jsonSerialize() : (array) $entity);
|
||||
yield ['type' => 'entity'] + $entityData;
|
||||
$total++;
|
||||
}
|
||||
} catch (\Throwable $t) {
|
||||
$logger->error('Error streaming entities', ['exception' => $t]);
|
||||
yield ['type' => 'error', 'message' => $t->getMessage()];
|
||||
return;
|
||||
}
|
||||
|
||||
yield ['type' => 'done', 'total' => $total];
|
||||
})();
|
||||
|
||||
return new StreamedNdJsonResponse($lines, self::STREAM_FLUSH_INTERVAL, 200, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -763,6 +763,83 @@ class Manager {
|
||||
|
||||
return $responseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream entities
|
||||
*
|
||||
* @since 2026.02.01
|
||||
*
|
||||
* @param string $tenantId Tenant identifier
|
||||
* @param string $userId User identifier
|
||||
* @param SourceSelector $sources Message sources with collection identifiers
|
||||
* @param array|null $filter Message filter
|
||||
* @param array|null $sort Message sort
|
||||
* @param array|null $range Message range/pagination
|
||||
*
|
||||
* @return \Generator<EntityBaseInterface> Yields each entity as it is retrieved
|
||||
*/
|
||||
public function entityStream(string $tenantId, string $userId, SourceSelector $sources, array|null $filter = null, array|null $sort = null, array|null $range = null): \Generator {
|
||||
// retrieve providers
|
||||
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||
// retrieve services for each provider
|
||||
foreach ($providers as $provider) {
|
||||
$serviceSelector = $sources[$provider->identifier()];
|
||||
$servicesSelected = $provider->serviceList($tenantId, $userId, $serviceSelector->identifiers());
|
||||
/** @var ServiceBaseInterface $service */
|
||||
foreach ($servicesSelected as $service) {
|
||||
// retrieve collections for each service
|
||||
$collectionSelector = $serviceSelector[$service->identifier()];
|
||||
$collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
|
||||
if ($collectionSelected === []) {
|
||||
$collections = $service->collectionList('');
|
||||
$collectionSelected = array_map(
|
||||
fn($collection) => $collection->identifier(),
|
||||
$collections
|
||||
);
|
||||
}
|
||||
if ($collectionSelected === []) {
|
||||
continue;
|
||||
}
|
||||
// construct filter for entities
|
||||
$entityFilter = null;
|
||||
if ($filter !== null && $filter !== []) {
|
||||
$entityFilter = $service->entityListFilter();
|
||||
foreach ($filter as $attribute => $value) {
|
||||
$entityFilter->condition($attribute, $value);
|
||||
}
|
||||
}
|
||||
// construct sort for entities
|
||||
$entitySort = null;
|
||||
if ($sort !== null && $sort !== []) {
|
||||
$entitySort = $service->entityListSort();
|
||||
foreach ($sort as $attribute => $direction) {
|
||||
$entitySort->condition($attribute, $direction);
|
||||
}
|
||||
}
|
||||
// construct range for entities
|
||||
$entityRange = null;
|
||||
if ($range !== null && $range !== [] && isset($range['type'])) {
|
||||
$entityRange = $service->entityListRange(RangeType::from($range['type']));
|
||||
if ($entityRange->type() === RangeType::TALLY) {
|
||||
/** @var IRangeTally $entityRange */
|
||||
if (isset($range['anchor'])) {
|
||||
$entityRange->setAnchor(RangeAnchorType::from($range['anchor']));
|
||||
}
|
||||
if (isset($range['position'])) {
|
||||
$entityRange->setPosition($range['position']);
|
||||
}
|
||||
if (isset($range['tally'])) {
|
||||
$entityRange->setTally($range['tally']);
|
||||
}
|
||||
}
|
||||
}
|
||||
// yield entities for each collection individually
|
||||
foreach ($collectionSelected as $collectionId) {
|
||||
yield from $service->entityListStream($collectionId, $entityFilter, $entitySort, $entityRange, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch specific messages
|
||||
|
||||
Reference in New Issue
Block a user