refactor(scraping): job PENDING dès le POST HTTP, handler sans Doctrine

- ScrapingJob: mangaId/chapterNumber/sourceId optionnels (nullable) pour
  permettre la création en PENDING sans lookup DB dans le StateProcessor
- ScrapeChapter: ajoute jobId (pré-généré par le StateProcessor)
- ScrapeChapterStateProcessor: crée et persiste le job PENDING avant
  dispatch; injecte JobRepositoryInterface uniquement
- ScrapeChapterHandler: supprime EntityManagerInterface, beginTransaction/
  commit/rollback; charge le job existant via jobId, complete() sur succès
  seulement, fail() si toutes les sources échouent
- ScrapeChapterHandlerTest: pré-crée le job, passe jobId dans la commande,
  supprime le mock EntityManagerInterface
- ScrapeChapterTest: accès aux messages via static InMemoryMessageBus,
  vérifie la présence du jobId dans la commande dispatchée
This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-17 15:33:20 +01:00
parent ec4a8be934
commit fa035bfbfa
10 changed files with 252 additions and 356 deletions

View File

@@ -3,11 +3,14 @@ import { ApiJobRepository } from '../../infrastructure/api/ApiJobRepository';
const jobRepository = new ApiJobRepository();
const ACTIVE_STATUSES = ['pending', 'in_progress'];
export const useActivityStore = defineStore('activity', {
state: () => ({
jobs: [],
loading: false,
error: null,
mercureEventSource: null,
// Pagination
currentPage: 1,
totalPages: 0,
@@ -15,21 +18,15 @@ export const useActivityStore = defineStore('activity', {
limit: 20,
hasNextPage: false,
hasPreviousPage: false,
// Filtres
filter: {
status: ['pending', 'in_progress'], // Par défaut, ne montrer que les actifs
sortBy: 'createdAt',
sortOrder: 'DESC'
}
// Tri
sortBy: 'createdAt',
sortOrder: 'DESC',
}),
getters: {
activeJobs: state => state.jobs.filter(job => job.isActive()),
completedJobs: state => state.jobs.filter(job => job.isCompleted()),
failedJobs: state => state.jobs.filter(job => job.hasError()),
isLoading: state => state.loading,
hasError: state => !!state.error,
// Getters pour la pagination
paginationInfo: state => ({
currentPage: state.currentPage,
totalPages: state.totalPages,
@@ -41,44 +38,25 @@ export const useActivityStore = defineStore('activity', {
},
actions: {
/**
* Charge la liste des jobs selon les filtres actuels
* @param {number} page - Numéro de page optionnel
*/
async loadJobs(page = null) {
this.loading = true;
this.error = null;
try {
const options = {
const jobCollection = await jobRepository.getJobs({
page: page || this.currentPage,
limit: this.limit,
sortBy: this.filter.sortBy,
sortOrder: this.filter.sortOrder,
status: this.filter.status
};
sortBy: this.sortBy,
sortOrder: this.sortOrder,
status: ACTIVE_STATUSES,
});
const jobCollection = await jobRepository.getJobs(options);
// Mettre à jour les données
this.jobs = jobCollection.items;
this.currentPage = jobCollection.page;
this.total = jobCollection.total;
this.hasNextPage = jobCollection.hasNextPage;
this.hasPreviousPage = jobCollection.hasPreviousPage;
// Calculer le nombre total de pages
this.totalPages = Math.ceil(this.total / this.limit);
console.log('Store updated with:', {
jobs: this.jobs.length,
currentPage: this.currentPage,
total: this.total,
limit: this.limit,
totalPages: this.totalPages,
hasNextPage: this.hasNextPage,
hasPreviousPage: this.hasPreviousPage
});
} catch (error) {
this.error = error.message;
console.error('Error loading jobs:', error);
@@ -87,10 +65,6 @@ export const useActivityStore = defineStore('activity', {
}
},
/**
* Va à une page spécifique
* @param {number} page
*/
async goToPage(page) {
if (page >= 1 && page <= this.totalPages && page !== this.currentPage) {
this.currentPage = page;
@@ -98,39 +72,26 @@ export const useActivityStore = defineStore('activity', {
}
},
/**
* Met à jour les filtres et recharge la liste
* @param {Object} filter
*/
async updateFilter(filter) {
this.filter = { ...this.filter, ...filter };
this.currentPage = 1; // Retourner à la première page lors du changement de filtre
async updateSort(sortBy, sortOrder) {
this.sortBy = sortBy;
this.sortOrder = sortOrder;
this.currentPage = 1;
await this.loadJobs(1);
},
/**
* Met à jour la limite par page
* @param {number} limit
*/
async updateLimit(limit) {
this.limit = limit;
this.currentPage = 1; // Retourner à la première page
this.currentPage = 1;
await this.loadJobs(1);
},
/**
* Supprime un job par son ID
* @param {string} id
*/
async deleteJob(id) {
this.loading = true;
this.error = null;
try {
await jobRepository.deleteJob(id);
// Supprimer le job de la liste locale
this.jobs = this.jobs.filter(job => job.id !== id);
// Recharger la page courante pour avoir les bons totaux
await this.loadJobs(this.currentPage);
} catch (error) {
this.error = error.message;
@@ -140,17 +101,37 @@ export const useActivityStore = defineStore('activity', {
}
},
/**
* Supprime tous les jobs correspondant aux critères
* @param {Object} criteria
*/
updateJobProgress(jobId, progress) {
const job = this.jobs.find(j => j.id === jobId);
if (job) job.progress = progress;
},
subscribeMercure() {
if (this.mercureEventSource) return;
const url = new URL('/.well-known/mercure', window.location.origin);
url.searchParams.append('topic', 'jobs/activity');
this.mercureEventSource = new EventSource(url.toString());
this.mercureEventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'job.progress_updated') {
this.updateJobProgress(data.jobId, data.progress);
}
};
},
unsubscribeMercure() {
if (this.mercureEventSource) {
this.mercureEventSource.close();
this.mercureEventSource = null;
}
},
async deleteJobs(criteria = {}) {
this.loading = true;
this.error = null;
try {
const deleted = await jobRepository.deleteJobs(criteria);
// Recharger la liste après suppression
await this.loadJobs(this.currentPage);
return deleted;
} catch (error) {
@@ -160,26 +141,5 @@ export const useActivityStore = defineStore('activity', {
this.loading = false;
}
},
/**
* Supprime tous les jobs terminés
*/
async deleteCompletedJobs() {
return this.deleteJobs({ status: ['COMPLETED'] });
},
/**
* Supprime tous les jobs en erreur
*/
async deleteFailedJobs() {
return this.deleteJobs({ status: ['ERROR'] });
},
/**
* Supprime tous les jobs
*/
async deleteAllJobs() {
return this.deleteJobs({});
}
}
});