feat(activity): mises à jour temps réel des jobs via Mercure

- Ajoute jobId dans ChapterScrapingStarted et ChapterScrapingFailed
- Publie job.created (PENDING) depuis ScrapeChapterStateProcessor
- Publie job.status_changed (in_progress/completed/failed) depuis ScrapingEventSubscriber
- Gère job.created et job.status_changed dans activityStore : ajout instantané et suppression différée (1.5s)
This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-17 16:19:40 +01:00
parent a7e6879e83
commit 07d1b2daed
7 changed files with 87 additions and 6 deletions

View File

@@ -45,7 +45,7 @@ readonly class ScrapeChapterHandler
$job->start();
$this->jobRepository->save($job);
$this->eventBus->dispatch(new ChapterScrapingStarted($manga->getTitle(), $chapter->chapterNumber));
$this->eventBus->dispatch(new ChapterScrapingStarted($job->id, $manga->getTitle(), $chapter->chapterNumber));
$sources = $this->getSourcesToTry($manga);
$slugsToTry = array_merge([$manga->getSlug()], $manga->getAlternativeSlugs());
@@ -106,7 +106,7 @@ readonly class ScrapeChapterHandler
$errorMessage = $lastException?->getMessage() ?? 'Failed to scrape chapter from all available sources';
$job->fail($errorMessage);
$this->jobRepository->save($job);
$this->eventBus->dispatch(new ChapterScrapingFailed($chapter->mangaId, $chapter->chapterNumber, $errorMessage));
$this->eventBus->dispatch(new ChapterScrapingFailed($job->id, $chapter->mangaId, $chapter->chapterNumber, $errorMessage));
}
}

View File

@@ -5,12 +5,18 @@ namespace App\Domain\Scraping\Domain\Event;
readonly class ChapterScrapingFailed
{
public function __construct(
private string $jobId,
private string $mangaId,
private string $chapterNumber,
private string $reason
) {
}
public function getJobId(): string
{
return $this->jobId;
}
public function getMangaId(): string
{
return $this->mangaId;

View File

@@ -5,11 +5,17 @@ namespace App\Domain\Scraping\Domain\Event;
class ChapterScrapingStarted
{
public function __construct(
private readonly string $jobId,
private readonly string $mangaTitle,
private readonly float $chapterNumber,
) {
}
public function getJobId(): string
{
return $this->jobId;
}
public function getMangaTitle(): string
{
return $this->mangaTitle;

View File

@@ -9,6 +9,8 @@ use App\Domain\Scraping\Domain\Model\ScrapingJob;
use App\Domain\Scraping\Infrastructure\ApiPlatform\Dto\ScrapeChapterRequest;
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
use Symfony\Component\Messenger\MessageBusInterface;
final class ScrapeChapterStateProcessor implements ProcessorInterface
@@ -16,6 +18,7 @@ final class ScrapeChapterStateProcessor implements ProcessorInterface
public function __construct(
private readonly MessageBusInterface $commandBus,
private readonly JobRepositoryInterface $jobRepository,
private readonly HubInterface $hub,
) {
}
@@ -29,6 +32,20 @@ final class ScrapeChapterStateProcessor implements ProcessorInterface
$job->context['chapterId'] = $data->chapterId;
$this->jobRepository->save($job);
$this->hub->publish(new Update(
'jobs/activity',
json_encode([
'type' => 'job.created',
'id' => $job->id,
'type_job' => $job->type,
'status' => $job->status->value,
'createdAt' => $job->createdAt->format('c'),
'context' => $job->context,
'attempts' => $job->attempts,
'maxAttempts' => $job->maxAttempts,
])
));
$this->commandBus->dispatch(new ScrapeChapter($data->chapterId, $jobId));
}
}

View File

@@ -50,11 +50,13 @@ class ScrapingEventSubscriber implements EventSubscriberInterface
#[AsMessageHandler]
public function onChapterScrapingStarted(ChapterScrapingStarted $event): void
{
$chapterNumber = $event->getChapterNumber();
$mangaTitle = $event->getMangaTitle();
$this->hub->publish(new Update(
'jobs/activity',
json_encode(['type' => 'job.status_changed', 'jobId' => $event->getJobId(), 'status' => 'in_progress'])
));
$this->notification->sendInfo(
sprintf('Scraping du chapitre %s de "%s" démarré', $chapterNumber, $mangaTitle)
sprintf('Scraping du chapitre %s de "%s" démarré', $event->getChapterNumber(), $event->getMangaTitle())
);
}
@@ -101,6 +103,11 @@ class ScrapingEventSubscriber implements EventSubscriberInterface
$update = new Update($topics, json_encode($data));
$this->hub->publish($update);
$this->hub->publish(new Update(
'jobs/activity',
json_encode(['type' => 'job.status_changed', 'jobId' => $jobId, 'status' => 'completed'])
));
$mangaTitle = $job->context['mangaTitle'] ?? 'manga inconnu';
$this->notification->sendSuccess(
sprintf('Chapitre %s de "%s" scrappé avec succès', $chapter->chapterNumber, $mangaTitle)
@@ -110,6 +117,11 @@ class ScrapingEventSubscriber implements EventSubscriberInterface
#[AsMessageHandler]
public function onChapterScrapingFailed(ChapterScrapingFailed $event): void
{
$this->hub->publish(new Update(
'jobs/activity',
json_encode(['type' => 'job.status_changed', 'jobId' => $event->getJobId(), 'status' => 'failed'])
));
$this->logger->info('ChapterScrapingFailed reçu pour mangaId: ' . $event->getMangaId() . ', chapter: ' . $event->getChapterNumber());
$data = [