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

@@ -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']);
}
}